@@ -53,16 +53,17 @@ struct CodecDeleter
5353};
5454using CodecPtr = std::unique_ptr<grk_object, CodecDeleter>;
5555
56- // Create a synthetic test image and compress it to a J2K file with TLM markers
56+ // Create a synthetic test image and compress it to a J2K file
5757static bool createTestImage (const std::string& path, uint32_t width, uint32_t height,
58- uint32_t tileWidth, uint32_t tileHeight)
58+ uint32_t tileWidth, uint32_t tileHeight, bool writeTlm = true )
5959{
6060 grk_cparameters cparams{};
6161 grk_compress_set_default_params (&cparams);
6262 cparams.cod_format = GRK_FMT_J2K;
6363 cparams.t_width = tileWidth;
6464 cparams.t_height = tileHeight;
65- cparams.write_tlm = true ;
65+ cparams.tile_size_on = (tileWidth < width || tileHeight < height);
66+ cparams.write_tlm = writeTlm;
6667 cparams.numresolution = 3 ;
6768 // Lossless
6869 cparams.irreversible = false ;
@@ -362,6 +363,150 @@ static bool testLRUCacheEviction()
362363 return true ;
363364}
364365
366+ // /////////////////////////////////////////////////////////////////
367+ // Test 4: LRU eviction + re-decompress from cached SOT offsets
368+ //
369+ // Creates a non-TLM image with 16 tiles, sets max_active_tiles=4,
370+ // decompresses all tiles (causing eviction), then re-decompresses
371+ // evicted tiles and verifies pixel data matches a fresh reference.
372+ // /////////////////////////////////////////////////////////////////
373+ static std::vector<TileData> decompressTileByTile (const std::string& path,
374+ uint32_t tileCacheStrategy,
375+ uint16_t maxActiveTiles)
376+ {
377+ std::vector<TileData> result;
378+
379+ grk_decompress_parameters params{};
380+ params.core .tile_cache_strategy = tileCacheStrategy;
381+ params.core .max_active_tiles = maxActiveTiles;
382+
383+ grk_stream_params streamParams{};
384+ safe_strcpy (streamParams.file , path.data ());
385+
386+ CodecPtr codec (grk_decompress_init (&streamParams, ¶ms));
387+ if (!codec)
388+ {
389+ spdlog::error (" Failed to init decompressor" );
390+ return result;
391+ }
392+
393+ grk_header_info headerInfo{};
394+ if (!grk_decompress_read_header (codec.get (), &headerInfo))
395+ {
396+ spdlog::error (" Failed to read header" );
397+ return result;
398+ }
399+
400+ if (!grk_decompress_update (¶ms, codec.get ()))
401+ {
402+ spdlog::error (" grk_decompress_update failed" );
403+ return result;
404+ }
405+
406+ uint16_t numTiles = headerInfo.t_grid_width * headerInfo.t_grid_height ;
407+ spdlog::info (" decompressTileByTile: grid {}x{} = {} tiles" , headerInfo.t_grid_width ,
408+ headerInfo.t_grid_height , numTiles);
409+ for (uint16_t t = 0 ; t < numTiles; ++t)
410+ {
411+ if (!grk_decompress_tile (codec.get (), t))
412+ {
413+ spdlog::error (" grk_decompress_tile({}) failed" , t);
414+ return {};
415+ }
416+ }
417+
418+ // Now re-decompress all tiles (evicted tiles trigger re-decompress path)
419+ for (uint16_t t = 0 ; t < numTiles; ++t)
420+ {
421+ if (!grk_decompress_tile (codec.get (), t))
422+ {
423+ spdlog::error (" Re-decompress of tile {} failed" , t);
424+ return {};
425+ }
426+
427+ auto * tileImg = grk_decompress_get_tile_image (codec.get (), t, true );
428+ if (!tileImg)
429+ {
430+ spdlog::error (" get_tile_image({}) returned null after re-decompress" , t);
431+ return {};
432+ }
433+
434+ TileData td;
435+ td.tileIndex = t;
436+ for (uint16_t c = 0 ; c < tileImg->numcomps ; ++c)
437+ {
438+ auto & comp = tileImg->comps [c];
439+ if (!comp.data )
440+ continue ;
441+ uint32_t w = comp.w ;
442+ uint32_t h = comp.h ;
443+ auto * pixelData = static_cast <int32_t *>(comp.data );
444+ std::vector<int32_t > pixels (pixelData, pixelData + w * h);
445+ td.componentData .push_back (std::move (pixels));
446+ }
447+ result.push_back (std::move (td));
448+ }
449+
450+ return result;
451+ }
452+
453+ static bool testLRUEvictionReDecompress (const std::string& testFile)
454+ {
455+ spdlog::info (" === Test: LRU eviction + re-decompress from cached SOT offsets ===" );
456+
457+ // Reference: decompress each tile individually, no LRU
458+ auto refData = decompressTileByTile (testFile, GRK_TILE_CACHE_IMAGE, 0 );
459+ if (refData.empty ())
460+ {
461+ spdlog::error (" Reference tile-by-tile decompression produced no data" );
462+ return false ;
463+ }
464+ spdlog::info (" Reference: {} tiles captured" , refData.size ());
465+
466+ // Test: decompress with LRU, max_active_tiles=4 (forces eviction on 16-tile image)
467+ auto lruData = decompressTileByTile (testFile, GRK_TILE_CACHE_IMAGE | GRK_TILE_CACHE_LRU, 4 );
468+ if (lruData.empty ())
469+ {
470+ spdlog::error (" LRU re-decompress produced no data" );
471+ return false ;
472+ }
473+ spdlog::info (" LRU re-decompress: {} tiles captured" , lruData.size ());
474+
475+ if (refData.size () != lruData.size ())
476+ {
477+ spdlog::error (" Tile count mismatch: ref={}, lru={}" , refData.size (), lruData.size ());
478+ return false ;
479+ }
480+
481+ for (size_t i = 0 ; i < refData.size (); ++i)
482+ {
483+ auto & ref = refData[i];
484+ auto & lru = lruData[i];
485+ if (ref.tileIndex != lru.tileIndex )
486+ {
487+ spdlog::error (" Tile index mismatch at position {}" , i);
488+ return false ;
489+ }
490+ if (ref.componentData .size () != lru.componentData .size ())
491+ {
492+ spdlog::error (" Component count mismatch for tile {}" , ref.tileIndex );
493+ return false ;
494+ }
495+ for (size_t c = 0 ; c < ref.componentData .size (); ++c)
496+ {
497+ if (ref.componentData [c] != lru.componentData [c])
498+ {
499+ spdlog::error (" Pixel data mismatch for tile {} component {}" , ref.tileIndex , c);
500+ return false ;
501+ }
502+ }
503+ }
504+
505+ spdlog::info (" PASS: LRU eviction + re-decompress matches reference for all {} tiles" ,
506+ refData.size ());
507+ return true ;
508+ }
509+
365510// /////////////////////////////////////////////////////////////////
366511// Main
367512// /////////////////////////////////////////////////////////////////
@@ -395,6 +540,24 @@ int GrkLRUCacheTest::main(int argc, char** argv)
395540 std::filesystem::remove (testFile, ec);
396541 }
397542
543+ // Integration test: non-TLM image with LRU eviction + re-decompress
544+ std::string noTlmFile =
545+ (std::filesystem::temp_directory_path () / " grk_lru_no_tlm_test.j2k" ).string ();
546+ bool noTlmCreated = createTestImage (noTlmFile, 256 , 256 , 64 , 64 , false ); // 4x4 = 16 tiles, no TLM
547+ if (!noTlmCreated)
548+ {
549+ spdlog::error (" Failed to create non-TLM test image, skipping eviction test" );
550+ failures++;
551+ }
552+ else
553+ {
554+ if (!testLRUEvictionReDecompress (noTlmFile))
555+ failures++;
556+
557+ std::error_code ec;
558+ std::filesystem::remove (noTlmFile, ec);
559+ }
560+
398561 if (failures > 0 )
399562 {
400563 spdlog::error (" {} test(s) FAILED" , failures);
0 commit comments