geopro/tests/render/test_view_adaptive_source.cpp

552 lines
22 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 <vtkImageData.h>
#include <vtkPointData.h>
#include <vtkShortArray.h>
#include <chrono>
#include <cmath>
#include <filesystem>
#include <thread>
#include <gtest/gtest.h>
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<short>((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<vtkImageData> 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<double>(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^levelexagg=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<std::int16_t> 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<std::size_t>(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<vtkIdType>(lk) * d[1] + lj) * d[0] + li;
const std::int16_t got = arr->GetValue(id);
const std::int16_t want =
storeVoxelAt(static_cast<int>(gi), static_cast<int>(gj),
static_cast<int>(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<double>(1 << level);
double sp[3], org[3];
img->GetSpacing(sp);
img->GetOrigin(org);
// spacingx 不夸张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.originy/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<double, std::milli>(t1 - t0).count();
EXPECT_LT(ms, 50.0) << "updateView 阻塞了?耗时 " << ms << "ms";
// 仍能最终就绪(后台重组完成)。
auto img = pollReady(src, 5000, 2);
ASSERT_NE(img.Get(), nullptr);
}
// ── C3-6 常驻粗底图baseImage() 非空 / 整卷最粗各轴 ≤16384 / VTK_SHORT / 盖全 ──
// 底图 = 整卷「各轴 ≤16384」最粗层单纹理。构造时即建好不需 updateView/不需异步)。
TEST(ViewAdaptiveVolumeSource, BaseImageWholeVolumeCoarsest) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_base").string();
makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 3);
render::ViewAdaptiveVolumeSource src(dir, /*exagg*/ 1.0);
// 构造后即非空(不调 updateView→ 永不空白的底图常驻。
vtkImageData* base = src.baseImage();
ASSERT_NE(base, nullptr);
EXPECT_EQ(base->GetScalarType(), VTK_SHORT);
int d[3];
base->GetDimensions(d);
EXPECT_GT(d[0], 0);
EXPECT_GT(d[1], 0);
EXPECT_GT(d[2], 0);
EXPECT_LE(d[0], 16384);
EXPECT_LE(d[1], 16384);
EXPECT_LE(d[2], 16384);
// 底图层 = 该 store「各轴 ≤16384」的最细满足层本 store 各层都 ≤16384 → level 0。
const int bl = src.baseLevel();
int sdx = 0, sdy = 0, sdz = 0;
data::ChunkedVolumeStore store(dir);
store.dims(bl, sdx, sdy, sdz);
EXPECT_LE(sdx, 16384);
EXPECT_LE(sdy, 16384);
EXPECT_LE(sdz, 16384);
// 盖全:底图各轴 == 该 level 全卷 store.dims全卷区间未被 maxTextureDim 裁切)。
EXPECT_EQ(d[0], sdx) << "底图未盖全 x";
EXPECT_EQ(d[1], sdy) << "底图未盖全 y";
EXPECT_EQ(d[2], sdz) << "底图未盖全 z";
// 世界范围盖全整卷origin == meta.origin全卷从 0 起);
// 跨度 ≈ 整卷世界跨度spacing×dims == meta.spacing×meta.dims同一物理范围
const data::StoreMeta& m = src.meta();
double org[3], sp[3];
base->GetOrigin(org);
base->GetSpacing(sp);
EXPECT_DOUBLE_EQ(org[0], m.origin[0]);
EXPECT_DOUBLE_EQ(org[1], m.origin[1]);
EXPECT_DOUBLE_EQ(org[2], m.origin[2]);
// 底图世界跨度应覆盖整卷 level0 世界跨度(粗层 spacing×dims ≥ 细层覆盖范围)。
EXPECT_GE(sp[0] * d[0], m.spacing[0] * m.nx - sp[0]);
EXPECT_GE(sp[1] * d[1], m.spacing[1] * m.ny - sp[1]);
EXPECT_GE(sp[2] * d[2], m.spacing[2] * m.nz - sp[2]);
}
// ── C3-6 底图永不空updateView 出界(empty)、高清从未就绪时baseImage 仍非空盖全 ──
// 这是「拖动/缩放绝不空白」的核:高清异步路径(current_)与底图(baseImage_)完全分离,
// 高清空不影响底图。
TEST(ViewAdaptiveVolumeSource, BaseImagePersistsWhenHiResEmpty) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_basepersist").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);
// 视锥外:体在相机身后 → 高清不提交、currentImages 恒空。
CameraView c = lookFromX(src.meta(), 1000.0);
c.focal[0] = c.pos[0] + 1000.0;
src.updateView(c, vol);
// 高清空(验空)但底图始终非空、盖全。
for (int i = 0; i < 10; ++i) {
EXPECT_TRUE(src.currentImages().empty());
ASSERT_NE(src.baseImage(), nullptr); // 底图永不空
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
int d[3];
src.baseImage()->GetDimensions(d);
EXPECT_GT(d[0], 0);
EXPECT_GT(d[1], 0);
EXPECT_GT(d[2], 0);
}
// ── C3-6 超长 X 整卷:最粗 ≤16384 层选取正确X 远超 16384 须升到能盖全的层)──────
// 模拟全路段长条体X 极长):底图 level 必须升到 store.dims(level).x ≤16384 的最细层,
// 且底图各轴恒 ≤16384盖全整卷、绝不撞纹理墙
TEST(ViewAdaptiveVolumeSource, BaseImageLongVolumePicksCoarseEnough) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_baselong").string();
// X=40000>16384多级金字塔 → L0:40000, L1:20000(>16384), L2:10000(≤16384)。
// 底图须选 L2最细的「各轴 ≤16384」满足层
makePyramidStore(dir, 40000, 64, 48, 0, 0, 0, 0.05, 0.05, 0.05, 64, 4);
render::ViewAdaptiveVolumeSource src(dir, 1.0);
vtkImageData* base = src.baseImage();
ASSERT_NE(base, nullptr);
int d[3];
base->GetDimensions(d);
EXPECT_LE(d[0], 16384) << "底图 x 超纹理上限";
EXPECT_LE(d[1], 16384);
EXPECT_LE(d[2], 16384);
EXPECT_GT(d[0], 0);
// 选定层各轴 store.dims 必 ≤16384且更细一层(level-1)的 x 必 >16384确认是最细满足层
const int bl = src.baseLevel();
data::ChunkedVolumeStore store(dir);
int sdx = 0, sdy = 0, sdz = 0;
store.dims(bl, sdx, sdy, sdz);
EXPECT_LE(sdx, 16384);
EXPECT_EQ(d[0], sdx) << "盖全整卷(全卷区间 ≤16384未被裁";
if (bl > 0) {
int fdx = 0, fdy = 0, fdz = 0;
store.dims(bl - 1, fdx, fdy, fdz);
EXPECT_GT(fdx, 16384) << "更细一层 x 应 >16384故选了这层";
}
}
// ── 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×613 级金字塔。
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);
// 全卷区间(各轴 ≤16384store.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";
}