Skip to content

Commit 353a8c5

Browse files
author
Grok Compression
committed
Add Test 19: selective fetch simulation for LRCP disjoint ranges
Tests the full pipeline: create LRCP file → extract PLT → compute disjoint selective fetch ranges → assemble header + concatenated needed data → verify decompression succeeds. Validates 77.5% bandwidth savings for LRCP with 2 layers, 4 res, reduce=2. Confirms 2 disjoint ranges and successful decompress of the assembled sparse data.
1 parent 0b65954 commit 353a8c5

1 file changed

Lines changed: 318 additions & 0 deletions

File tree

tests/GrkLRUCacheTest.cpp

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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, &params));
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(&params, 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, &params));
2393+
grk_header_info hi{};
2394+
grk_decompress_read_header(codec.get(), &hi);
2395+
grk_decompress_update(&params, 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, &params));
2411+
grk_header_info hi{};
2412+
grk_decompress_read_header(codec.get(), &hi);
2413+
grk_decompress_update(&params, 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

Comments
 (0)