feat/vtk-3d-view #7
|
|
@ -4,8 +4,10 @@
|
|||
|
||||
namespace geopro::render {
|
||||
|
||||
AsyncRegionBuilder::AsyncRegionBuilder(const std::string& storeDir)
|
||||
: store_(storeDir) {
|
||||
AsyncRegionBuilder::AsyncRegionBuilder(const std::string& storeDir,
|
||||
std::size_t cacheCapacity)
|
||||
: store_(storeDir),
|
||||
cacheCapacity_(cacheCapacity > 0 ? cacheCapacity : 1) {
|
||||
worker_ = std::thread([this] { workerLoop(); });
|
||||
}
|
||||
|
||||
|
|
@ -23,42 +25,125 @@ void AsyncRegionBuilder::setMaxTextureDim(int 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); // 移到 front(touch)
|
||||
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) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mutex_);
|
||||
// 与已就绪结果对应的目标相同则不必重建(避免重复劳动);与在建/排队的期望相同
|
||||
// 亦无需扰动。简化:直接覆盖期望并标脏——worker 会以最新期望为准(supersede)。
|
||||
desired_ = t;
|
||||
hasDesired_ = true;
|
||||
mainTarget_ = t;
|
||||
hasMain_ = true;
|
||||
// 短路:已在就绪缓存→无需重建,但仍把缓存结果发布给 takeLatest(兼容 C3-1/C3-2:
|
||||
// 主目标切到一个已缓存区域时下一帧 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();
|
||||
}
|
||||
|
||||
vtkSmartPointer<vtkImageData> AsyncRegionBuilder::takeLatest() {
|
||||
// 非阻塞:仅在锁内做指针移动(不做任何重组/IO/长时操作)。
|
||||
// std::move 把 ready_ 的引用移交给返回值(无 refcount 增减,且全程在锁内/单线程),
|
||||
// ready_ 置空——下次无新结果再调返回 nullptr。
|
||||
void AsyncRegionBuilder::prefetch(const std::vector<RegionTarget>& targets) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mutex_);
|
||||
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_);
|
||||
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) {
|
||||
// 非阻塞:仅在锁内做指针移动。消费式——仅当主目标有未消费的新就绪结果时返回非空。
|
||||
std::lock_guard<std::mutex> lk(mutex_);
|
||||
if (ready_) outLevel = readyLevel_; // 仅有新结果时回传 level
|
||||
return std::move(ready_);
|
||||
if (!latestMainFresh_) return nullptr;
|
||||
outLevel = latestMainLevel_;
|
||||
latestMainFresh_ = false;
|
||||
return std::move(latestMain_); // 移交所有权(锁内,单线程 refcount)
|
||||
}
|
||||
|
||||
void AsyncRegionBuilder::workerLoop() {
|
||||
std::unique_lock<std::mutex> lk(mutex_);
|
||||
for (;;) {
|
||||
// 等到有期望或停止。
|
||||
cv_.wait(lk, [this] { return stop_.load() || hasDesired_; });
|
||||
cv_.wait(lk, [this] {
|
||||
return stop_.load() || (hasMain_ && mainPending_) || !prefetchQ_.empty();
|
||||
});
|
||||
if (stop_.load()) return;
|
||||
|
||||
// 取出最新期望(supersede:只建当前最新,丢弃中途旧请求)。
|
||||
const RegionTarget target = desired_;
|
||||
RegionTarget target{};
|
||||
bool isMain = false;
|
||||
if (!pickNextLocked(target, isMain)) continue; // 虚假唤醒/已全缓存
|
||||
|
||||
const int maxDim = maxTextureDim_;
|
||||
hasDesired_ = false;
|
||||
building_ = true;
|
||||
|
||||
// 解锁后构建:worker 在自己线程内重组,主线程全程不触碰该局部 image。
|
||||
|
|
@ -69,25 +154,40 @@ void AsyncRegionBuilder::workerLoop() {
|
|||
|
||||
if (stop_.load()) return;
|
||||
|
||||
// 若构建期间又来了更新的期望,则本次结果已过期——丢弃,继续建最新。
|
||||
// built 离开作用域前在锁内释放 refcount(单线程独占)。
|
||||
if (hasDesired_) {
|
||||
building_ = false;
|
||||
built = nullptr; // 锁内释放(单线程独占 refcount)
|
||||
building_ = false;
|
||||
|
||||
if (built == nullptr) {
|
||||
// 空区间(不应发生于合法目标):标记主目标已尝试,避免死循环空转。
|
||||
if (isMain && hasMain_ && mainTarget_ == target) mainPending_ = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// publish:在锁内把所有权 move 进 ready_(旧 ready_ 若未取走在此处锁内释放)。
|
||||
// 所有 refcount 增减均在锁内/单线程独占完成。随结果一同发布其 level。
|
||||
ready_ = std::move(built);
|
||||
readyLevel_ = target.level;
|
||||
building_ = false;
|
||||
// 入就绪缓存(锁内,所有 refcount 增减单线程独占)。
|
||||
insertCacheLocked(target, built, target.level);
|
||||
|
||||
if (isMain) {
|
||||
// 主目标就绪:仅当它仍是当前主目标时清 pending、发布给 takeLatest。
|
||||
// (构建期间主目标若已变更,则本次非最新——保留新主目标的 pending 由下轮建。)
|
||||
if (hasMain_ && mainTarget_ == target) {
|
||||
mainPending_ = false;
|
||||
// 消费式发布:从缓存拷一份给 takeLatest(content 同缓存,不改)。
|
||||
latestMain_ = built;
|
||||
latestMainLevel_ = target.level;
|
||||
latestMainFresh_ = true;
|
||||
}
|
||||
}
|
||||
built = nullptr; // 锁内释放本地引用(缓存已持有)
|
||||
}
|
||||
}
|
||||
|
||||
bool AsyncRegionBuilder::hasPending() const {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
#pragma once
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <vtkImageData.h>
|
||||
#include <vtkSmartPointer.h>
|
||||
|
|
@ -16,19 +19,27 @@ namespace geopro::render {
|
|||
// C3 并发核心:把「从 store 重组视野区域单纹理」放后台线程,主线程非阻塞取最新
|
||||
// 就绪结果——使拖动/缩放时不被解压+重组卡住。
|
||||
//
|
||||
// C3-3 增强:就绪缓存(按 RegionTarget LRU,容量 N) + 预取(后台额外建预测的下一目标,
|
||||
// 不抢占主目标) + 相同目标短路(已就绪/在建则不重复提交,修 C3-2 LOW)。
|
||||
//
|
||||
// 线程安全设计(VTK 引用计数跨线程要小心):
|
||||
// - 后台 worker 在自己线程内调 reorganizeRegion(C2/C3 共用重组核)构建出一个
|
||||
// vtkSmartPointer<vtkImageData>(全程不被主线程触碰)。
|
||||
// - 完成后在 mutex 下把它移交到成员 ready_。主线程在同一 mutex 下取走(移动出)。
|
||||
// 所有 vtkImageData 引用计数的增减都发生在锁内/单线程独占——避免两线程同时碰
|
||||
// 同一对象的 refcount。worker 构建期间主线程看不到它;publish 后所有权经锁交给
|
||||
// 主线程。
|
||||
// - 请求合并/取最新(supersede):worker 在建 A 时若 requestTarget(B),完成 A 后
|
||||
// 去建最新期望(B),不堆积旧请求——主线程高频 update 不致排队爆炸。
|
||||
// - 完成后在 mutex 下把它存入【就绪缓存】(按 target 键的 LRU)。主线程在同一
|
||||
// mutex 下从缓存取(拷贝 SmartPointer 引用)。所有 vtkImageData 引用计数的增减
|
||||
// 都发生在锁内——避免两线程同时碰同一对象的 refcount。worker 构建期间主线程看
|
||||
// 不到它;publish 后所有权经锁可见。
|
||||
// - 主目标优先:worker 每轮先建主目标(若未缓存),否则建预取队列里最新的;
|
||||
// 预取永不抢占主目标——主目标一旦变更,worker 完成当前一格后立即转向主目标。
|
||||
// - 短路:requestTarget 同一目标若已缓存或正在建,不重复提交,省 worker 空转。
|
||||
class AsyncRegionBuilder {
|
||||
public:
|
||||
// 起 worker 线程。storeDir:含金字塔的分块 store。
|
||||
explicit AsyncRegionBuilder(const std::string& storeDir);
|
||||
// 就绪缓存容量(按 RegionTarget LRU)。
|
||||
static constexpr std::size_t kDefaultCacheCapacity = 6;
|
||||
|
||||
// 起 worker 线程。storeDir:含金字塔的分块 store。cacheCapacity:就绪缓存容量。
|
||||
explicit AsyncRegionBuilder(const std::string& storeDir,
|
||||
std::size_t cacheCapacity = kDefaultCacheCapacity);
|
||||
|
||||
// 停 worker(join)。析构忙时干净 join 不崩、不泄漏、不死锁。
|
||||
~AsyncRegionBuilder();
|
||||
|
|
@ -36,40 +47,77 @@ class AsyncRegionBuilder {
|
|||
AsyncRegionBuilder(const AsyncRegionBuilder&) = delete;
|
||||
AsyncRegionBuilder& operator=(const AsyncRegionBuilder&) = delete;
|
||||
|
||||
// 主线程调:设期望目标。与当前在建/已建不同则唤醒 worker 重建;相同则忽略。
|
||||
// 主线程调:设主目标(优先建)。短路:若该 target 已在就绪缓存或正在建,不重复
|
||||
// 提交(仅置为主目标供 getReady/takeLatest 命中),省 worker 空转。
|
||||
void requestTarget(const RegionTarget& t);
|
||||
|
||||
// 主线程调:取最新已就绪 image(非阻塞)。无新结果返回 nullptr(主线程继续用
|
||||
// 上一张)。取走后清空,下次无新结果再调返回 nullptr。
|
||||
// 永不阻塞主线程:仅在锁内做指针移动。
|
||||
// 主线程调:预取这些目标(后台额外建,低优先,绝不抢占主目标)。已缓存或与主目标
|
||||
// 相同的会被跳过。建好后入就绪缓存(LRU 有界)。
|
||||
void prefetch(const std::vector<RegionTarget>& targets);
|
||||
|
||||
// 主线程调:非阻塞取某 target 的就绪 image。缓存命中→返回该 image 并 touch LRU
|
||||
// (回传其 level 到 levelOut);未命中→nullptr(levelOut 不变)。永不阻塞主线程。
|
||||
vtkSmartPointer<vtkImageData> getReady(const RegionTarget& t, int& levelOut);
|
||||
|
||||
// 主线程调:取最新已就绪的【主目标】image(非阻塞,兼容 C3-1/C3-2 行为)。
|
||||
// 仅当主目标自上次 take 后有新就绪结果时返回非空(消费式:取走后再调返回 nullptr
|
||||
// 直到主目标再次就绪)。无新结果返回 nullptr(主线程继续用上一张)。
|
||||
vtkSmartPointer<vtkImageData> takeLatest();
|
||||
|
||||
// 同上,并通过 outLevel 回传该就绪结果对应的 target.level(仅当返回非空时有效;
|
||||
// 返回空时 outLevel 不变)。供调用方同步 UI 的 LOD 显示(C3-2 ViewAdaptive 用)。
|
||||
// 非破坏式重载:无参版行为不变。
|
||||
// 同上,并通过 outLevel 回传该就绪结果对应的 target.level(仅当返回非空时有效)。
|
||||
vtkSmartPointer<vtkImageData> takeLatest(int& outLevel);
|
||||
|
||||
// 是否有在建/排队(供 UI/测试)。
|
||||
// 是否有在建/排队(主目标未就绪或预取队列非空,供 UI/测试)。
|
||||
bool hasPending() const;
|
||||
|
||||
// 就绪缓存当前条目数(供测试验证 LRU 有界)。
|
||||
std::size_t cacheSize() const;
|
||||
|
||||
// 单张 3D 纹理各轴上限(GL_MAX_3D_TEXTURE_SIZE)。须在 requestTarget 前设。
|
||||
void setMaxTextureDim(int dim);
|
||||
|
||||
private:
|
||||
void workerLoop();
|
||||
|
||||
// 就绪缓存条目(LRU:front=最近用,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_;
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
std::condition_variable cv_;
|
||||
|
||||
// 受 mutex_ 保护的共享状态:
|
||||
RegionTarget desired_{}; // 主线程最新期望目标
|
||||
bool hasDesired_ = false; // 是否有未消费的期望
|
||||
bool building_ = false; // worker 当前是否在建
|
||||
vtkSmartPointer<vtkImageData> ready_; // 已就绪、待主线程取走的最新结果
|
||||
int readyLevel_ = 0; // ready_ 对应 target.level(随 ready_ 一同发布)
|
||||
std::atomic<bool> stop_{false}; // 析构置位,唤醒 worker 退出
|
||||
RegionTarget mainTarget_{}; // 主线程最新主目标
|
||||
bool hasMain_ = false; // 是否设过主目标
|
||||
bool mainPending_ = false; // 主目标尚未就绪(未在缓存)→ 需建
|
||||
std::vector<RegionTarget> prefetchQ_; // 预取队列(低优先;建最新的先)
|
||||
bool building_ = 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;
|
||||
|
||||
std::thread worker_; // 最后声明:构造完上述成员后再起线程
|
||||
|
|
|
|||
|
|
@ -64,13 +64,59 @@ void ViewAdaptiveVolumeSource::updateView(const CameraView& cam,
|
|||
target.bz0 = sel.bz0;
|
||||
target.bz1 = sel.bz1;
|
||||
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-1,brick 区间 ×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 {
|
||||
// 非阻塞:取最新已就绪。有新结果则换上 current_ 并同步 lastLevel_;否则沿用上一张。
|
||||
// 非阻塞:主目标 getReady 命中(含预取已备好的区域→即刻就绪)则换上 current_ 并
|
||||
// 同步 lastLevel_;否则沿用上一张(C3-2 行为:拖动/出界不闪空)。
|
||||
if (!hasMainTarget_) return;
|
||||
int level = lastLevel_;
|
||||
vtkSmartPointer<vtkImageData> latest = builder_.takeLatest(level);
|
||||
vtkSmartPointer<vtkImageData> latest = builder_.getReady(mainTarget_, level);
|
||||
if (latest) {
|
||||
current_ = std::move(latest);
|
||||
lastLevel_ = level;
|
||||
|
|
|
|||
|
|
@ -89,10 +89,16 @@ class ViewAdaptiveVolumeSource : public IVolumeRenderSource {
|
|||
// 由 meta + exagg 填 VolumeView(spacing 已含 exagg 于 y/z)。
|
||||
VolumeView volumeView() const;
|
||||
|
||||
// 从 builder 拉一次最新就绪结果:有新结果则更新 current_/lastLevel_。
|
||||
// const(仅刷新 mutable 缓存),供 currentImages/sliceSource 共用(DRY)。
|
||||
// 从 builder 拉一次最新就绪结果:主目标 getReady 命中→用之并更新 current_/
|
||||
// lastLevel_;否则沿用上一张(C3-2 行为)。const(仅刷新 mutable 缓存),供
|
||||
// currentImages/sliceSource 共用(DRY)。
|
||||
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::StoreMeta meta_;
|
||||
double exagg_ = 1.0;
|
||||
|
|
@ -107,6 +113,10 @@ class ViewAdaptiveVolumeSource : public IVolumeRenderSource {
|
|||
// 最新已就绪单图 + 其 level(mutable:currentImages/sliceSource const 内懒取最新)。
|
||||
mutable vtkSmartPointer<vtkImageData> current_; // 空指针 = 从未就绪
|
||||
mutable int lastLevel_ = 0;
|
||||
|
||||
// 当前主目标(updateView 设;pullLatest 用它向 builder getReady)。
|
||||
RegionTarget mainTarget_{};
|
||||
bool hasMainTarget_ = false;
|
||||
};
|
||||
|
||||
} // namespace geopro::render
|
||||
|
|
|
|||
|
|
@ -214,3 +214,128 @@ TEST(AsyncRegionBuilder, TakeLatestNonBlockingWhenEmpty) {
|
|||
EXPECT_EQ(img.Get(), nullptr);
|
||||
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)
|
||||
<< "预取饿死了主目标";
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue