feat(render): brick 分页器(LRU 工作集,内存恒定核外渲染)

实现 geopro::render::BrickPager:驻留 ≤ budget 个解压块,
按 LRU 淘汰,与体总大小无关。requestVisible 按请求顺序更新
LRU 并淘汰至预算;get 命中返回数据指针、不改 LRU。
键为完整 BrickId(含 level);std::list 记录 recency +
unordered_map 存数据与迭代器,touch/淘汰均 O(1)。
TDD:test_brick_pager 验证恒定驻留与最早块淘汰。
This commit is contained in:
gaozheng 2026-06-23 12:08:29 +08:00
parent 86e2b6b8a8
commit 5dbbb2576c
5 changed files with 168 additions and 1 deletions

View File

@ -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)

View File

@ -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<std::size_t>(id.level);
h = h * 31 + static_cast<std::size_t>(id.bx);
h = h * 31 + static_cast<std::size_t>(id.by);
h = h * 31 + static_cast<std::size_t>(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<BrickId>& 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<std::int16_t>* BrickPager::get(const BrickId& id) const {
auto it = cache_.find(id);
if (it == cache_.end()) {
return nullptr;
}
return &it->second.data;
}
} // namespace geopro::render

View File

@ -0,0 +1,66 @@
#ifndef GEOPRO_RENDER_SOURCE_BRICKPAGER_HPP
#define GEOPRO_RENDER_SOURCE_BRICKPAGER_HPP
#include <cstddef>
#include <cstdint>
#include <list>
#include <unordered_map>
#include <vector>
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<BrickId>& visible);
// 命中返回该块解压数据指针(数据由 pager 持有,下次淘汰前有效);未驻留返回
// nullptr。get 不改动 LRU保持 const 语义),驻留集仅由 requestVisible 决定。
const std::vector<std::int16_t>* 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<std::int16_t> data;
std::list<BrickId>::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<BrickId> lru_; // front = 最近使用(MRU)back = 最久未用(LRU)
std::unordered_map<BrickId, Entry, BrickIdHash> cache_;
};
} // namespace geopro::render
#endif // GEOPRO_RENDER_SOURCE_BRICKPAGER_HPP

View File

@ -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})

View File

@ -0,0 +1,36 @@
#include "render/source/BrickPager.hpp"
#include "data/store/ChunkedVolumeStore.hpp"
#include "core/algo/GprVolumeBuilder.hpp"
#include <gtest/gtest.h>
#include <filesystem>
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<render::BrickId> 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);
}