552 lines
22 KiB
C++
552 lines
22 KiB
C++
// 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^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<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);
|
||
// 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<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×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";
|
||
}
|