feat/vtk-3d-view #7

Merged
gaozheng merged 301 commits from feat/vtk-3d-view into main 2026-06-27 18:43:52 +08:00
5 changed files with 385 additions and 56 deletions
Showing only changes of commit ced2ca7811 - Show all commits

View File

@ -4,8 +4,10 @@
namespace geopro::render { namespace geopro::render {
AsyncRegionBuilder::AsyncRegionBuilder(const std::string& storeDir) AsyncRegionBuilder::AsyncRegionBuilder(const std::string& storeDir,
: store_(storeDir) { std::size_t cacheCapacity)
: store_(storeDir),
cacheCapacity_(cacheCapacity > 0 ? cacheCapacity : 1) {
worker_ = std::thread([this] { workerLoop(); }); worker_ = std::thread([this] { workerLoop(); });
} }
@ -23,42 +25,125 @@ void AsyncRegionBuilder::setMaxTextureDim(int dim) {
if (dim > 0) maxTextureDim_ = dim; if (dim > 0) maxTextureDim_ = dim;
} }
std::list<AsyncRegionBuilder::CacheEntry>::iterator
AsyncRegionBuilder::findCached(const RegionTarget& t) {
for (auto it = cache_.begin(); it != cache_.end(); ++it)
if (it->target == t) return it;
return cache_.end();
}
void AsyncRegionBuilder::insertCacheLocked(const RegionTarget& t,
vtkSmartPointer<vtkImageData> img,
int level) {
// 已存在 → 更新并提到 front否则新插 front超容淘汰 back最久未用
auto it = findCached(t);
if (it != cache_.end()) {
it->image = std::move(img);
it->level = level;
cache_.splice(cache_.begin(), cache_, it); // 移到 fronttouch
return;
}
cache_.push_front(CacheEntry{t, std::move(img), level});
while (cache_.size() > cacheCapacity_) {
cache_.pop_back(); // 旧 image refcount 在锁内释放(单线程独占)
}
}
void AsyncRegionBuilder::requestTarget(const RegionTarget& t) { void AsyncRegionBuilder::requestTarget(const RegionTarget& t) {
{ {
std::lock_guard<std::mutex> lk(mutex_); std::lock_guard<std::mutex> lk(mutex_);
// 与已就绪结果对应的目标相同则不必重建(避免重复劳动);与在建/排队的期望相同 mainTarget_ = t;
// 亦无需扰动。简化直接覆盖期望并标脏——worker 会以最新期望为准supersede hasMain_ = true;
desired_ = t; // 短路:已在就绪缓存→无需重建,但仍把缓存结果发布给 takeLatest兼容 C3-1/C3-2
hasDesired_ = true; // 主目标切到一个已缓存区域时下一帧 takeLatest 即换上),并 touch LRU。
// 否则标记主目标待建。
auto it = findCached(t);
if (it != cache_.end()) {
mainPending_ = false;
latestMain_ = it->image;
latestMainLevel_ = it->level;
latestMainFresh_ = true;
cache_.splice(cache_.begin(), cache_, it); // touch LRU
} else {
mainPending_ = true;
}
} }
cv_.notify_one(); cv_.notify_one();
} }
vtkSmartPointer<vtkImageData> AsyncRegionBuilder::takeLatest() { void AsyncRegionBuilder::prefetch(const std::vector<RegionTarget>& targets) {
// 非阻塞:仅在锁内做指针移动(不做任何重组/IO/长时操作)。 {
// std::move 把 ready_ 的引用移交给返回值(无 refcount 增减,且全程在锁内/单线程), std::lock_guard<std::mutex> lk(mutex_);
// ready_ 置空——下次无新结果再调返回 nullptr。 for (const auto& t : targets) {
// 跳过已缓存、与主目标相同、或已在预取队列中的(去重,省空转)。
if (findCached(t) != cache_.end()) continue;
if (hasMain_ && t == mainTarget_) continue;
bool dup = false;
for (const auto& q : prefetchQ_)
if (q == t) { dup = true; break; }
if (!dup) prefetchQ_.push_back(t);
}
}
cv_.notify_one();
}
bool AsyncRegionBuilder::pickNextLocked(RegionTarget& out, bool& isMain) {
// 主目标优先:未缓存的主目标先建。
if (hasMain_ && mainPending_) {
out = mainTarget_;
isMain = true;
return true;
}
// 否则预取建队列里最新back且未缓存的顺手丢弃已缓存的过期项。
while (!prefetchQ_.empty()) {
RegionTarget t = prefetchQ_.back();
prefetchQ_.pop_back();
if (findCached(t) != cache_.end()) continue; // 已缓存→跳过
out = t;
isMain = false;
return true;
}
return false;
}
vtkSmartPointer<vtkImageData> AsyncRegionBuilder::getReady(const RegionTarget& t,
int& levelOut) {
std::lock_guard<std::mutex> lk(mutex_); std::lock_guard<std::mutex> lk(mutex_);
return std::move(ready_); auto it = findCached(t);
if (it == cache_.end()) return nullptr;
levelOut = it->level;
vtkSmartPointer<vtkImageData> img = it->image; // 锁内拷贝 SmartPointer
cache_.splice(cache_.begin(), cache_, it); // touch LRU移到 front
return img;
}
vtkSmartPointer<vtkImageData> AsyncRegionBuilder::takeLatest() {
int dummy = 0;
return takeLatest(dummy);
} }
vtkSmartPointer<vtkImageData> AsyncRegionBuilder::takeLatest(int& outLevel) { vtkSmartPointer<vtkImageData> AsyncRegionBuilder::takeLatest(int& outLevel) {
// 非阻塞:仅在锁内做指针移动。消费式——仅当主目标有未消费的新就绪结果时返回非空。
std::lock_guard<std::mutex> lk(mutex_); std::lock_guard<std::mutex> lk(mutex_);
if (ready_) outLevel = readyLevel_; // 仅有新结果时回传 level if (!latestMainFresh_) return nullptr;
return std::move(ready_); outLevel = latestMainLevel_;
latestMainFresh_ = false;
return std::move(latestMain_); // 移交所有权(锁内,单线程 refcount
} }
void AsyncRegionBuilder::workerLoop() { void AsyncRegionBuilder::workerLoop() {
std::unique_lock<std::mutex> lk(mutex_); std::unique_lock<std::mutex> lk(mutex_);
for (;;) { for (;;) {
// 等到有期望或停止。 cv_.wait(lk, [this] {
cv_.wait(lk, [this] { return stop_.load() || hasDesired_; }); return stop_.load() || (hasMain_ && mainPending_) || !prefetchQ_.empty();
});
if (stop_.load()) return; if (stop_.load()) return;
// 取出最新期望supersede只建当前最新丢弃中途旧请求 RegionTarget target{};
const RegionTarget target = desired_; bool isMain = false;
if (!pickNextLocked(target, isMain)) continue; // 虚假唤醒/已全缓存
const int maxDim = maxTextureDim_; const int maxDim = maxTextureDim_;
hasDesired_ = false;
building_ = true; building_ = true;
// 解锁后构建worker 在自己线程内重组,主线程全程不触碰该局部 image。 // 解锁后构建worker 在自己线程内重组,主线程全程不触碰该局部 image。
@ -69,25 +154,40 @@ void AsyncRegionBuilder::workerLoop() {
if (stop_.load()) return; if (stop_.load()) return;
// 若构建期间又来了更新的期望,则本次结果已过期——丢弃,继续建最新。 building_ = false;
// built 离开作用域前在锁内释放 refcount单线程独占
if (hasDesired_) { if (built == nullptr) {
building_ = false; // 空区间(不应发生于合法目标):标记主目标已尝试,避免死循环空转。
built = nullptr; // 锁内释放(单线程独占 refcount if (isMain && hasMain_ && mainTarget_ == target) mainPending_ = false;
continue; continue;
} }
// publish在锁内把所有权 move 进 ready_旧 ready_ 若未取走在此处锁内释放)。 // 入就绪缓存(锁内,所有 refcount 增减单线程独占)。
// 所有 refcount 增减均在锁内/单线程独占完成。随结果一同发布其 level。 insertCacheLocked(target, built, target.level);
ready_ = std::move(built);
readyLevel_ = target.level; if (isMain) {
building_ = false; // 主目标就绪:仅当它仍是当前主目标时清 pending、发布给 takeLatest。
// (构建期间主目标若已变更,则本次非最新——保留新主目标的 pending 由下轮建。)
if (hasMain_ && mainTarget_ == target) {
mainPending_ = false;
// 消费式发布:从缓存拷一份给 takeLatestcontent 同缓存,不改)。
latestMain_ = built;
latestMainLevel_ = target.level;
latestMainFresh_ = true;
}
}
built = nullptr; // 锁内释放本地引用(缓存已持有)
} }
} }
bool AsyncRegionBuilder::hasPending() const { bool AsyncRegionBuilder::hasPending() const {
std::lock_guard<std::mutex> lk(mutex_); std::lock_guard<std::mutex> lk(mutex_);
return hasDesired_ || building_; return (hasMain_ && mainPending_) || building_ || !prefetchQ_.empty();
}
std::size_t AsyncRegionBuilder::cacheSize() const {
std::lock_guard<std::mutex> lk(mutex_);
return cache_.size();
} }
} // namespace geopro::render } // namespace geopro::render

View File

@ -1,9 +1,12 @@
#pragma once #pragma once
#include <atomic> #include <atomic>
#include <condition_variable> #include <condition_variable>
#include <cstddef>
#include <list>
#include <mutex> #include <mutex>
#include <string> #include <string>
#include <thread> #include <thread>
#include <vector>
#include <vtkImageData.h> #include <vtkImageData.h>
#include <vtkSmartPointer.h> #include <vtkSmartPointer.h>
@ -16,19 +19,27 @@ namespace geopro::render {
// C3 并发核心:把「从 store 重组视野区域单纹理」放后台线程,主线程非阻塞取最新 // C3 并发核心:把「从 store 重组视野区域单纹理」放后台线程,主线程非阻塞取最新
// 就绪结果——使拖动/缩放时不被解压+重组卡住。 // 就绪结果——使拖动/缩放时不被解压+重组卡住。
// //
// C3-3 增强:就绪缓存(按 RegionTarget LRU容量 N) + 预取(后台额外建预测的下一目标,
// 不抢占主目标) + 相同目标短路(已就绪/在建则不重复提交,修 C3-2 LOW)。
//
// 线程安全设计VTK 引用计数跨线程要小心): // 线程安全设计VTK 引用计数跨线程要小心):
// - 后台 worker 在自己线程内调 reorganizeRegionC2/C3 共用重组核)构建出一个 // - 后台 worker 在自己线程内调 reorganizeRegionC2/C3 共用重组核)构建出一个
// vtkSmartPointer<vtkImageData>(全程不被主线程触碰)。 // vtkSmartPointer<vtkImageData>(全程不被主线程触碰)。
// - 完成后在 mutex 下把它移交到成员 ready_。主线程在同一 mutex 下取走(移动出)。 // - 完成后在 mutex 下把它存入【就绪缓存】(按 target 键的 LRU。主线程在同一
// 所有 vtkImageData 引用计数的增减都发生在锁内/单线程独占——避免两线程同时碰 // mutex 下从缓存取(拷贝 SmartPointer 引用)。所有 vtkImageData 引用计数的增减
// 同一对象的 refcount。worker 构建期间主线程看不到它publish 后所有权经锁交给 // 都发生在锁内——避免两线程同时碰同一对象的 refcount。worker 构建期间主线程看
// 主线程。 // 不到它publish 后所有权经锁可见。
// - 请求合并/取最新supersedeworker 在建 A 时若 requestTarget(B),完成 A 后 // - 主目标优先worker 每轮先建主目标(若未缓存),否则建预取队列里最新的;
// 去建最新期望B不堆积旧请求——主线程高频 update 不致排队爆炸。 // 预取永不抢占主目标——主目标一旦变更worker 完成当前一格后立即转向主目标。
// - 短路requestTarget 同一目标若已缓存或正在建,不重复提交,省 worker 空转。
class AsyncRegionBuilder { class AsyncRegionBuilder {
public: public:
// 起 worker 线程。storeDir含金字塔的分块 store。 // 就绪缓存容量(按 RegionTarget LRU
explicit AsyncRegionBuilder(const std::string& storeDir); static constexpr std::size_t kDefaultCacheCapacity = 6;
// 起 worker 线程。storeDir含金字塔的分块 store。cacheCapacity就绪缓存容量。
explicit AsyncRegionBuilder(const std::string& storeDir,
std::size_t cacheCapacity = kDefaultCacheCapacity);
// 停 workerjoin。析构忙时干净 join 不崩、不泄漏、不死锁。 // 停 workerjoin。析构忙时干净 join 不崩、不泄漏、不死锁。
~AsyncRegionBuilder(); ~AsyncRegionBuilder();
@ -36,40 +47,77 @@ class AsyncRegionBuilder {
AsyncRegionBuilder(const AsyncRegionBuilder&) = delete; AsyncRegionBuilder(const AsyncRegionBuilder&) = delete;
AsyncRegionBuilder& operator=(const AsyncRegionBuilder&) = delete; AsyncRegionBuilder& operator=(const AsyncRegionBuilder&) = delete;
// 主线程调:设期望目标。与当前在建/已建不同则唤醒 worker 重建;相同则忽略。 // 主线程调:设主目标(优先建)。短路:若该 target 已在就绪缓存或正在建,不重复
// 提交(仅置为主目标供 getReady/takeLatest 命中),省 worker 空转。
void requestTarget(const RegionTarget& t); void requestTarget(const RegionTarget& t);
// 主线程调:取最新已就绪 image非阻塞。无新结果返回 nullptr主线程继续用 // 主线程调:预取这些目标(后台额外建,低优先,绝不抢占主目标)。已缓存或与主目标
// 上一张)。取走后清空,下次无新结果再调返回 nullptr。 // 相同的会被跳过。建好后入就绪缓存LRU 有界)。
// 永不阻塞主线程:仅在锁内做指针移动。 void prefetch(const std::vector<RegionTarget>& targets);
// 主线程调:非阻塞取某 target 的就绪 image。缓存命中→返回该 image 并 touch LRU
// (回传其 level 到 levelOut未命中→nullptrlevelOut 不变)。永不阻塞主线程。
vtkSmartPointer<vtkImageData> getReady(const RegionTarget& t, int& levelOut);
// 主线程调取最新已就绪的【主目标】image非阻塞兼容 C3-1/C3-2 行为)。
// 仅当主目标自上次 take 后有新就绪结果时返回非空(消费式:取走后再调返回 nullptr
// 直到主目标再次就绪)。无新结果返回 nullptr主线程继续用上一张
vtkSmartPointer<vtkImageData> takeLatest(); vtkSmartPointer<vtkImageData> takeLatest();
// 同上,并通过 outLevel 回传该就绪结果对应的 target.level仅当返回非空时有效 // 同上,并通过 outLevel 回传该就绪结果对应的 target.level仅当返回非空时有效
// 返回空时 outLevel 不变)。供调用方同步 UI 的 LOD 显示C3-2 ViewAdaptive 用)。
// 非破坏式重载:无参版行为不变。
vtkSmartPointer<vtkImageData> takeLatest(int& outLevel); vtkSmartPointer<vtkImageData> takeLatest(int& outLevel);
// 是否有在建/排队(供 UI/测试)。 // 是否有在建/排队(主目标未就绪或预取队列非空,供 UI/测试)。
bool hasPending() const; bool hasPending() const;
// 就绪缓存当前条目数(供测试验证 LRU 有界)。
std::size_t cacheSize() const;
// 单张 3D 纹理各轴上限GL_MAX_3D_TEXTURE_SIZE。须在 requestTarget 前设。 // 单张 3D 纹理各轴上限GL_MAX_3D_TEXTURE_SIZE。须在 requestTarget 前设。
void setMaxTextureDim(int dim); void setMaxTextureDim(int dim);
private: private:
void workerLoop(); void workerLoop();
// 就绪缓存条目LRUfront=最近用back=最久未用)。
struct CacheEntry {
RegionTarget target;
vtkSmartPointer<vtkImageData> image;
int level;
};
// 以下私有辅助均要求调用方已持锁。
// 查缓存命中(返回迭代器或 end
std::list<CacheEntry>::iterator findCached(const RegionTarget& t);
// 插入/更新缓存并把该条目移到 front超容则淘汰 back。
void insertCacheLocked(const RegionTarget& t, vtkSmartPointer<vtkImageData> img,
int level);
// worker 选下一个要建的目标:主目标未缓存→主目标;否则预取队列里最新未缓存的;
// 都没有→返回 false。out 回传目标与是否为主目标。
bool pickNextLocked(RegionTarget& out, bool& isMain);
geopro::data::ChunkedVolumeStore store_; geopro::data::ChunkedVolumeStore store_;
mutable std::mutex mutex_; mutable std::mutex mutex_;
std::condition_variable cv_; std::condition_variable cv_;
// 受 mutex_ 保护的共享状态: // 受 mutex_ 保护的共享状态:
RegionTarget desired_{}; // 主线程最新期望目标 RegionTarget mainTarget_{}; // 主线程最新主目标
bool hasDesired_ = false; // 是否有未消费的期望 bool hasMain_ = false; // 是否设过主目标
bool building_ = false; // worker 当前是否在建 bool mainPending_ = false; // 主目标尚未就绪(未在缓存)→ 需建
vtkSmartPointer<vtkImageData> ready_; // 已就绪、待主线程取走的最新结果 std::vector<RegionTarget> prefetchQ_; // 预取队列(低优先;建最新的先)
int readyLevel_ = 0; // ready_ 对应 target.level随 ready_ 一同发布) bool building_ = false; // worker 当前是否在建
std::atomic<bool> stop_{false}; // 析构置位,唤醒 worker 退出
// 就绪缓存LRU。front=最近用。
std::list<CacheEntry> cache_;
std::size_t cacheCapacity_ = kDefaultCacheCapacity;
// takeLatest 消费式发布worker 建好主目标后置位takeLatest 取走后清零。
vtkSmartPointer<vtkImageData> latestMain_;
int latestMainLevel_ = 0;
bool latestMainFresh_ = false; // 有未被 takeLatest 消费的新主目标结果
std::atomic<bool> stop_{false}; // 析构置位,唤醒 worker 退出
int maxTextureDim_ = 16384; int maxTextureDim_ = 16384;
std::thread worker_; // 最后声明:构造完上述成员后再起线程 std::thread worker_; // 最后声明:构造完上述成员后再起线程

View File

@ -64,13 +64,59 @@ void ViewAdaptiveVolumeSource::updateView(const CameraView& cam,
target.bz0 = sel.bz0; target.bz0 = sel.bz0;
target.bz1 = sel.bz1; target.bz1 = sel.bz1;
target.exagg = exagg_; target.exagg = exagg_;
builder_.requestTarget(target); // 与在建/已建相同则忽略;否则唤醒 worker 重建 mainTarget_ = target;
hasMainTarget_ = true;
builder_.requestTarget(target); // 短路:已就绪/在建则不重复;否则唤醒 worker
// C3-3预测下一目标并预取低优先不抢主目标
const std::vector<RegionTarget> next = predictNext(target, cam);
if (!next.empty()) builder_.prefetch(next);
}
std::vector<RegionTarget> ViewAdaptiveVolumeSource::predictNext(
const RegionTarget& main, const CameraView& cam) const {
std::vector<RegionTarget> out;
// (a) 拉近预测更细一层level-1同区。金字塔下一细层每个 brick 区间在更细层
// 大致翻倍level L 维度 = ceil(n/2^L)。简单可用level-1brick 区间 ×2。
if (main.level > 0) {
RegionTarget finer = main;
finer.level = main.level - 1;
finer.bx0 = main.bx0 * 2;
finer.bx1 = main.bx1 * 2;
finer.by0 = main.by0 * 2;
finer.by1 = main.by1 * 2;
finer.bz0 = main.bz0 * 2;
finer.bz1 = main.bz1 * 2;
out.push_back(finer);
}
// (b) 邻接平移沿相机水平前进方向focal-pos 在 x 上的符号)平移一格 brick 列。
// 简单可用:只在 x 轴平移主区间宽度的「一格 brick」区间宽 = bx1-bx0平移 1 列)。
const int wx = main.bx1 - main.bx0;
if (wx > 0) {
const double dx = cam.focal[0] - cam.pos[0];
const int dir = dx >= 0 ? 1 : -1; // 朝相机视线前进方向
RegionTarget shifted = main;
shifted.bx0 = main.bx0 + dir;
shifted.bx1 = main.bx1 + dir;
// 越界(负)则改向另一侧,仍非负才纳入(避免无效区间)。
if (shifted.bx0 < 0) {
shifted.bx0 = main.bx0 - dir;
shifted.bx1 = main.bx1 - dir;
}
if (shifted.bx0 >= 0 && shifted != main) out.push_back(shifted);
}
return out;
} }
void ViewAdaptiveVolumeSource::pullLatest() const { void ViewAdaptiveVolumeSource::pullLatest() const {
// 非阻塞:取最新已就绪。有新结果则换上 current_ 并同步 lastLevel_否则沿用上一张。 // 非阻塞:主目标 getReady 命中(含预取已备好的区域→即刻就绪)则换上 current_ 并
// 同步 lastLevel_否则沿用上一张C3-2 行为:拖动/出界不闪空)。
if (!hasMainTarget_) return;
int level = lastLevel_; int level = lastLevel_;
vtkSmartPointer<vtkImageData> latest = builder_.takeLatest(level); vtkSmartPointer<vtkImageData> latest = builder_.getReady(mainTarget_, level);
if (latest) { if (latest) {
current_ = std::move(latest); current_ = std::move(latest);
lastLevel_ = level; lastLevel_ = level;

View File

@ -89,10 +89,16 @@ class ViewAdaptiveVolumeSource : public IVolumeRenderSource {
// 由 meta + exagg 填 VolumeViewspacing 已含 exagg 于 y/z // 由 meta + exagg 填 VolumeViewspacing 已含 exagg 于 y/z
VolumeView volumeView() const; VolumeView volumeView() const;
// 从 builder 拉一次最新就绪结果:有新结果则更新 current_/lastLevel_。 // 从 builder 拉一次最新就绪结果:主目标 getReady 命中→用之并更新 current_/
// const仅刷新 mutable 缓存),供 currentImages/sliceSource 共用DRY // lastLevel_否则沿用上一张C3-2 行为。const仅刷新 mutable 缓存),供
// currentImages/sliceSource 共用DRY
void pullLatest() const; void pullLatest() const;
// C3-3 预测下一目标(先简单可用):从主选区派生 (a) 更细一层同区、(b) 沿相机
// 前进方向平移一格 brick 列的邻接区间。返回去重后的预取候选。
std::vector<RegionTarget> predictNext(const RegionTarget& main,
const CameraView& cam) const;
geopro::data::ChunkedVolumeStore store_; geopro::data::ChunkedVolumeStore store_;
geopro::data::StoreMeta meta_; geopro::data::StoreMeta meta_;
double exagg_ = 1.0; double exagg_ = 1.0;
@ -107,6 +113,10 @@ class ViewAdaptiveVolumeSource : public IVolumeRenderSource {
// 最新已就绪单图 + 其 levelmutablecurrentImages/sliceSource const 内懒取最新)。 // 最新已就绪单图 + 其 levelmutablecurrentImages/sliceSource const 内懒取最新)。
mutable vtkSmartPointer<vtkImageData> current_; // 空指针 = 从未就绪 mutable vtkSmartPointer<vtkImageData> current_; // 空指针 = 从未就绪
mutable int lastLevel_ = 0; mutable int lastLevel_ = 0;
// 当前主目标updateView 设pullLatest 用它向 builder getReady
RegionTarget mainTarget_{};
bool hasMainTarget_ = false;
}; };
} // namespace geopro::render } // namespace geopro::render

View File

@ -214,3 +214,128 @@ TEST(AsyncRegionBuilder, TakeLatestNonBlockingWhenEmpty) {
EXPECT_EQ(img.Get(), nullptr); EXPECT_EQ(img.Get(), nullptr);
EXPECT_LT(dtMs, 100); // 远小于任一次重组耗时,证明未阻塞等 worker EXPECT_LT(dtMs, 100); // 远小于任一次重组耗时,证明未阻塞等 worker
} }
// ── C3-3轮询某 target 的 getReady 直到非空(带超时)──────────────────────
vtkSmartPointer<vtkImageData> waitGetReady(AsyncRegionBuilder& b,
const RegionTarget& t,
int maxTries = 800, int stepMs = 5) {
vtkSmartPointer<vtkImageData> img;
int lv = -1;
for (int i = 0; i < maxTries && img == nullptr; ++i) {
img = b.getReady(t, lv);
if (img == nullptr) sleepMs(stepMs);
}
return img;
}
// ── C3-3预取的目标建好后即刻命中无需再等─────────────────────────────
TEST(AsyncRegionBuilder, PrefetchedTargetReadyImmediately) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_arb_prefetch").string();
makePyramidStore(dir, 200, 80, 60, 64, 3);
const RegionTarget cur = makeTarget(1, 0, 2, 0, 1, 0, 1);
const RegionTarget nxt = makeTarget(1, 1, 3, 0, 2, 0, 1);
AsyncRegionBuilder b(dir);
b.requestTarget(cur);
b.prefetch({nxt});
// 等两者都进缓存就绪。
ASSERT_NE(waitGetReady(b, cur).Get(), nullptr);
ASSERT_NE(waitGetReady(b, nxt).Get(), nullptr);
// 此后 getReady(nxt) 即刻命中(无需再等;计时极短)。
const auto t0 = std::chrono::steady_clock::now();
int lv = -1;
auto hit = b.getReady(nxt, lv);
const auto dtMs = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - t0)
.count();
ASSERT_NE(hit.Get(), nullptr);
EXPECT_EQ(lv, nxt.level);
EXPECT_LT(dtMs, 50);
// 内容 == 同步重组(缓存不改内容)。
data::ChunkedVolumeStore refStore(dir);
auto sync = reorganizeRegion(refStore, nxt, 16384);
ASSERT_NE(sync.Get(), nullptr);
EXPECT_TRUE(imagesEqual(hit.Get(), sync.Get()));
}
// ── C3-3缓存有界 LRU——请求/预取 > 容量个不同 target → size ≤ N ────────────
TEST(AsyncRegionBuilder, CacheIsBoundedLru) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_arb_lru").string();
makePyramidStore(dir, 200, 80, 60, 64, 3);
const std::size_t cap = 3;
AsyncRegionBuilder b(dir, cap);
// 提交 6 个不同 target> 容量),逐个等就绪。
std::vector<RegionTarget> ts = {
makeTarget(0, 0, 1, 0, 1, 0, 1), makeTarget(0, 1, 2, 0, 1, 0, 1),
makeTarget(0, 2, 3, 0, 1, 0, 1), makeTarget(1, 0, 1, 0, 1, 0, 1),
makeTarget(1, 1, 2, 0, 1, 0, 1), makeTarget(2, 0, 1, 0, 1, 0, 1),
};
for (const auto& t : ts) {
b.requestTarget(t);
ASSERT_NE(waitGetReady(b, t).Get(), nullptr) << "target 未就绪";
}
// 缓存有界size ≤ 容量。
EXPECT_LE(b.cacheSize(), cap);
// 最久未用的第一个应被淘汰getReady 即刻 nullptr不重建——非阻塞
int lv = -1;
EXPECT_EQ(b.getReady(ts.front(), lv).Get(), nullptr)
<< "最久未用项应已被 LRU 淘汰";
// 最近的几个仍命中。
EXPECT_NE(b.getReady(ts.back(), lv).Get(), nullptr);
}
// ── C3-3相同请求短路——同一 target 连请求多次不重复建(建一次后命中缓存)────
TEST(AsyncRegionBuilder, IdenticalRequestShortCircuits) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_arb_short").string();
makePyramidStore(dir, 200, 80, 60, 64, 3);
AsyncRegionBuilder b(dir);
const RegionTarget t = makeTarget(1, 0, 2, 0, 1, 0, 1);
b.requestTarget(t);
ASSERT_NE(waitGetReady(b, t).Get(), nullptr);
// 已就绪后再连发同一 target短路不应再有 pending/在建),缓存仍 1 条。
for (int i = 0; i < 10; ++i) b.requestTarget(t);
// 给 worker 一点时间(若误重建会出现 pending/building
sleepMs(50);
EXPECT_FALSE(b.hasPending()) << "同一已就绪 target 不应触发重建";
EXPECT_EQ(b.cacheSize(), 1u) << "同一 target 不应在缓存里重复占位";
// 短路时主目标仍可经 takeLatest 取到(兼容 C3-1/C3-2
int lv = -1;
EXPECT_NE(b.getReady(t, lv).Get(), nullptr);
}
// ── C3-3预取不饿死主目标——大量 prefetch + 一个主 requestTarget → 主先就绪 ──
TEST(AsyncRegionBuilder, PrefetchDoesNotStarveMainTarget) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_arb_starve").string();
makePyramidStore(dir, 200, 80, 60, 64, 3);
AsyncRegionBuilder b(dir, /*cap*/ 6);
// 先灌大量预取(不同 target再设主目标。
std::vector<RegionTarget> many;
for (int i = 0; i < 12; ++i)
many.push_back(makeTarget(0, i % 3, (i % 3) + 1, 0, 1, 0, 1, 1.0 + i));
b.prefetch(many);
const RegionTarget main = makeTarget(2, 0, 1, 0, 1, 0, 1);
b.requestTarget(main);
// 主目标应优先就绪(轮询 getReady(main) 命中)。
ASSERT_NE(waitGetReady(b, main, 800, 2).Get(), nullptr)
<< "预取饿死了主目标";
}