From 86e2b6b8a8360e483fadf525628775095d5fa882 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Tue, 23 Jun 2026 12:02:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(store):=20brickRange=20=E7=94=A8=20hasRange?= =?UTF-8?q?=20=E6=A0=87=E5=BF=97=E6=9B=BF=E4=BB=A3=20(0,0)=20=E5=93=A8?= =?UTF-8?q?=E5=85=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (0,0) 是合法值域(真实全零块,kBlank=INT16_MIN 非 0),旧实现用 (vmin==0&&vmax==0) 当未计算哨兵会误判,导致全零块每次 brickRange 都无谓解压重算,且 buildPyramid 后仍走惰性。 - BrickEntry 加 bool hasRange 显式标志 - brickRange: hasRange 真→直接返回;假→惰性算并就地缓存(mutable levels_) - meta.json 序列化/反序列化带 hasRange(老 store 缺字段→false,惰性兼容) - buildPyramid 回填值域时一并置 hasRange=true - 补测试:真实全零块 brickRange 返回 (0,0) 不退化(金字塔/老 store 两路) --- src/data/store/ChunkedVolumeStore.cpp | 35 +++++++++++++++----- src/data/store/ChunkedVolumeStore.hpp | 6 +++- tests/data/store/test_pyramid.cpp | 46 +++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 9 deletions(-) diff --git a/src/data/store/ChunkedVolumeStore.cpp b/src/data/store/ChunkedVolumeStore.cpp index eed4aa5..a47edb8 100644 --- a/src/data/store/ChunkedVolumeStore.cpp +++ b/src/data/store/ChunkedVolumeStore.cpp @@ -194,7 +194,9 @@ ChunkedVolumeStore::ChunkedVolumeStore(const std::string& dir) : dir_(dir) { be.bw = e.at("bw").get(); be.bh = e.at("bh").get(); be.bd = e.at("bd").get(); - // 老 store 无 min/max;缺失时置 0(brickRange(0,...) 会惰性算)。 + // hasRange 显式标志:老 store 无此字段 → false(brickRange 走惰性算)。 + // 若有 min/max 字段则视为已算(兼容 buildPyramid 之前写的、只带 min/max 的 meta)。 + be.hasRange = e.value("hasRange", e.contains("min") && e.contains("max")); be.vmin = e.value("min", static_cast(0)); be.vmax = e.value("max", static_cast(0)); bricks_.push_back(be); @@ -236,6 +238,8 @@ ChunkedVolumeStore::ChunkedVolumeStore(const std::string& dir) : dir_(dir) { be.bw = e.at("bw").get(); be.bh = e.at("bh").get(); be.bd = e.at("bd").get(); + // 降采样级由 buildPyramid 写入,恒带 min/max → hasRange 默认 true。 + be.hasRange = e.value("hasRange", true); be.vmin = e.value("min", kBlank); be.vmax = e.value("max", kBlank); lv.bricks.push_back(be); @@ -249,10 +253,13 @@ ChunkedVolumeStore::ChunkedVolumeStore(const std::string& dir) : dir_(dir) { for (std::size_t i = 0; i < jb0.size(); ++i) { const std::int16_t mn = jb0[i].value("min", kBlank); const std::int16_t mx = jb0[i].value("max", kBlank); + const bool hr = jb0[i].value("hasRange", jb0[i].contains("min")); levels_[0].bricks[i].vmin = mn; levels_[0].bricks[i].vmax = mx; + levels_[0].bricks[i].hasRange = hr; bricks_[i].vmin = mn; bricks_[i].vmax = mx; + bricks_[i].hasRange = hr; } } } @@ -322,17 +329,26 @@ std::vector ChunkedVolumeStore::readBrick(int level, int bx, std::pair ChunkedVolumeStore::brickRange( int level, int bx, int by, int bz) const { - const Level& lv = levelAt(level); + if (level < 0 || level >= static_cast(levels_.size())) { + throw std::out_of_range("ChunkedVolumeStore: level out of range"); + } + Level& lv = levels_[static_cast(level)]; // mutable:可缓存惰性结果 if (bx < 0 || by < 0 || bz < 0 || bx >= lv.bx || by >= lv.by || bz >= lv.bz) { throw std::out_of_range("ChunkedVolumeStore::brickRange: brick index out of range"); } - const BrickEntry& be = + BrickEntry& be = lv.bricks.at(static_cast(brickIndexAt(lv, bx, by, bz))); - // level 0 老 store 无存储 min/max(vmin==vmax==0 哨兵):惰性读块计算。 - if (level == 0 && be.vmin == 0 && be.vmax == 0) { - return computeRange(readBrickFrom(lv, bx, by, bz)); + // 已算(buildPyramid 时算过、或之前惰性算并缓存)→ 直接返回。 + // 注意:用 hasRange 显式标志而非 (0,0) 哨兵——(0,0) 是合法值域,且 kBlank!=0。 + if (be.hasRange) { + return {be.vmin, be.vmax}; } - return {be.vmin, be.vmax}; + // 未算(老 store level 0):惰性读块计算,并就地缓存 + 置 hasRange。 + const auto rng = computeRange(readBrickFrom(lv, bx, by, bz)); + be.vmin = rng.first; + be.vmax = rng.second; + be.hasRange = true; + return rng; } void ChunkedVolumeStore::buildPyramid(int levels) { @@ -467,6 +483,7 @@ void ChunkedVolumeStore::buildPyramid(int levels) { be.bd = bd; be.vmin = rng.first; be.vmax = rng.second; + be.hasRange = true; // buildPyramid 现算现存,值域确定。 lv.bricks.push_back(be); offset += clen; } @@ -496,6 +513,7 @@ void ChunkedVolumeStore::buildPyramid(int levels) { for (std::size_t i = 0; i < jb.size(); ++i) { jb[i]["min"] = levels_[0].bricks[i].vmin; jb[i]["max"] = levels_[0].bricks[i].vmax; + jb[i]["hasRange"] = levels_[0].bricks[i].hasRange; } } } @@ -510,7 +528,8 @@ void ChunkedVolumeStore::buildPyramid(int levels) { {"bh", be.bh}, {"bd", be.bd}, {"min", be.vmin}, - {"max", be.vmax}}); + {"max", be.vmax}, + {"hasRange", be.hasRange}}); } jlevels.push_back(json{{"nx", lv.nx}, {"ny", lv.ny}, diff --git a/src/data/store/ChunkedVolumeStore.hpp b/src/data/store/ChunkedVolumeStore.hpp index efeb997..78c0beb 100644 --- a/src/data/store/ChunkedVolumeStore.hpp +++ b/src/data/store/ChunkedVolumeStore.hpp @@ -87,6 +87,9 @@ class ChunkedVolumeStore { std::int64_t compressedLen = 0; int bw = 0, bh = 0, bd = 0; std::int16_t vmin = 0, vmax = 0; // 块内 (min,max),跳过 kBlank;全 blank=(kBlank,kBlank) + // 显式「值域已算」标志:替代 (0,0) 哨兵。(0,0) 是合法值域,不能当未计算用。 + // false → brickRange 惰性读块计算(并缓存);true → 直接返回 (vmin,vmax)。 + bool hasRange = false; }; // 一个分辨率级:维度 + 块数 + 逐块索引 + 数据文件名。 @@ -110,7 +113,8 @@ class ChunkedVolumeStore { std::vector bricks_; // = level 0 索引(兼容字段,readBrick(bx,by,bz) 用) int levelCount_ = 1; - std::vector levels_; // levels_[0] 即 level 0(与 bricks_ 同源) + // mutable:brickRange 为 const,但惰性算出值域后需就地缓存(置 hasRange=true)。 + mutable std::vector levels_; // levels_[0] 即 level 0(与 bricks_ 同源) }; } // namespace geopro::data diff --git a/tests/data/store/test_pyramid.cpp b/tests/data/store/test_pyramid.cpp index 981de46..cc44613 100644 --- a/tests/data/store/test_pyramid.cpp +++ b/tests/data/store/test_pyramid.cpp @@ -98,6 +98,52 @@ TEST(Pyramid, AllBlankBrickRange) { std::filesystem::remove_all(dir); } +// 真实全零块(非 blank):brickRange 返回 (0,0) 且不退化为惰性。 +// (0,0) 是合法值域,旧实现用 (vmin==0&&vmax==0) 当「未计算」哨兵会误判; +// hasRange 标志修正后:buildPyramid 算出的全零块 hasRange=true,返回 (0,0)。 +TEST(Pyramid, RealAllZeroBrickRangeIsZeroZeroNotDegenerate) { + auto dir = + (std::filesystem::temp_directory_path() / "gpr_pyr_zero").string(); + std::filesystem::remove_all(dir); + geopro::core::BuiltI16 b; + b.vol = geopro::core::ScalarVolumeI16(64, 64, 64); + for (auto& v : b.vol.data()) v = 0; // 真实 0(非 kBlank) + b.quant = {1.0, 0.0}; + b.origin = {{0, 0, 0}}; + b.spacing = {{1, 1, 1}}; + b.vminPhys = 0; + b.vmaxPhys = 0; + ChunkedVolumeStore::write(dir, b, 32); + ChunkedVolumeStore s(dir); + s.buildPyramid(1); + auto r0 = s.brickRange(0, 0, 0, 0); + EXPECT_EQ(r0.first, 0); + EXPECT_EQ(r0.second, 0); // 合法 (0,0),不退化、不是 kBlank + EXPECT_NE(r0.first, geopro::core::ScalarVolumeI16::kBlank); + std::filesystem::remove_all(dir); +} + +// 老 store 全零块(无金字塔):首次 brickRange 惰性算出 (0,0),不无限退化。 +TEST(Pyramid, LegacyRealAllZeroBrickRangeIsZeroZero) { + auto dir = + (std::filesystem::temp_directory_path() / "gpr_pyr_zero_legacy").string(); + std::filesystem::remove_all(dir); + geopro::core::BuiltI16 b; + b.vol = geopro::core::ScalarVolumeI16(64, 64, 64); + for (auto& v : b.vol.data()) v = 0; + b.quant = {1.0, 0.0}; + b.origin = {{0, 0, 0}}; + b.spacing = {{1, 1, 1}}; + b.vminPhys = 0; + b.vmaxPhys = 0; + ChunkedVolumeStore::write(dir, b, 32); + ChunkedVolumeStore s(dir); // 未 buildPyramid:老 store,块无 hasRange + auto r0 = s.brickRange(0, 0, 0, 0); // 惰性算 → (0,0) + EXPECT_EQ(r0.first, 0); + EXPECT_EQ(r0.second, 0); + std::filesystem::remove_all(dir); +} + // 老 store(未 buildPyramid):levels()==1;brickRange(0,...) 仍可惰性算。 TEST(Pyramid, LegacyStoreNoPyramidLevelsIsOne) { auto dir = (std::filesystem::temp_directory_path() / "gpr_pyr_legacy").string();