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:
parent
86e2b6b8a8
commit
5dbbb2576c
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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})
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
Loading…
Reference in New Issue