@@ -2134,6 +2134,320 @@ static bool testPLTHeaderExtraction()
21342134 return pass;
21352135}
21362136
2137+ // /////////////////////////////////////////////////////////////////
2138+ // Test 19: Selective fetch simulation for LRCP (disjoint ranges)
2139+ //
2140+ // Creates a J2K file with LRCP progression, 2 layers, 4 resolutions.
2141+ // Computes selective fetch ranges for reduce=2 (want 2 out of 4 res).
2142+ // Verifies:
2143+ // - Ranges are disjoint (not contiguous from offset 0)
2144+ // - Range data sum < full data (bandwidth savings)
2145+ // - Assembled buffer (header + concatenated ranges) decompresses correctly
2146+ // /////////////////////////////////////////////////////////////////
2147+ static bool testSelectiveFetchSimulation ()
2148+ {
2149+ using namespace grk ;
2150+ spdlog::info (" === Test: Selective fetch simulation (LRCP disjoint) ===" );
2151+
2152+ std::string testFile =
2153+ (std::filesystem::temp_directory_path () / " grk_selective_sim.j2k" ).string ();
2154+
2155+ // Create LRCP image with 2 layers, 4 resolutions
2156+ {
2157+ grk_cparameters cparams{};
2158+ grk_compress_set_default_params (&cparams);
2159+ cparams.cod_format = GRK_FMT_J2K ;
2160+ cparams.t_width = 128 ;
2161+ cparams.t_height = 128 ;
2162+ cparams.tile_size_on = false ;
2163+ cparams.write_tlm = true ;
2164+ cparams.write_plt = true ;
2165+ cparams.numresolution = 4 ;
2166+ cparams.irreversible = false ;
2167+ cparams.numlayers = 2 ;
2168+ cparams.prog_order = GRK_LRCP ;
2169+ cparams.layer_rate [0 ] = 20 ;
2170+ cparams.layer_rate [1 ] = 0 ; // lossless
2171+
2172+ grk_image_comp comp{};
2173+ comp.dx = 1 ;
2174+ comp.dy = 1 ;
2175+ comp.w = 128 ;
2176+ comp.h = 128 ;
2177+ comp.prec = 8 ;
2178+ comp.sgnd = 0 ;
2179+
2180+ auto * image = grk_image_new (1 , &comp, GRK_CLRSPC_GRAY , true );
2181+ if (!image)
2182+ return false ;
2183+
2184+ auto * data = static_cast <int32_t *>(image->comps [0 ].data );
2185+ for (uint32_t y = 0 ; y < 128 ; ++y)
2186+ for (uint32_t x = 0 ; x < 128 ; ++x)
2187+ data[y * 128 + x] = static_cast <int32_t >((x * 7 + y * 13 ) % 256 );
2188+
2189+ grk_stream_params sp{};
2190+ safe_strcpy (sp.file , testFile.data ());
2191+
2192+ auto * codec = grk_compress_init (&sp, &cparams, image);
2193+ if (!codec)
2194+ {
2195+ grk_object_unref (&image->obj );
2196+ return false ;
2197+ }
2198+ uint64_t len = grk_compress (codec, nullptr );
2199+ grk_object_unref (codec);
2200+ grk_object_unref (&image->obj );
2201+ if (len == 0 )
2202+ {
2203+ spdlog::error (" LRCP compression failed" );
2204+ return false ;
2205+ }
2206+ spdlog::info (" Created LRCP image: 128x128, 2 layers, 4 res, {} bytes" , len);
2207+ }
2208+
2209+ // Read raw file
2210+ std::ifstream file (testFile, std::ios::binary | std::ios::ate);
2211+ if (!file)
2212+ return false ;
2213+ auto fileSize = file.tellg ();
2214+ file.seekg (0 );
2215+ std::vector<uint8_t > rawData (fileSize);
2216+ file.read (reinterpret_cast <char *>(rawData.data ()), fileSize);
2217+ file.close ();
2218+
2219+ // Find SOT
2220+ uint64_t sotOffset = 0 ;
2221+ for (size_t i = 0 ; i + 1 < rawData.size (); ++i)
2222+ {
2223+ if (rawData[i] == 0xFF && rawData[i + 1 ] == 0x90 )
2224+ {
2225+ sotOffset = i;
2226+ break ;
2227+ }
2228+ }
2229+
2230+ constexpr size_t sotLen = 12 ;
2231+ uint64_t headerStart = sotOffset + sotLen;
2232+ auto headerInfo = extractTilePartHeaderInfo (rawData.data () + headerStart,
2233+ rawData.size () - headerStart);
2234+ bool pass = true ;
2235+
2236+ if (!headerInfo.valid || headerInfo.pltLengths .empty ())
2237+ {
2238+ spdlog::error (" FAIL: Could not extract PLT from LRCP file" );
2239+ std::filesystem::remove (testFile);
2240+ return false ;
2241+ }
2242+
2243+ spdlog::info (" PLT packets: {}, SOD offset: {}" , headerInfo.pltLengths .size (),
2244+ headerInfo.sodOffset );
2245+
2246+ // Build TilePacketInfo for LRCP
2247+ // 128x128 single tile, 1 component, 4 resolutions, 2 layers
2248+ TilePacketInfo tpi;
2249+ tpi.progression = GRK_LRCP ;
2250+ tpi.numComponents = 1 ;
2251+ tpi.numLayers = 2 ;
2252+ tpi.numResolutions = {4 };
2253+ tpi.pltLengths = headerInfo.pltLengths ;
2254+
2255+ // Compute precincts per resolution
2256+ tpi.precinctsPerRes .resize (1 );
2257+ tpi.precinctsPerRes [0 ].resize (4 );
2258+ for (uint8_t r = 0 ; r < 4 ; ++r)
2259+ {
2260+ tpi.precinctsPerRes [0 ][r] = computeNumPrecincts (0 , 0 , 128 , 128 , 4 , r, 15 , 15 );
2261+ }
2262+
2263+ // Compute ranges for reduce=2 (want 2 out of 4 resolutions)
2264+ auto ranges = computeSelectiveFetchRanges (tpi, 2 );
2265+
2266+ uint64_t fullDataSize = 0 ;
2267+ for (auto len : headerInfo.pltLengths )
2268+ fullDataSize += len;
2269+
2270+ uint64_t selectiveSize = 0 ;
2271+ for (auto & r : ranges)
2272+ selectiveSize += r.length ;
2273+
2274+ spdlog::info (" Full data: {} bytes, selective: {} bytes ({:.1f}% savings)" ,
2275+ fullDataSize, selectiveSize,
2276+ 100.0 * (1.0 - (double )selectiveSize / fullDataSize));
2277+
2278+ // Verify ranges are disjoint (LRCP with 2 layers should produce > 1 range)
2279+ if (ranges.size () <= 1 )
2280+ {
2281+ spdlog::error (" FAIL: Expected disjoint ranges for LRCP with 2 layers, got {}" ,
2282+ ranges.size ());
2283+ pass = false ;
2284+ }
2285+ else
2286+ {
2287+ spdlog::info (" PASS: Got {} disjoint ranges (as expected for LRCP)" , ranges.size ());
2288+ }
2289+
2290+ // Verify savings > 0
2291+ if (selectiveSize >= fullDataSize)
2292+ {
2293+ spdlog::error (" FAIL: No bandwidth savings from selective fetch" );
2294+ pass = false ;
2295+ }
2296+ else
2297+ {
2298+ spdlog::info (" PASS: Selective fetch saves {:.1f}% bandwidth" ,
2299+ 100.0 * (1.0 - (double )selectiveSize / fullDataSize));
2300+ }
2301+
2302+ // Verify ranges don't exceed data bounds
2303+ for (auto & r : ranges)
2304+ {
2305+ if (r.end () > fullDataSize)
2306+ {
2307+ spdlog::error (" FAIL: Range [{}, {}) exceeds data size {}" , r.offset , r.end (), fullDataSize);
2308+ pass = false ;
2309+ break ;
2310+ }
2311+ }
2312+
2313+ // Verify assembled buffer decompresses: build header + concatenated ranges
2314+ uint64_t sodOffsetInFile = sotOffset + headerInfo.sodOffset + sotLen;
2315+ uint64_t dataStartInFile = sodOffsetInFile + 2 ; // after SOD marker
2316+
2317+ // Assemble: SOT + tile-part header + SOD + concatenated needed data
2318+ uint64_t assembledHeaderSize = headerInfo.sodOffset + sotLen + 2 ; // SOT(12) + headers + SOD(2)
2319+ uint64_t assembledSize = assembledHeaderSize + selectiveSize;
2320+ std::vector<uint8_t > assembled (assembledSize);
2321+
2322+ // Copy header
2323+ std::memcpy (assembled.data (), rawData.data () + sotOffset, assembledHeaderSize);
2324+
2325+ // Copy concatenated data ranges
2326+ uint64_t pos = assembledHeaderSize;
2327+ for (auto & r : ranges)
2328+ {
2329+ std::memcpy (assembled.data () + pos, rawData.data () + dataStartInFile + r.offset , r.length );
2330+ pos += r.length ;
2331+ }
2332+
2333+ // Write assembled to temp file and decompress with reduce=2
2334+ std::string assembledFile =
2335+ (std::filesystem::temp_directory_path () / " grk_selective_assembled.j2k" ).string ();
2336+
2337+ // We need to also include the main header (everything before SOT)
2338+ {
2339+ std::vector<uint8_t > fullAssembled (sotOffset + assembledSize);
2340+ std::memcpy (fullAssembled.data (), rawData.data (), sotOffset); // main header
2341+ std::memcpy (fullAssembled.data () + sotOffset, assembled.data (), assembledSize);
2342+
2343+ // Fix SOT tile-part length to match assembled size
2344+ uint32_t newTpLen = (uint32_t )assembledSize;
2345+ fullAssembled[sotOffset + 6 ] = (uint8_t )(newTpLen >> 24 );
2346+ fullAssembled[sotOffset + 7 ] = (uint8_t )(newTpLen >> 16 );
2347+ fullAssembled[sotOffset + 8 ] = (uint8_t )(newTpLen >> 8 );
2348+ fullAssembled[sotOffset + 9 ] = (uint8_t )(newTpLen);
2349+
2350+ std::ofstream out (assembledFile, std::ios::binary);
2351+ out.write (reinterpret_cast <char *>(fullAssembled.data ()), fullAssembled.size ());
2352+ out.close ();
2353+ }
2354+
2355+ // Decompress assembled file with reduce=2
2356+ {
2357+ grk_decompress_parameters params{};
2358+ params.core .tile_cache_strategy = GRK_TILE_CACHE_IMAGE ;
2359+ params.core .reduce = 2 ;
2360+ grk_stream_params sp{};
2361+ safe_strcpy (sp.file , assembledFile.data ());
2362+ CodecPtr codec (grk_decompress_init (&sp, ¶ms));
2363+ grk_header_info hi{};
2364+ if (!grk_decompress_read_header (codec.get (), &hi))
2365+ {
2366+ spdlog::error (" FAIL: Could not read assembled file header" );
2367+ pass = false ;
2368+ }
2369+ else
2370+ {
2371+ grk_decompress_update (¶ms, codec.get ());
2372+ if (!grk_decompress_tile (codec.get (), 0 ))
2373+ {
2374+ spdlog::error (" FAIL: Could not decompress assembled file" );
2375+ pass = false ;
2376+ }
2377+ else
2378+ {
2379+ spdlog::info (" PASS: Assembled selective data decompresses successfully" );
2380+ }
2381+ }
2382+ }
2383+
2384+ // Also decompress original with reduce=2 for reference pixel comparison
2385+ std::vector<int32_t > refPixels, selPixels;
2386+ {
2387+ grk_decompress_parameters params{};
2388+ params.core .tile_cache_strategy = GRK_TILE_CACHE_IMAGE ;
2389+ params.core .reduce = 2 ;
2390+ grk_stream_params sp{};
2391+ safe_strcpy (sp.file , testFile.data ());
2392+ CodecPtr codec (grk_decompress_init (&sp, ¶ms));
2393+ grk_header_info hi{};
2394+ grk_decompress_read_header (codec.get (), &hi);
2395+ grk_decompress_update (¶ms, codec.get ());
2396+ auto * img = grk_decompress_get_sample_image (codec.get (), 0 );
2397+ grk_decompress_tile (codec.get (), 0 );
2398+ if (img && img->comps [0 ].data )
2399+ {
2400+ auto * d = (int32_t *)img->comps [0 ].data ;
2401+ refPixels.assign (d, d + img->comps [0 ].w * img->comps [0 ].h );
2402+ }
2403+ }
2404+ {
2405+ grk_decompress_parameters params{};
2406+ params.core .tile_cache_strategy = GRK_TILE_CACHE_IMAGE ;
2407+ params.core .reduce = 2 ;
2408+ grk_stream_params sp{};
2409+ safe_strcpy (sp.file , assembledFile.data ());
2410+ CodecPtr codec (grk_decompress_init (&sp, ¶ms));
2411+ grk_header_info hi{};
2412+ grk_decompress_read_header (codec.get (), &hi);
2413+ grk_decompress_update (¶ms, codec.get ());
2414+ auto * img = grk_decompress_get_sample_image (codec.get (), 0 );
2415+ grk_decompress_tile (codec.get (), 0 );
2416+ if (img && img->comps [0 ].data )
2417+ {
2418+ auto * d = (int32_t *)img->comps [0 ].data ;
2419+ selPixels.assign (d, d + img->comps [0 ].w * img->comps [0 ].h );
2420+ }
2421+ }
2422+
2423+ if (!refPixels.empty () && !selPixels.empty () && refPixels.size () == selPixels.size ())
2424+ {
2425+ if (refPixels == selPixels)
2426+ spdlog::info (" PASS: Selective fetch pixels match reference" );
2427+ else
2428+ {
2429+ spdlog::error (" FAIL: Selective fetch pixels differ from reference" );
2430+ pass = false ;
2431+ }
2432+ }
2433+ else if (refPixels.empty () || selPixels.empty ())
2434+ {
2435+ spdlog::warn (" Could not compare pixels (ref: {}, sel: {})" , refPixels.size (),
2436+ selPixels.size ());
2437+ }
2438+
2439+ std::error_code ec;
2440+ std::filesystem::remove (testFile, ec);
2441+ std::filesystem::remove (assembledFile, ec);
2442+
2443+ if (pass)
2444+ spdlog::info (" PASS: Selective fetch simulation (LRCP disjoint)" );
2445+ else
2446+ spdlog::error (" FAIL: Selective fetch simulation (LRCP disjoint)" );
2447+
2448+ return pass;
2449+ }
2450+
21372451// /////////////////////////////////////////////////////////////////
21382452// Main
21392453// /////////////////////////////////////////////////////////////////
@@ -2292,6 +2606,10 @@ int GrkLRUCacheTest::main(int argc, char** argv)
22922606 if (!testPLTHeaderExtraction ())
22932607 failures++;
22942608
2609+ // Test 19: Selective fetch simulation (LRCP disjoint ranges)
2610+ if (!testSelectiveFetchSimulation ())
2611+ failures++;
2612+
22952613 if (failures > 0 )
22962614 {
22972615 spdlog::error (" {} test(s) FAILED" , failures);
0 commit comments