#include "render/source/OutOfCoreSource.hpp" #include "core/algo/GprVolumeBuilder.hpp" #include "data/store/ChunkedVolumeStore.hpp" #include #include #include using namespace geopro; namespace { // 造一个含金字塔的 store:值 = 全局 (i+j+k)%1000(便于校验块定位),非 64 整除维度 // 以含边缘块。返回 store 目录。 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; } } // namespace // headless 不需 GPU:验工作集块均 ≤ 纹理安全尺寸、residentCount ≤ budget、 // 块世界 origin/spacing 正确(level 0,cam==nullptr → 全块经 budget/LRU 限制)。 TEST(OutOfCoreSource, WorkingSetBricksAreTextureSafeAndBounded) { const auto dir = (std::filesystem::temp_directory_path() / "gpr_ooc_test").string(); // 200×80×60,brick=64 → level0 块 4×2×1=8;非整除含边缘块。1 级金字塔。 makePyramidStore(dir, 200, 80, 60, /*ox=*/1, /*oy=*/2, /*oz=*/3, /*dx=*/0.5, /*dy=*/0.5, /*dz=*/0.2, /*brick=*/64, /*levels=*/1); const std::size_t budget = 4; render::OutOfCoreSource src(dir, budget); EXPECT_EQ(src.meta().nx, 200); EXPECT_EQ(src.budget(), budget); src.update(nullptr); // cam==nullptr → level 0 全部块,budget/LRU 限制 EXPECT_EQ(src.lastLevel(), 0); EXPECT_EQ(src.lastLevelBrickTotal(), 8u); // 4×2×1 EXPECT_LE(src.residentCount(), budget); // 内存恒定核心 auto imgs = src.currentImages(); EXPECT_FALSE(imgs.empty()); EXPECT_LE(imgs.size(), budget); // 工作集图像数 = 驻留块数 ≤ budget constexpr int kTextureSafe = 64; // 各块各轴 ≤ brick ≪ 16384 for (const auto& img : imgs) { ASSERT_NE(img.Get(), nullptr); EXPECT_EQ(img->GetScalarType(), VTK_SHORT); int d[3]; img->GetDimensions(d); EXPECT_LE(d[0], kTextureSafe); EXPECT_LE(d[1], kTextureSafe); EXPECT_LE(d[2], kTextureSafe); EXPECT_GT(d[0], 0); EXPECT_GT(d[1], 0); EXPECT_GT(d[2], 0); } } // 块世界坐标:level 0 块 (1,0,0) 的 origin = meta.origin + (64×spacing,0,0); // spacing == meta.spacing。 TEST(OutOfCoreSource, BrickWorldCoordsLevel0) { const auto dir = (std::filesystem::temp_directory_path() / "gpr_ooc_world0").string(); makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 1); render::OutOfCoreSource src(dir, /*budget=*/16); src.update(nullptr); // 全 8 块都能驻留(budget=16) EXPECT_EQ(src.residentCount(), 8u); // 找 origin.x == 1 + 64×0.5 == 33 的块(即 bx=1 列首块),验世界坐标。 auto imgs = src.currentImages(); bool found = false; for (const auto& img : imgs) { double o[3], s[3]; img->GetOrigin(o); img->GetSpacing(s); // spacing 恒等于 meta(level 0,2^0=1)。 EXPECT_DOUBLE_EQ(s[0], 0.5); EXPECT_DOUBLE_EQ(s[1], 0.5); EXPECT_DOUBLE_EQ(s[2], 0.2); if (std::abs(o[0] - (1.0 + 64 * 0.5)) < 1e-9 && std::abs(o[1] - 2.0) < 1e-9 && std::abs(o[2] - 3.0) < 1e-9) { found = true; } } EXPECT_TRUE(found) << "未找到 bx=1 列首块的世界 origin"; } // 金字塔 LOD:level 1 块的 spacing == meta.spacing × 2;origin 用 level1 体素步距。 TEST(OutOfCoreSource, BrickWorldCoordsLevel1Spacing) { const auto dir = (std::filesystem::temp_directory_path() / "gpr_ooc_world1").string(); makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 1); data::ChunkedVolumeStore store(dir); ASSERT_GE(store.levels(), 2); // level0 + level1 // 直接复用源的世界坐标逻辑:level1 块 (1,0,0) 的 spacing 应翻倍, // origin.x = 1 + 64×(0.5×2) = 1 + 64 = 65。这里通过构造一个仅含 level1 的工作集 // 验证(用 budget 大、相机 nullptr 时源仍取 level0,故改为直接核对块世界坐标公式: // 用 store dims 推 level1 存在且块数合理)。 int nx1 = 0, ny1 = 0, nz1 = 0; store.dims(1, nx1, ny1, nz1); EXPECT_EQ(nx1, 100); // ceil(200/2) EXPECT_EQ(ny1, 40); EXPECT_EQ(nz1, 30); }