// ViewAdaptiveVolumeSource(C2→C3-2) headless 测试:用 C1 selectLod 选层选区,把 // 当前视野区域【异步】重组为【单张 VTK_SHORT vtkImageData】(各轴 ≤16384,世界 // origin/spacing 按 level+exagg)。核心 updateView(CameraView,VolumeView) 不需真 // vtkCamera/GL 上下文——构造 vtkImageData 不需渲染管线。 // // C3-2 异步集成:updateView 只提交目标(不阻塞、不在主线程重组),currentImages // 取最新已就绪(没就绪用上一张)。测试需在 updateView 后【轮询 currentImages // 直到非空(带超时)】再断言内容。 // // 验:远观粗层 / 近观细层 / 各轴 ≤16384 / VTK_SHORT / 重组体素与 store 对应 // level+区间位置一致(不错位)/ empty 情形空 / updateView 不阻塞 / // 维度取自 store.dims(单一真源,奇数维一致)/ 最终就绪内容 == reorganizeRegion。 #include "render/source/ViewAdaptiveVolumeSource.hpp" #include "core/algo/GprVolumeBuilder.hpp" #include "data/store/ChunkedVolumeStore.hpp" #include "lod/ViewAdaptiveLodPolicy.hpp" #include "render/source/RegionReorganizer.hpp" #include #include #include #include #include #include #include #include using namespace geopro; using geopro::render::CameraView; using geopro::render::VolumeView; namespace { // 造一个含金字塔的 store:值 = 全局 (i+j+k)%1000(便于校验块定位),非 64 整除 // 维度以含边缘块。返回 store 目录。与 test_outofcore_source 同口径。 std::string makePyramidStore(const std::string& dir, int nx, int ny, int nz, double ox, double oy, double oz, double dx, double dy, double dz, int brick, int levels) { std::filesystem::remove_all(dir); core::BuiltI16 b; b.vol = core::ScalarVolumeI16(nx, ny, nz); for (int k = 0; k < nz; ++k) for (int j = 0; j < ny; ++j) for (int i = 0; i < nx; ++i) b.vol.at(i, j, k) = static_cast((i + j + k) % 1000); b.quant = {1.0, 0.0}; b.origin = {{ox, oy, oz}}; b.spacing = {{dx, dy, dz}}; b.vminPhys = 0; b.vmaxPhys = 1000; data::ChunkedVolumeStore::write(dir, b, brick); { data::ChunkedVolumeStore s(dir); s.buildPyramid(levels); } return dir; } // 由 store meta + levels + exagg 填 VolumeView(与生产 volumeViewOf 同口径)。 VolumeView volumeViewOf(const data::StoreMeta& m, int levels, double exagg) { VolumeView v{}; v.nx = m.nx; v.ny = m.ny; v.nz = m.nz; v.brick = m.brick; v.levels = levels; v.origin[0] = m.origin[0]; v.origin[1] = m.origin[1]; v.origin[2] = m.origin[2]; // C1 约定:spacing 已含 exagg 于 y/z。 v.spacing[0] = m.spacing[0]; v.spacing[1] = m.spacing[1] * exagg; v.spacing[2] = m.spacing[2] * exagg; v.exagg = exagg; return v; } // 相机:从 +X 看体中心(用 level0 物理范围,未含 exagg 于 X)。 CameraView lookFromX(const data::StoreMeta& m, double dist, double fovYDeg = 30.0, int viewportH = 1080) { CameraView c{}; const double cx = m.origin[0] + 0.5 * m.nx * m.spacing[0]; const double cy = m.origin[1] + 0.5 * m.ny * m.spacing[1]; const double cz = m.origin[2] + 0.5 * m.nz * m.spacing[2]; c.focal[0] = cx; c.focal[1] = cy; c.focal[2] = cz; c.pos[0] = cx + dist; c.pos[1] = cy; c.pos[2] = cz; c.up[0] = 0; c.up[1] = 0; c.up[2] = 1; c.fovYDeg = fovYDeg; c.aspect = 1.0; c.viewportH = viewportH; return c; } // 异步轮询:updateView 后 currentImages 可能还没就绪——轮询直到非空(带超时)。 // 返回首张就绪 image(超时仍空 → 返回 nullptr,由调用方 ASSERT)。 vtkSmartPointer pollReady(render::ViewAdaptiveVolumeSource& src, int maxTries = 2000, int sleepMs = 2) { for (int i = 0; i < maxTries; ++i) { auto imgs = src.currentImages(); if (!imgs.empty() && imgs[0] != nullptr) return imgs[0]; std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs)); } return nullptr; } } // namespace // ── 远观:选粗层、单图、各轴 ≤16384、VTK_SHORT(异步轮询就绪后断言)───────── TEST(ViewAdaptiveVolumeSource, FarViewCoarseLevelSingleTexture) { const auto dir = (std::filesystem::temp_directory_path() / "gpr_va_far").string(); // 200×80×60, brick=64 → 含边缘块。3 级金字塔。 makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 3); render::ViewAdaptiveVolumeSource src(dir, /*exagg*/ 1.0); EXPECT_EQ(src.meta().nx, 200); const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0); const CameraView far = lookFromX(src.meta(), 8000.0); src.updateView(far, vol); auto img = pollReady(src); ASSERT_NE(img.Get(), nullptr); // 就绪后 currentImages 仍是单张。 auto imgs = src.currentImages(); ASSERT_EQ(imgs.size(), 1u); EXPECT_EQ(img->GetScalarType(), VTK_SHORT); int d[3]; img->GetDimensions(d); EXPECT_LE(d[0], 16384); EXPECT_LE(d[1], 16384); EXPECT_LE(d[2], 16384); EXPECT_GT(d[0], 0); EXPECT_GT(d[1], 0); EXPECT_GT(d[2], 0); EXPECT_GT(src.lastLevel(), 0); // 远 → 粗 } // ── 近观:选最细层(0)、单图、仍 ≤16384 ───────────────────────────────────── TEST(ViewAdaptiveVolumeSource, NearViewFineLevel) { const auto dir = (std::filesystem::temp_directory_path() / "gpr_va_near").string(); makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 3); render::ViewAdaptiveVolumeSource src(dir, 1.0); const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0); const CameraView near = lookFromX(src.meta(), 8.0, 20.0, 1080); src.updateView(near, vol); auto img = pollReady(src); ASSERT_NE(img.Get(), nullptr); EXPECT_EQ(src.lastLevel(), 0); // 近 → 最细 int d[3]; img->GetDimensions(d); EXPECT_LE(d[0], 16384); EXPECT_LE(d[1], 16384); EXPECT_LE(d[2], 16384); } // ── 重组体素与 store 对应 level/区间位置一致(不错位)────────────────────── // 远观选某粗 level,重组单图后抽查若干体素:用世界坐标反推该 level 体素索引, // 与 store.readBrick(level,...) 对应块的同一体素值逐一相等 → 证明位置正确。 TEST(ViewAdaptiveVolumeSource, ReconstructedVoxelsMatchStore) { const auto dir = (std::filesystem::temp_directory_path() / "gpr_va_match").string(); makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 3); render::ViewAdaptiveVolumeSource src(dir, 1.0); const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0); const CameraView far = lookFromX(src.meta(), 8000.0); src.updateView(far, vol); auto pimg = pollReady(src); ASSERT_NE(pimg.Get(), nullptr); vtkImageData* img = pimg.Get(); const int level = src.lastLevel(); data::ChunkedVolumeStore store(dir); const data::StoreMeta& m = src.meta(); const int brick = m.brick; const double sc = static_cast(1 << level); int dimLx = 0, dimLy = 0, dimLz = 0; store.dims(level, dimLx, dimLy, dimLz); int d[3]; img->GetDimensions(d); double org[3], sp[3]; img->GetOrigin(org); img->GetSpacing(sp); // spacing 应 = meta.spacing × 2^level(exagg=1)。 EXPECT_DOUBLE_EQ(sp[0], m.spacing[0] * sc); EXPECT_DOUBLE_EQ(sp[1], m.spacing[1] * sc); EXPECT_DOUBLE_EQ(sp[2], m.spacing[2] * sc); auto* arr = vtkShortArray::SafeDownCast(img->GetPointData()->GetScalars()); ASSERT_NE(arr, nullptr); // 该图覆盖的 level 体素起点:由 origin 反推(应整数)。 const long gi0 = std::lround((org[0] - m.origin[0]) / sp[0]); const long gj0 = std::lround((org[1] - m.origin[1]) / sp[1]); const long gk0 = std::lround((org[2] - m.origin[2]) / sp[2]); EXPECT_GE(gi0, 0); EXPECT_GE(gj0, 0); EXPECT_GE(gk0, 0); // 取该 level 单块校验:读 store 块 (b*,0,0),把块内某体素与图中同位置对比。 auto storeVoxelAt = [&](int gi, int gj, int gk) -> std::int16_t { const int bx = gi / brick, by = gj / brick, bz = gk / brick; const std::vector raw = store.readBrick(level, bx, by, bz); const int i0 = bx * brick, j0 = by * brick, k0 = bz * brick; const int bw = std::min(brick, dimLx - i0); const int bh = std::min(brick, dimLy - j0); const int li = gi - i0, lj = gj - j0, lk = gk - k0; const std::size_t w = (static_cast(lk) * bh + lj) * bw + li; return raw[w]; }; // 抽查图内若干位置(含非角点、跨块),逐一与 store 对应 level 体素相等。 int checked = 0; for (int lk : {0, d[2] / 2, d[2] - 1}) { for (int lj : {0, d[1] / 2, d[1] - 1}) { for (int li : {0, d[0] / 2, d[0] - 1}) { const long gi = gi0 + li, gj = gj0 + lj, gk = gk0 + lk; if (gi >= dimLx || gj >= dimLy || gk >= dimLz) continue; const vtkIdType id = (static_cast(lk) * d[1] + lj) * d[0] + li; const std::int16_t got = arr->GetValue(id); const std::int16_t want = storeVoxelAt(static_cast(gi), static_cast(gj), static_cast(gk)); EXPECT_EQ(got, want) << "体素错位 at local(" << li << "," << lj << "," << lk << ") level=" << level; ++checked; } } } EXPECT_GT(checked, 0); } // ── 世界 origin:按 level+exagg 偏移正确(exagg≠1 校验 y/z 烘焙)───────────── TEST(ViewAdaptiveVolumeSource, WorldOriginSpacingWithExagg) { const auto dir = (std::filesystem::temp_directory_path() / "gpr_va_exagg").string(); makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 3); const double exagg = 4.0; render::ViewAdaptiveVolumeSource src(dir, exagg); const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), exagg); // 远观整体,区间从 0 起 → origin 应正好 = meta.origin(区间起点 0)。 const CameraView far = lookFromX(src.meta(), 8000.0); src.updateView(far, vol); auto img = pollReady(src); ASSERT_NE(img.Get(), nullptr); const data::StoreMeta& m = src.meta(); const int level = src.lastLevel(); const double sc = static_cast(1 << level); double sp[3], org[3]; img->GetSpacing(sp); img->GetOrigin(org); // spacing:x 不夸张,y/z ×exagg。 EXPECT_DOUBLE_EQ(sp[0], m.spacing[0] * sc); EXPECT_DOUBLE_EQ(sp[1], m.spacing[1] * sc * exagg); EXPECT_DOUBLE_EQ(sp[2], m.spacing[2] * sc * exagg); // 区间从 0 起 → origin = meta.origin(y/z 偏移为 0×… 仍是 origin)。 EXPECT_DOUBLE_EQ(org[0], m.origin[0]); EXPECT_DOUBLE_EQ(org[1], m.origin[1]); EXPECT_DOUBLE_EQ(org[2], m.origin[2]); } // ── empty:体完全在视锥外 → currentImages 空(从未就绪)────────────────────── TEST(ViewAdaptiveVolumeSource, EmptyWhenBehindCamera) { const auto dir = (std::filesystem::temp_directory_path() / "gpr_va_empty").string(); makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 3); render::ViewAdaptiveVolumeSource src(dir, 1.0); const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0); CameraView c = lookFromX(src.meta(), 1000.0); c.focal[0] = c.pos[0] + 1000.0; // 视线朝 +X,体在身后 → 视锥外 src.updateView(c, vol); // empty 选区 → 不提交目标;从未就绪 → currentImages 恒空(短轮询确认无意外结果)。 for (int i = 0; i < 20; ++i) { EXPECT_TRUE(src.currentImages().empty()); std::this_thread::sleep_for(std::chrono::milliseconds(2)); } EXPECT_EQ(src.sliceSource(), nullptr); } // ── 单层 store(无金字塔)也能重组(level 恒 0)─────────────────────────── TEST(ViewAdaptiveVolumeSource, SingleLevelStore) { const auto dir = (std::filesystem::temp_directory_path() / "gpr_va_1lvl").string(); // levels=0 → 仅 level 0。 makePyramidStore(dir, 128, 64, 48, 0, 0, 0, 1.0, 1.0, 1.0, 64, 0); render::ViewAdaptiveVolumeSource src(dir, 1.0); EXPECT_EQ(src.levelCount(), 1); const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0); const CameraView c = lookFromX(src.meta(), 500.0); src.updateView(c, vol); auto img = pollReady(src); ASSERT_NE(img.Get(), nullptr); EXPECT_EQ(src.lastLevel(), 0); EXPECT_EQ(img->GetScalarType(), VTK_SHORT); } // ── C3-2 异步:updateView 立即返回 + 最终就绪内容 == reorganizeRegion 同步结果 ── TEST(ViewAdaptiveVolumeSource, AsyncUpdateEventuallyReady) { const auto dir = (std::filesystem::temp_directory_path() / "gpr_va_async").string(); makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 3); render::ViewAdaptiveVolumeSource src(dir, 1.0); const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0); const CameraView far = lookFromX(src.meta(), 8000.0); src.updateView(far, vol); // 立即返回(不阻塞、不在主线程重组) auto img = pollReady(src); ASSERT_NE(img.Get(), nullptr); int d[3]; img->GetDimensions(d); EXPECT_LE(d[0], 16384); EXPECT_LE(d[1], 16384); EXPECT_LE(d[2], 16384); EXPECT_EQ(img->GetScalarType(), VTK_SHORT); // 最终就绪内容 == 同步 reorganizeRegion(同 target) 结果(位置/几何一致)。 // 用 selectLod 复算同一 target,与异步源选的同 level/区间。 const render::LodSelection sel = render::selectLod(vol, far, 16384); ASSERT_FALSE(sel.empty); EXPECT_EQ(sel.level, src.lastLevel()); render::RegionTarget t{}; t.level = sel.level; t.bx0 = sel.bx0; t.bx1 = sel.bx1; t.by0 = sel.by0; t.by1 = sel.by1; t.bz0 = sel.bz0; t.bz1 = sel.bz1; t.exagg = 1.0; data::ChunkedVolumeStore store(dir); auto sync = render::reorganizeRegion(store, t, 16384); ASSERT_NE(sync.Get(), nullptr); int ds[3]; sync->GetDimensions(ds); EXPECT_EQ(d[0], ds[0]); EXPECT_EQ(d[1], ds[1]); EXPECT_EQ(d[2], ds[2]); double oa[3], os[3], sa[3], ss[3]; img->GetOrigin(oa); img->GetSpacing(sa); sync->GetOrigin(os); sync->GetSpacing(ss); for (int i = 0; i < 3; ++i) { EXPECT_DOUBLE_EQ(oa[i], os[i]); EXPECT_DOUBLE_EQ(sa[i], ss[i]); } auto* aAsync = vtkShortArray::SafeDownCast(img->GetPointData()->GetScalars()); auto* aSync = vtkShortArray::SafeDownCast(sync->GetPointData()->GetScalars()); ASSERT_NE(aAsync, nullptr); ASSERT_NE(aSync, nullptr); ASSERT_EQ(aAsync->GetNumberOfTuples(), aSync->GetNumberOfTuples()); // 抽查若干体素逐一相等(含端点/中点)。 const vtkIdType n = aAsync->GetNumberOfTuples(); for (vtkIdType id : {vtkIdType(0), n / 3, n / 2, n - 1}) { if (id < 0 || id >= n) continue; EXPECT_EQ(aAsync->GetValue(id), aSync->GetValue(id)) << "id=" << id; } } // ── C3-2 非阻塞:updateView 本身耗时极短(不含重组)即便较大 store ───────────── TEST(ViewAdaptiveVolumeSource, UpdateDoesNotBlock) { const auto dir = (std::filesystem::temp_directory_path() / "gpr_va_noblock").string(); // 较大 store:同步重组该粗层整卷会有明显耗时;updateView 只提交目标应近 0。 makePyramidStore(dir, 512, 256, 192, 0, 0, 0, 0.5, 0.5, 0.2, 64, 3); render::ViewAdaptiveVolumeSource src(dir, 1.0); const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0); const CameraView far = lookFromX(src.meta(), 20000.0); const auto t0 = std::chrono::steady_clock::now(); src.updateView(far, vol); // 应立即返回(仅 selectLod + requestTarget) const auto t1 = std::chrono::steady_clock::now(); const double ms = std::chrono::duration(t1 - t0).count(); EXPECT_LT(ms, 50.0) << "updateView 阻塞了?耗时 " << ms << "ms"; // 仍能最终就绪(后台重组完成)。 auto img = pollReady(src, 5000, 2); ASSERT_NE(img.Get(), nullptr); } // ── C3-2 维度取自 store.dims(单一真源):奇数维 store 验重组 dims == store.dims ── // 全卷请求某粗 level,重组单图各轴 == store.dims(level)(而非自算公式)。奇数维 // 多级降采样下,store.dims 是唯一权威;本测试钉死「重组维度跟随 store.dims」。 TEST(ViewAdaptiveVolumeSource, UsesStoreDimsNotSelfComputed) { const auto dir = (std::filesystem::temp_directory_path() / "gpr_va_storedims").string(); // 奇数维度(含多次 ceil 降采样):199×83×61,3 级金字塔。 makePyramidStore(dir, 199, 83, 61, 0, 0, 0, 1.0, 1.0, 1.0, 64, 3); render::ViewAdaptiveVolumeSource src(dir, 1.0); const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0); // 极远 → 选最粗层、区间≈全卷,重组整卷该层 → 各轴应正好 == store.dims(level)。 const CameraView far = lookFromX(src.meta(), 1.0e7); src.updateView(far, vol); auto img = pollReady(src); ASSERT_NE(img.Get(), nullptr); const int level = src.lastLevel(); ASSERT_GT(level, 0); // 极远 → 粗层(多级降采样,奇数维有意义) int sdx = 0, sdy = 0, sdz = 0; data::ChunkedVolumeStore store(dir); store.dims(level, sdx, sdy, sdz); int d[3]; img->GetDimensions(d); // 全卷区间(各轴 ≤16384,store.dims 远小于此)→ 重组维度恒 == store.dims。 EXPECT_EQ(d[0], sdx) << "x 维度未跟随 store.dims"; EXPECT_EQ(d[1], sdy) << "y 维度未跟随 store.dims"; EXPECT_EQ(d[2], sdz) << "z 维度未跟随 store.dims"; }