diff --git a/src/render/CMakeLists.txt b/src/render/CMakeLists.txt index 5ac040c..f901582 100644 --- a/src/render/CMakeLists.txt +++ b/src/render/CMakeLists.txt @@ -4,7 +4,7 @@ add_library(geopro_render STATIC Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp VoxelFromScatters.cpp ContourBands.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp actors/MapLineActor.cpp actors/ScatterActor.cpp actors/AnomalyActor.cpp actors/ElectrodeActor.cpp actors/TerrainActor.cpp actors/AxesActor.cpp interact/SlicePlaneMath.cpp interact/SliceTool.cpp interact/PickInteractorStyle.cpp interact/InteractionManager.cpp interact/AnomalyDrawTool.cpp ground/TileMath.cpp - source/WholeVolumeSource.cpp) + source/WholeVolumeSource.cpp source/BrickPager.cpp) target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(geopro_render PUBLIC geopro_core geopro_store ${VTK_LIBRARIES} GDAL::GDAL) target_compile_features(geopro_render PUBLIC cxx_std_17) diff --git a/src/render/source/BrickPager.cpp b/src/render/source/BrickPager.cpp new file mode 100644 index 0000000..5964383 --- /dev/null +++ b/src/render/source/BrickPager.cpp @@ -0,0 +1,63 @@ +#include "render/source/BrickPager.hpp" + +#include "data/store/ChunkedVolumeStore.hpp" + +namespace geopro::render { + +std::size_t BrickPager::BrickIdHash::operator()(const BrickId& id) const + noexcept { + // 混合四个分量;level/块坐标均为小整数,简单移位哈希足够分散。 + std::size_t h = static_cast(id.level); + h = h * 31 + static_cast(id.bx); + h = h * 31 + static_cast(id.by); + h = h * 31 + static_cast(id.bz); + return h; +} + +BrickPager::BrickPager(const geopro::data::ChunkedVolumeStore& store, + std::size_t budgetBricks) + : store_(store), budget_(budgetBricks) {} + +void BrickPager::touch(const BrickId& id) { + auto it = cache_.find(id); + if (it == cache_.end()) { + return; + } + // 移到 MRU 端(splice 不失效迭代器)。 + lru_.splice(lru_.begin(), lru_, it->second.lruIt); + it->second.lruIt = lru_.begin(); +} + +void BrickPager::evictToBudget() { + while (cache_.size() > budget_ && !lru_.empty()) { + const BrickId victim = lru_.back(); + lru_.pop_back(); + cache_.erase(victim); + } +} + +void BrickPager::requestVisible(const std::vector& visible) { + for (const BrickId& id : visible) { + auto it = cache_.find(id); + if (it == cache_.end()) { + Entry e; + e.data = store_.readBrick(id.level, id.bx, id.by, id.bz); + lru_.push_front(id); + e.lruIt = lru_.begin(); + cache_.emplace(id, std::move(e)); + } else { + touch(id); + } + } + evictToBudget(); +} + +const std::vector* BrickPager::get(const BrickId& id) const { + auto it = cache_.find(id); + if (it == cache_.end()) { + return nullptr; + } + return &it->second.data; +} + +} // namespace geopro::render diff --git a/src/render/source/BrickPager.hpp b/src/render/source/BrickPager.hpp new file mode 100644 index 0000000..20865aa --- /dev/null +++ b/src/render/source/BrickPager.hpp @@ -0,0 +1,66 @@ +#ifndef GEOPRO_RENDER_SOURCE_BRICKPAGER_HPP +#define GEOPRO_RENDER_SOURCE_BRICKPAGER_HPP + +#include +#include +#include +#include +#include + +namespace geopro::data { +class ChunkedVolumeStore; +} + +namespace geopro::render { + +// 缓存键:完整 brick 标识(含 level)。 +struct BrickId { + int level = 0, bx = 0, by = 0, bz = 0; + bool operator==(const BrickId& o) const noexcept { + return level == o.level && bx == o.bx && by == o.by && bz == o.bz; + } +}; + +// 内存恒定的 brick LRU 缓存:任意时刻驻留 ≤ budgetBricks 个解压块, +// 与体总大小无关。这是 C 方案核外渲染的内存控制核心。 +class BrickPager { + public: + BrickPager(const geopro::data::ChunkedVolumeStore& store, + std::size_t budgetBricks); + + // 载入 visible 中缺失的块(经 store.readBrick 解压),按请求顺序更新 LRU; + // 之后淘汰最久未用的,直到 residentCount() <= budget。 + // 若 visible.size() > budget,保留最近请求(visible 末尾)的 budget 个。 + void requestVisible(const std::vector& visible); + + // 命中返回该块解压数据指针(数据由 pager 持有,下次淘汰前有效);未驻留返回 + // nullptr。get 不改动 LRU(保持 const 语义),驻留集仅由 requestVisible 决定。 + const std::vector* get(const BrickId& id) const; + + std::size_t residentCount() const { return cache_.size(); } + std::size_t budget() const { return budget_; } + + private: + struct Entry { + std::vector data; + std::list::iterator lruIt; // 指向 lru_ 中本块位置 + }; + + struct BrickIdHash { + std::size_t operator()(const BrickId& id) const noexcept; + }; + + // 把 id 移到 MRU 端(lru_ 头部)。 + void touch(const BrickId& id); + // 从 LRU 端淘汰直到 residentCount() <= budget_。 + void evictToBudget(); + + const geopro::data::ChunkedVolumeStore& store_; + std::size_t budget_; + std::list lru_; // front = 最近使用(MRU),back = 最久未用(LRU) + std::unordered_map cache_; +}; + +} // namespace geopro::render + +#endif // GEOPRO_RENDER_SOURCE_BRICKPAGER_HPP diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 636c483..86a0fde 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -117,6 +117,8 @@ target_sources(geopro_tests PRIVATE render/test_tile_math.cpp) target_sources(geopro_tests PRIVATE render/test_slice_plane_math.cpp) # WholeVolumeSource(B):读分块存储→重组整卷 VTK_SHORT image,校验 dims/类型/边缘块重组位置。 target_sources(geopro_tests PRIVATE render/test_whole_volume_source.cpp) +# BrickPager(C):内存恒定的 brick LRU 分页器,驻留 ≤ budget 个解压块,证明超大体浏览内存不爆。 +target_sources(geopro_tests PRIVATE render/test_brick_pager.cpp) target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES}) vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES}) diff --git a/tests/render/test_brick_pager.cpp b/tests/render/test_brick_pager.cpp new file mode 100644 index 0000000..68b74f7 --- /dev/null +++ b/tests/render/test_brick_pager.cpp @@ -0,0 +1,36 @@ +#include "render/source/BrickPager.hpp" +#include "data/store/ChunkedVolumeStore.hpp" +#include "core/algo/GprVolumeBuilder.hpp" +#include +#include +using namespace geopro; +namespace { +data::ChunkedVolumeStore makeStore(const std::string& dir){ + geopro::core::BuiltI16 b; b.vol=geopro::core::ScalarVolumeI16(128,128,128); + for(auto& v: b.vol.data()) v=3; + b.quant={1.0,0.0}; b.origin={{0,0,0}}; b.spacing={{1,1,1}}; b.vminPhys=0; b.vmaxPhys=3; + data::ChunkedVolumeStore::write(dir, b, 64); // 2×2×2=8 个 brick(level0) + return data::ChunkedVolumeStore(dir); +} +} +TEST(BrickPager, BoundedMemoryLruEviction){ + auto dir=(std::filesystem::temp_directory_path()/"gpr_pager").string(); + std::filesystem::remove_all(dir); + auto store = makeStore(dir); + render::BrickPager pager(store, 4); + EXPECT_EQ(pager.budget(), 4u); + EXPECT_EQ(pager.residentCount(), 0u); + std::vector six = { + {0,0,0,0},{0,1,0,0},{0,0,1,0},{0,1,1,0},{0,0,0,1},{0,1,0,1} }; + pager.requestVisible(six); + EXPECT_EQ(pager.residentCount(), 4u); // 恒定 ≤ budget + EXPECT_EQ(pager.get(six[0]), nullptr); // 最早请求→已淘汰 + EXPECT_EQ(pager.get(six[1]), nullptr); + ASSERT_NE(pager.get(six[5]), nullptr); // 最近请求→驻留 + EXPECT_EQ(pager.get(six[5])->size(), 64u*64*64); + // 再请求一个已驻留 + 一个新的,验证 LRU 更新与恒定驻留 + pager.requestVisible({ six[5], {0,1,1,1} }); + EXPECT_LE(pager.residentCount(), 4u); + EXPECT_NE(pager.get(six[5]), nullptr); // 刚 touch,仍在 + std::filesystem::remove_all(dir); +}