feat(vtk): ViewAdaptive 接异步重组(update 不阻塞,view 拖动不卡)+修 C2 维度用 store.dims

把 C3-1 AsyncRegionBuilder 接进 ViewAdaptiveVolumeSource:updateView 只提交目标
(selectLod→RegionTarget→requestTarget),不在主线程重组、立即返回;currentImages/
sliceSource 经 builder.takeLatest 取最新已就绪(没新结果用上一张),空选区不提交、
保留上一张。新增 AsyncRegionBuilder::takeLatest(int&) 非破坏式重载随结果回传 level,
供 lastLevel 同步。

C2 MEDIUM:RegionReorganizer 维度改用 store.dims(level)(单一真源),弃自算
ceil(n/2^level),防 store 降采样规则漂移。

gpr_poc view 切异步:拖动中 InteractionEvent 持续提交目标(非阻塞)+33ms 重复定时器
非阻塞拉取后台就绪纹理换上→主线程不被重组卡住(跟手);preview/smoke/默认取景用
阻塞轮询保证拿到首图。

测试:ViewAdaptive 9 测(原 6 调为异步轮询版+新增 3:AsyncUpdateEventuallyReady/
UpdateDoesNotBlock(<50ms)/UsesStoreDimsNotSelfComputed),AsyncRegionBuilder 5 测仍绿;
全量 382 测通过。
This commit is contained in:
gaozheng 2026-06-24 10:22:22 +08:00
parent fa34cb0bc3
commit cec41e3539
8 changed files with 321 additions and 74 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 121 KiB

View File

@ -42,6 +42,12 @@ vtkSmartPointer<vtkImageData> AsyncRegionBuilder::takeLatest() {
return std::move(ready_); return std::move(ready_);
} }
vtkSmartPointer<vtkImageData> AsyncRegionBuilder::takeLatest(int& outLevel) {
std::lock_guard<std::mutex> lk(mutex_);
if (ready_) outLevel = readyLevel_; // 仅有新结果时回传 level
return std::move(ready_);
}
void AsyncRegionBuilder::workerLoop() { void AsyncRegionBuilder::workerLoop() {
std::unique_lock<std::mutex> lk(mutex_); std::unique_lock<std::mutex> lk(mutex_);
for (;;) { for (;;) {
@ -72,8 +78,9 @@ void AsyncRegionBuilder::workerLoop() {
} }
// publish在锁内把所有权 move 进 ready_旧 ready_ 若未取走在此处锁内释放)。 // publish在锁内把所有权 move 进 ready_旧 ready_ 若未取走在此处锁内释放)。
// 所有 refcount 增减均在锁内/单线程独占完成。 // 所有 refcount 增减均在锁内/单线程独占完成。随结果一同发布其 level。
ready_ = std::move(built); ready_ = std::move(built);
readyLevel_ = target.level;
building_ = false; building_ = false;
} }
} }

View File

@ -44,6 +44,11 @@ class AsyncRegionBuilder {
// 永不阻塞主线程:仅在锁内做指针移动。 // 永不阻塞主线程:仅在锁内做指针移动。
vtkSmartPointer<vtkImageData> takeLatest(); vtkSmartPointer<vtkImageData> takeLatest();
// 同上,并通过 outLevel 回传该就绪结果对应的 target.level仅当返回非空时有效
// 返回空时 outLevel 不变)。供调用方同步 UI 的 LOD 显示C3-2 ViewAdaptive 用)。
// 非破坏式重载:无参版行为不变。
vtkSmartPointer<vtkImageData> takeLatest(int& outLevel);
// 是否有在建/排队(供 UI/测试)。 // 是否有在建/排队(供 UI/测试)。
bool hasPending() const; bool hasPending() const;
@ -63,6 +68,7 @@ class AsyncRegionBuilder {
bool hasDesired_ = false; // 是否有未消费的期望 bool hasDesired_ = false; // 是否有未消费的期望
bool building_ = false; // worker 当前是否在建 bool building_ = false; // worker 当前是否在建
vtkSmartPointer<vtkImageData> ready_; // 已就绪、待主线程取走的最新结果 vtkSmartPointer<vtkImageData> ready_; // 已就绪、待主线程取走的最新结果
int readyLevel_ = 0; // ready_ 对应 target.level随 ready_ 一同发布)
std::atomic<bool> stop_{false}; // 析构置位,唤醒 worker 退出 std::atomic<bool> stop_{false}; // 析构置位,唤醒 worker 退出
int maxTextureDim_ = 16384; int maxTextureDim_ = 16384;

View File

@ -10,16 +10,6 @@
namespace geopro::render { namespace geopro::render {
namespace {
// 该 level 某轴体素维度 = ceil(n / 2^level),至少 1与 C1/store 同口径)。
int dimAtLevel(int n, int level) {
const int d = (n + (1 << level) - 1) >> level;
return d > 0 ? d : 1;
}
} // namespace
vtkSmartPointer<vtkImageData> reorganizeRegion( vtkSmartPointer<vtkImageData> reorganizeRegion(
const geopro::data::ChunkedVolumeStore& store, const RegionTarget& target, const geopro::data::ChunkedVolumeStore& store, const RegionTarget& target,
int maxTextureDim) { int maxTextureDim) {
@ -28,9 +18,10 @@ vtkSmartPointer<vtkImageData> reorganizeRegion(
const double exagg = target.exagg > 0 ? target.exagg : 1.0; const double exagg = target.exagg > 0 ? target.exagg : 1.0;
const int brick = meta.brick; const int brick = meta.brick;
const int dimLx = dimAtLevel(meta.nx, level); // C2 MEDIUM维度一律取自 store.dims(level)(单一真源),不自算 ceil(n/2^level)
const int dimLy = dimAtLevel(meta.ny, level); // 防 store 降采样规则漂移时本侧公式失同步。store.dims 是金字塔实际落盘的权威维度。
const int dimLz = dimAtLevel(meta.nz, level); int dimLx = 0, dimLy = 0, dimLz = 0;
store.dims(level, dimLx, dimLy, dimLz);
// 重组单纹理某轴范围(与 C1 hpp 契约逐字一致): // 重组单纹理某轴范围(与 C1 hpp 契约逐字一致):
// 起点 = b0*brick终点 = min(b1*brick, dimL, 起点 + maxTextureDim)。 // 起点 = b0*brick终点 = min(b1*brick, dimL, 起点 + maxTextureDim)。

View File

@ -1,14 +1,19 @@
#include "source/ViewAdaptiveVolumeSource.hpp" #include "source/ViewAdaptiveVolumeSource.hpp"
#include <vtkCamera.h> #include <utility>
#include "source/RegionReorganizer.hpp" #include <vtkCamera.h>
namespace geopro::render { namespace geopro::render {
ViewAdaptiveVolumeSource::ViewAdaptiveVolumeSource(const std::string& storeDir, ViewAdaptiveVolumeSource::ViewAdaptiveVolumeSource(const std::string& storeDir,
double exagg) double exagg)
: store_(storeDir), meta_(store_.meta()), exagg_(exagg > 0 ? exagg : 1.0) {} : store_(storeDir),
meta_(store_.meta()),
exagg_(exagg > 0 ? exagg : 1.0),
builder_(storeDir) { // 后台 builder 用同一 storeDir独立打开线程独占
builder_.setMaxTextureDim(maxTextureDim_);
}
VolumeView ViewAdaptiveVolumeSource::volumeView() const { VolumeView ViewAdaptiveVolumeSource::volumeView() const {
VolumeView v{}; VolumeView v{};
@ -29,10 +34,7 @@ VolumeView ViewAdaptiveVolumeSource::volumeView() const {
} }
void ViewAdaptiveVolumeSource::update(vtkCamera* cam) { void ViewAdaptiveVolumeSource::update(vtkCamera* cam) {
if (cam == nullptr) { if (cam == nullptr) return; // 无相机:不提交目标(保留上一就绪结果)
current_ = nullptr;
return;
}
CameraView c{}; CameraView c{};
cam->GetPosition(c.pos); cam->GetPosition(c.pos);
cam->GetFocalPoint(c.focal); cam->GetFocalPoint(c.focal);
@ -47,12 +49,12 @@ void ViewAdaptiveVolumeSource::updateView(const CameraView& cam,
const VolumeView& vol) { const VolumeView& vol) {
const LodSelection sel = selectLod(vol, cam, maxTextureDim_); const LodSelection sel = selectLod(vol, cam, maxTextureDim_);
if (sel.empty) { if (sel.empty) {
current_ = nullptr; // 空选区(体在视锥外):不提交目标,保留上一就绪结果(拖动出界时不闪空)。
return; return;
} }
lastLevel_ = sel.level;
// C2 重组改委托公共重组核 reorganizeRegionDRYC3 AsyncRegionBuilder 同源)。 // C3-2只【提交目标】给后台 builder不在主线程重组不阻塞。维度/裁剪由
// reorganizeRegion 内按 store.dims 处理;本侧只传 brick 区间 + level + exagg。
RegionTarget target{}; RegionTarget target{};
target.level = sel.level; target.level = sel.level;
target.bx0 = sel.bx0; target.bx0 = sel.bx0;
@ -62,13 +64,29 @@ 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_;
current_ = reorganizeRegion(store_, target, maxTextureDim_); builder_.requestTarget(target); // 与在建/已建相同则忽略;否则唤醒 worker 重建
}
void ViewAdaptiveVolumeSource::pullLatest() const {
// 非阻塞:取最新已就绪。有新结果则换上 current_ 并同步 lastLevel_否则沿用上一张。
int level = lastLevel_;
vtkSmartPointer<vtkImageData> latest = builder_.takeLatest(level);
if (latest) {
current_ = std::move(latest);
lastLevel_ = level;
}
} }
std::vector<vtkSmartPointer<vtkImageData>> std::vector<vtkSmartPointer<vtkImageData>>
ViewAdaptiveVolumeSource::currentImages() const { ViewAdaptiveVolumeSource::currentImages() const {
pullLatest();
if (current_ == nullptr) return {}; if (current_ == nullptr) return {};
return {current_}; return {current_};
} }
vtkImageData* ViewAdaptiveVolumeSource::sliceSource() const {
pullLatest();
return current_.Get();
}
} // namespace geopro::render } // namespace geopro::render

View File

@ -7,19 +7,24 @@
#include "data/store/ChunkedVolumeStore.hpp" #include "data/store/ChunkedVolumeStore.hpp"
#include "lod/ViewAdaptiveLodPolicy.hpp" #include "lod/ViewAdaptiveLodPolicy.hpp"
#include "source/AsyncRegionBuilder.hpp"
#include "source/IVolumeRenderSource.hpp" #include "source/IVolumeRenderSource.hpp"
class vtkCamera; class vtkCamera;
namespace geopro::render { namespace geopro::render {
// C2 实现:视野自适应单纹理体绘制数据源。 // C2/C3-2 实现:视野自适应单纹理体绘制数据源(异步重组)
// //
// 用 C1 selectLod(VolumeView,CameraView,maxTextureDim) 选 LOD level + 视野内 brick // 用 C1 selectLod(VolumeView,CameraView,maxTextureDim) 选 LOD level + 视野内 brick
// 区间,从 ChunkedVolumeStore 把【当前视野区域】重组为【单张 VTK_SHORT // 区间,从 ChunkedVolumeStore 把【当前视野区域】重组为【单张 VTK_SHORT
// vtkImageData】各轴 ≤maxTextureDim由 C1 硬约束保证),带世界 origin/spacing // vtkImageData】各轴 ≤maxTextureDim由 C1 硬约束保证),带世界 origin/spacing
// (按 level + 垂向夸张 exagg // (按 level + 垂向夸张 exagg
// //
// C3-2 异步集成updateView 只【提交目标】给内含的 AsyncRegionBuilder
//不阻塞、不在主线程重组currentImages 取【最新已就绪】结果(没就绪就用上一
// 张)。这样拖动/缩放时主线程不被解压+重组卡住——后台备好新纹理后下一帧自然换上。
//
// 与 B(WholeVolumeSource 整卷单图)/旧 C(OutOfCoreSource MultiBlock 多块)的区别: // 与 B(WholeVolumeSource 整卷单图)/旧 C(OutOfCoreSource MultiBlock 多块)的区别:
// - 远观 → C1 选粗层、区间≈全体 → 重组整卷粗纹理(一张); // - 远观 → C1 选粗层、区间≈全体 → 重组整卷粗纹理(一张);
// - 近观 → C1 选细层、区间为视锥内小块 → 重组视野子体(一张); // - 近观 → C1 选细层、区间为视锥内小块 → 重组视野子体(一张);
@ -30,6 +35,12 @@ namespace geopro::render {
//(构造 vtkImageData 无需渲染管线)→ headless 可测。update(vtkCamera*) 仅把相机 //(构造 vtkImageData 无需渲染管线)→ headless 可测。update(vtkCamera*) 仅把相机
// 参数填成 CameraView 再调 updateView。viewportH/aspect 经 setter 注入vtkCamera // 参数填成 CameraView 再调 updateView。viewportH/aspect 经 setter 注入vtkCamera
// 不自带视口像素高/宽高比)。 // 不自带视口像素高/宽高比)。
//
// 线程契约:本类的【公共方法只由主/渲染线程调用】VTK 渲染循环单线程)。唯一的跨
// 线程边界在内含的 AsyncRegionBuilderworker 线程独占 builder 自己的 store 实例做
// 重组(与本类 store_ 是不同实例),主线程经 builder 的 mutex 保护的 takeLatest 取
// 结果——vtkImageData 的 refcount 增减全发生在锁内或主线程单线程,无跨线程竞争。
// current_/lastLevel_ 为 mutable仅由主线程在 currentImages/sliceSource 内更新。
class ViewAdaptiveVolumeSource : public IVolumeRenderSource { class ViewAdaptiveVolumeSource : public IVolumeRenderSource {
public: public:
// storeDir含金字塔的分块 store。exagg垂向夸张烘焙进 y/z 的 spacing/origin // storeDir含金字塔的分块 store。exagg垂向夸张烘焙进 y/z 的 spacing/origin
@ -42,14 +53,17 @@ class ViewAdaptiveVolumeSource : public IVolumeRenderSource {
// GL_MAX_3D_TEXTURE_SIZE 上限走 maxTextureDim_默认 16384 // GL_MAX_3D_TEXTURE_SIZE 上限走 maxTextureDim_默认 16384
void update(vtkCamera* cam) override; void update(vtkCamera* cam) override;
// 可测缝:纯数值核——选层选区 + 重组单图。headless 可测。 // 可测缝:选层选区 → 把目标【提交】给后台 builder不阻塞、不在主线程重组
// headless 可测。空选区 → 不提交(保留上一就绪结果)。
void updateView(const CameraView& cam, const VolumeView& vol); void updateView(const CameraView& cam, const VolumeView& vol);
// 当前视野区域单图empty → 空 vector场景不渲 // 当前视野区域单图(取最新已就绪:先 builder.takeLatest(),有新结果则换上并更新
// lastLevel_否则沿用上一张从未就绪 → 空 vector
// 非阻塞:仅在 builder 锁内做指针移动。current_/lastLevel_ 为 mutable懒取最新
std::vector<vtkSmartPointer<vtkImageData>> currentImages() const override; std::vector<vtkSmartPointer<vtkImageData>> currentImages() const override;
// reslice 源 = 当前单图empty → nullptr // reslice 源 = 当前最新就绪单图empty → nullptr。先拉一次最新就绪再返回
vtkImageData* sliceSource() const override { return current_.Get(); } vtkImageData* sliceSource() const override;
// 供 UI 显示当前 LOD level。 // 供 UI 显示当前 LOD level。
int lastLevel() const { return lastLevel_; } int lastLevel() const { return lastLevel_; }
@ -62,13 +76,23 @@ class ViewAdaptiveVolumeSource : public IVolumeRenderSource {
void setViewportHeight(int h) { viewportH_ = h > 0 ? h : viewportH_; } void setViewportHeight(int h) { viewportH_ = h > 0 ? h : viewportH_; }
void setAspect(double aspect) { aspect_ = aspect > 0 ? aspect : aspect_; } void setAspect(double aspect) { aspect_ = aspect > 0 ? aspect : aspect_; }
// 单张 3D 纹理各轴上限GL_MAX_3D_TEXTURE_SIZE // 单张 3D 纹理各轴上限GL_MAX_3D_TEXTURE_SIZE。同步给后台 builder重组用同
void setMaxTextureDim(int dim) { maxTextureDim_ = dim > 0 ? dim : maxTextureDim_; } // 一上限)。须在 updateView 前设。
void setMaxTextureDim(int dim) {
if (dim > 0) {
maxTextureDim_ = dim;
builder_.setMaxTextureDim(dim);
}
}
private: private:
// 由 meta + exagg 填 VolumeViewspacing 已含 exagg 于 y/z // 由 meta + exagg 填 VolumeViewspacing 已含 exagg 于 y/z
VolumeView volumeView() const; VolumeView volumeView() const;
// 从 builder 拉一次最新就绪结果:有新结果则更新 current_/lastLevel_。
// const仅刷新 mutable 缓存),供 currentImages/sliceSource 共用DRY
void pullLatest() 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;
@ -76,8 +100,13 @@ class ViewAdaptiveVolumeSource : public IVolumeRenderSource {
int viewportH_ = 1080; int viewportH_ = 1080;
double aspect_ = 1280.0 / 800.0; double aspect_ = 1280.0 / 800.0;
vtkSmartPointer<vtkImageData> current_; // 当前视野区域单图empty 时为空指针) // 后台重组器updateView 提交目标currentImages/sliceSource 非阻塞取最新就绪。
int lastLevel_ = 0; // 须在 store_ 之后、current_ 之前声明(构造顺序无依赖,但语义上属重组核心)。
mutable AsyncRegionBuilder builder_;
// 最新已就绪单图 + 其 levelmutablecurrentImages/sliceSource const 内懒取最新)。
mutable vtkSmartPointer<vtkImageData> current_; // 空指针 = 从未就绪
mutable int lastLevel_ = 0;
}; };
} // namespace geopro::render } // namespace geopro::render

View File

@ -1,23 +1,31 @@
// ViewAdaptiveVolumeSource(C2) headless 测试:用 C1 selectLod 选层选区,从分块 // ViewAdaptiveVolumeSource(C2→C3-2) headless 测试:用 C1 selectLod 选层选区,
// 存储重组当前视野区域为【单张 VTK_SHORT vtkImageData】各轴 ≤16384世界 // 当前视野区域【异步】重组为【单张 VTK_SHORT vtkImageData】各轴 ≤16384世界
// origin/spacing 按 level+exagg。核心 updateView(CameraView,VolumeView) 不需真 // origin/spacing 按 level+exagg。核心 updateView(CameraView,VolumeView) 不需真
// vtkCamera/GL 上下文——构造 vtkImageData 不需渲染管线。 // vtkCamera/GL 上下文——构造 vtkImageData 不需渲染管线。
// //
// C3-2 异步集成updateView 只提交目标不阻塞、不在主线程重组currentImages
// 取最新已就绪(没就绪用上一张)。测试需在 updateView 后【轮询 currentImages
// 直到非空(带超时)】再断言内容。
//
// 验:远观粗层 / 近观细层 / 各轴 ≤16384 / VTK_SHORT / 重组体素与 store 对应 // 验:远观粗层 / 近观细层 / 各轴 ≤16384 / VTK_SHORT / 重组体素与 store 对应
// level+区间位置一致(不错位)/ empty 情形空。 // level+区间位置一致(不错位)/ empty 情形空 / updateView 不阻塞 /
// 维度取自 store.dims单一真源奇数维一致/ 最终就绪内容 == reorganizeRegion。
#include "render/source/ViewAdaptiveVolumeSource.hpp" #include "render/source/ViewAdaptiveVolumeSource.hpp"
#include "core/algo/GprVolumeBuilder.hpp" #include "core/algo/GprVolumeBuilder.hpp"
#include "data/store/ChunkedVolumeStore.hpp" #include "data/store/ChunkedVolumeStore.hpp"
#include "lod/ViewAdaptiveLodPolicy.hpp" #include "lod/ViewAdaptiveLodPolicy.hpp"
#include "render/source/RegionReorganizer.hpp"
#include <vtkImageData.h> #include <vtkImageData.h>
#include <vtkPointData.h> #include <vtkPointData.h>
#include <vtkShortArray.h> #include <vtkShortArray.h>
#include <chrono>
#include <cmath> #include <cmath>
#include <filesystem> #include <filesystem>
#include <thread>
#include <gtest/gtest.h> #include <gtest/gtest.h>
using namespace geopro; using namespace geopro;
@ -92,9 +100,21 @@ CameraView lookFromX(const data::StoreMeta& m, double dist, double fovYDeg = 30.
return c; return c;
} }
// 异步轮询updateView 后 currentImages 可能还没就绪——轮询直到非空(带超时)。
// 返回首张就绪 image超时仍空 → 返回 nullptr由调用方 ASSERT
vtkSmartPointer<vtkImageData> pollReady(render::ViewAdaptiveVolumeSource& src,
int maxTries = 2000, int sleepMs = 2) {
for (int i = 0; i < maxTries; ++i) {
auto imgs = src.currentImages();
if (!imgs.empty() && imgs[0] != nullptr) return imgs[0];
std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs));
}
return nullptr;
}
} // namespace } // namespace
// ── 远观:选粗层、单图、各轴 ≤16384、VTK_SHORT ─────────────────────────────── // ── 远观:选粗层、单图、各轴 ≤16384、VTK_SHORT(异步轮询就绪后断言)─────────
TEST(ViewAdaptiveVolumeSource, FarViewCoarseLevelSingleTexture) { TEST(ViewAdaptiveVolumeSource, FarViewCoarseLevelSingleTexture) {
const auto dir = const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_far").string(); (std::filesystem::temp_directory_path() / "gpr_va_far").string();
@ -108,12 +128,14 @@ TEST(ViewAdaptiveVolumeSource, FarViewCoarseLevelSingleTexture) {
const CameraView far = lookFromX(src.meta(), 8000.0); const CameraView far = lookFromX(src.meta(), 8000.0);
src.updateView(far, vol); src.updateView(far, vol);
auto img = pollReady(src);
ASSERT_NE(img.Get(), nullptr);
// 就绪后 currentImages 仍是单张。
auto imgs = src.currentImages(); auto imgs = src.currentImages();
ASSERT_EQ(imgs.size(), 1u); ASSERT_EQ(imgs.size(), 1u);
ASSERT_NE(imgs[0].Get(), nullptr); EXPECT_EQ(img->GetScalarType(), VTK_SHORT);
EXPECT_EQ(imgs[0]->GetScalarType(), VTK_SHORT);
int d[3]; int d[3];
imgs[0]->GetDimensions(d); img->GetDimensions(d);
EXPECT_LE(d[0], 16384); EXPECT_LE(d[0], 16384);
EXPECT_LE(d[1], 16384); EXPECT_LE(d[1], 16384);
EXPECT_LE(d[2], 16384); EXPECT_LE(d[2], 16384);
@ -134,11 +156,11 @@ TEST(ViewAdaptiveVolumeSource, NearViewFineLevel) {
const CameraView near = lookFromX(src.meta(), 8.0, 20.0, 1080); const CameraView near = lookFromX(src.meta(), 8.0, 20.0, 1080);
src.updateView(near, vol); src.updateView(near, vol);
auto imgs = src.currentImages(); auto img = pollReady(src);
ASSERT_EQ(imgs.size(), 1u); ASSERT_NE(img.Get(), nullptr);
EXPECT_EQ(src.lastLevel(), 0); // 近 → 最细 EXPECT_EQ(src.lastLevel(), 0); // 近 → 最细
int d[3]; int d[3];
imgs[0]->GetDimensions(d); img->GetDimensions(d);
EXPECT_LE(d[0], 16384); EXPECT_LE(d[0], 16384);
EXPECT_LE(d[1], 16384); EXPECT_LE(d[1], 16384);
EXPECT_LE(d[2], 16384); EXPECT_LE(d[2], 16384);
@ -157,9 +179,9 @@ TEST(ViewAdaptiveVolumeSource, ReconstructedVoxelsMatchStore) {
const CameraView far = lookFromX(src.meta(), 8000.0); const CameraView far = lookFromX(src.meta(), 8000.0);
src.updateView(far, vol); src.updateView(far, vol);
auto imgs = src.currentImages(); auto pimg = pollReady(src);
ASSERT_EQ(imgs.size(), 1u); ASSERT_NE(pimg.Get(), nullptr);
vtkImageData* img = imgs[0]; vtkImageData* img = pimg.Get();
const int level = src.lastLevel(); const int level = src.lastLevel();
data::ChunkedVolumeStore store(dir); data::ChunkedVolumeStore store(dir);
@ -241,14 +263,14 @@ TEST(ViewAdaptiveVolumeSource, WorldOriginSpacingWithExagg) {
const CameraView far = lookFromX(src.meta(), 8000.0); const CameraView far = lookFromX(src.meta(), 8000.0);
src.updateView(far, vol); src.updateView(far, vol);
auto imgs = src.currentImages(); auto img = pollReady(src);
ASSERT_EQ(imgs.size(), 1u); ASSERT_NE(img.Get(), nullptr);
const data::StoreMeta& m = src.meta(); const data::StoreMeta& m = src.meta();
const int level = src.lastLevel(); const int level = src.lastLevel();
const double sc = static_cast<double>(1 << level); const double sc = static_cast<double>(1 << level);
double sp[3], org[3]; double sp[3], org[3];
imgs[0]->GetSpacing(sp); img->GetSpacing(sp);
imgs[0]->GetOrigin(org); img->GetOrigin(org);
// spacingx 不夸张y/z ×exagg。 // spacingx 不夸张y/z ×exagg。
EXPECT_DOUBLE_EQ(sp[0], m.spacing[0] * sc); EXPECT_DOUBLE_EQ(sp[0], m.spacing[0] * sc);
EXPECT_DOUBLE_EQ(sp[1], m.spacing[1] * sc * exagg); EXPECT_DOUBLE_EQ(sp[1], m.spacing[1] * sc * exagg);
@ -259,7 +281,7 @@ TEST(ViewAdaptiveVolumeSource, WorldOriginSpacingWithExagg) {
EXPECT_DOUBLE_EQ(org[2], m.origin[2]); EXPECT_DOUBLE_EQ(org[2], m.origin[2]);
} }
// ── empty体完全在视锥外 → currentImages 空 ─────────────────────────────── // ── empty体完全在视锥外 → currentImages 空(从未就绪)──────────────────────
TEST(ViewAdaptiveVolumeSource, EmptyWhenBehindCamera) { TEST(ViewAdaptiveVolumeSource, EmptyWhenBehindCamera) {
const auto dir = const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_empty").string(); (std::filesystem::temp_directory_path() / "gpr_va_empty").string();
@ -271,7 +293,11 @@ TEST(ViewAdaptiveVolumeSource, EmptyWhenBehindCamera) {
c.focal[0] = c.pos[0] + 1000.0; // 视线朝 +X体在身后 → 视锥外 c.focal[0] = c.pos[0] + 1000.0; // 视线朝 +X体在身后 → 视锥外
src.updateView(c, vol); src.updateView(c, vol);
// empty 选区 → 不提交目标;从未就绪 → currentImages 恒空(短轮询确认无意外结果)。
for (int i = 0; i < 20; ++i) {
EXPECT_TRUE(src.currentImages().empty()); EXPECT_TRUE(src.currentImages().empty());
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
EXPECT_EQ(src.sliceSource(), nullptr); EXPECT_EQ(src.sliceSource(), nullptr);
} }
@ -288,8 +314,125 @@ TEST(ViewAdaptiveVolumeSource, SingleLevelStore) {
const CameraView c = lookFromX(src.meta(), 500.0); const CameraView c = lookFromX(src.meta(), 500.0);
src.updateView(c, vol); src.updateView(c, vol);
auto imgs = src.currentImages(); auto img = pollReady(src);
ASSERT_EQ(imgs.size(), 1u); ASSERT_NE(img.Get(), nullptr);
EXPECT_EQ(src.lastLevel(), 0); EXPECT_EQ(src.lastLevel(), 0);
EXPECT_EQ(imgs[0]->GetScalarType(), VTK_SHORT); EXPECT_EQ(img->GetScalarType(), VTK_SHORT);
}
// ── C3-2 异步updateView 立即返回 + 最终就绪内容 == reorganizeRegion 同步结果 ──
TEST(ViewAdaptiveVolumeSource, AsyncUpdateEventuallyReady) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_async").string();
makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 3);
render::ViewAdaptiveVolumeSource src(dir, 1.0);
const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0);
const CameraView far = lookFromX(src.meta(), 8000.0);
src.updateView(far, vol); // 立即返回(不阻塞、不在主线程重组)
auto img = pollReady(src);
ASSERT_NE(img.Get(), nullptr);
int d[3];
img->GetDimensions(d);
EXPECT_LE(d[0], 16384);
EXPECT_LE(d[1], 16384);
EXPECT_LE(d[2], 16384);
EXPECT_EQ(img->GetScalarType(), VTK_SHORT);
// 最终就绪内容 == 同步 reorganizeRegion(同 target) 结果(位置/几何一致)。
// 用 selectLod 复算同一 target与异步源选的同 level/区间。
const render::LodSelection sel = render::selectLod(vol, far, 16384);
ASSERT_FALSE(sel.empty);
EXPECT_EQ(sel.level, src.lastLevel());
render::RegionTarget t{};
t.level = sel.level;
t.bx0 = sel.bx0; t.bx1 = sel.bx1;
t.by0 = sel.by0; t.by1 = sel.by1;
t.bz0 = sel.bz0; t.bz1 = sel.bz1;
t.exagg = 1.0;
data::ChunkedVolumeStore store(dir);
auto sync = render::reorganizeRegion(store, t, 16384);
ASSERT_NE(sync.Get(), nullptr);
int ds[3];
sync->GetDimensions(ds);
EXPECT_EQ(d[0], ds[0]);
EXPECT_EQ(d[1], ds[1]);
EXPECT_EQ(d[2], ds[2]);
double oa[3], os[3], sa[3], ss[3];
img->GetOrigin(oa);
img->GetSpacing(sa);
sync->GetOrigin(os);
sync->GetSpacing(ss);
for (int i = 0; i < 3; ++i) {
EXPECT_DOUBLE_EQ(oa[i], os[i]);
EXPECT_DOUBLE_EQ(sa[i], ss[i]);
}
auto* aAsync = vtkShortArray::SafeDownCast(img->GetPointData()->GetScalars());
auto* aSync = vtkShortArray::SafeDownCast(sync->GetPointData()->GetScalars());
ASSERT_NE(aAsync, nullptr);
ASSERT_NE(aSync, nullptr);
ASSERT_EQ(aAsync->GetNumberOfTuples(), aSync->GetNumberOfTuples());
// 抽查若干体素逐一相等(含端点/中点)。
const vtkIdType n = aAsync->GetNumberOfTuples();
for (vtkIdType id : {vtkIdType(0), n / 3, n / 2, n - 1}) {
if (id < 0 || id >= n) continue;
EXPECT_EQ(aAsync->GetValue(id), aSync->GetValue(id)) << "id=" << id;
}
}
// ── C3-2 非阻塞updateView 本身耗时极短(不含重组)即便较大 store ─────────────
TEST(ViewAdaptiveVolumeSource, UpdateDoesNotBlock) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_noblock").string();
// 较大 store同步重组该粗层整卷会有明显耗时updateView 只提交目标应近 0。
makePyramidStore(dir, 512, 256, 192, 0, 0, 0, 0.5, 0.5, 0.2, 64, 3);
render::ViewAdaptiveVolumeSource src(dir, 1.0);
const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0);
const CameraView far = lookFromX(src.meta(), 20000.0);
const auto t0 = std::chrono::steady_clock::now();
src.updateView(far, vol); // 应立即返回(仅 selectLod + requestTarget
const auto t1 = std::chrono::steady_clock::now();
const double ms =
std::chrono::duration<double, std::milli>(t1 - t0).count();
EXPECT_LT(ms, 50.0) << "updateView 阻塞了?耗时 " << ms << "ms";
// 仍能最终就绪(后台重组完成)。
auto img = pollReady(src, 5000, 2);
ASSERT_NE(img.Get(), nullptr);
}
// ── C3-2 维度取自 store.dims单一真源奇数维 store 验重组 dims == store.dims ──
// 全卷请求某粗 level重组单图各轴 == store.dims(level)(而非自算公式)。奇数维
// 多级降采样下store.dims 是唯一权威;本测试钉死「重组维度跟随 store.dims」。
TEST(ViewAdaptiveVolumeSource, UsesStoreDimsNotSelfComputed) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_storedims").string();
// 奇数维度(含多次 ceil 降采样199×83×613 级金字塔。
makePyramidStore(dir, 199, 83, 61, 0, 0, 0, 1.0, 1.0, 1.0, 64, 3);
render::ViewAdaptiveVolumeSource src(dir, 1.0);
const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0);
// 极远 → 选最粗层、区间≈全卷,重组整卷该层 → 各轴应正好 == store.dims(level)。
const CameraView far = lookFromX(src.meta(), 1.0e7);
src.updateView(far, vol);
auto img = pollReady(src);
ASSERT_NE(img.Get(), nullptr);
const int level = src.lastLevel();
ASSERT_GT(level, 0); // 极远 → 粗层(多级降采样,奇数维有意义)
int sdx = 0, sdy = 0, sdz = 0;
data::ChunkedVolumeStore store(dir);
store.dims(level, sdx, sdy, sdz);
int d[3];
img->GetDimensions(d);
// 全卷区间(各轴 ≤16384store.dims 远小于此)→ 重组维度恒 == store.dims。
EXPECT_EQ(d[0], sdx) << "x 维度未跟随 store.dims";
EXPECT_EQ(d[1], sdy) << "y 维度未跟随 store.dims";
EXPECT_EQ(d[2], sdz) << "z 维度未跟随 store.dims";
} }

View File

@ -12,6 +12,7 @@
// gpr_poc renderB <storeDir> [--frames 120] —— 离屏体绘制/切片 fps 基准 // gpr_poc renderB <storeDir> [--frames 120] —— 离屏体绘制/切片 fps 基准
#include <algorithm> #include <algorithm>
#include <chrono>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <cstdlib> #include <cstdlib>
@ -20,6 +21,7 @@
#include <iostream> #include <iostream>
#include <map> #include <map>
#include <string> #include <string>
#include <thread>
#include <vector> #include <vector>
#include "Probe.hpp" #include "Probe.hpp"
@ -2792,22 +2794,41 @@ struct ViewState {
bool inCb = false; bool inCb = false;
}; };
// 单纹理刷新source->update(cam) 选 LOD + 重组当前视野区域单图,喂单 // C3-2 非阻塞拉取:把最新已就绪单图喂 mapper若有新结果。不阻塞主线程——
// SmartVolumeMapper。返回喂入的块数恒为 1 单纹理;视锥外 → 0 不渲)。 // 后台 builder 没新结果就沿用上一帧(拖动跟手的关键)。返回 1=喂了新图0=无变化。
// 同步刷新 st->lastLevelfps 文本用)。 std::size_t viewPickLatest(ViewState* st) {
std::size_t viewRefreshSingle(ViewState* st) { auto imgs = st->source->currentImages(); // 内部 takeLatest非阻塞
st->source->update(st->cam); if (imgs.empty() || imgs[0] == nullptr) return 0; // 无新结果:保留上一帧
st->lastLevel = st->source->lastLevel(); if (imgs[0] == st->currentImg) return 0; // 同一张:无需重喂
auto imgs = st->source->currentImages();
if (imgs.empty() || imgs[0] == nullptr) {
return 0; // 视锥外:不更新输入(保留上一帧),由调用方决定是否渲。
}
st->currentImg = imgs[0]; st->currentImg = imgs[0];
st->lastLevel = st->source->lastLevel();
st->mapper->SetInputData(st->currentImg); st->mapper->SetInputData(st->currentImg);
st->mapper->Update(); st->mapper->Update();
return 1; return 1;
} }
// 单纹理刷新C3-2 异步source->update(cam) 只【提交目标】(非阻塞),随后非阻塞
// 拉一次最新就绪。拖动中主线程不被重组卡住——新纹理由后台备好、下一帧/定时器换上。
// 返回当前喂入的块数1=有就绪单图0=尚无就绪/视锥外)。
std::size_t viewRefreshSingle(ViewState* st) {
st->source->update(st->cam); // 提交目标,立即返回
viewPickLatest(st);
return st->currentImg != nullptr ? 1 : 0;
}
// 阻塞式刷新:提交目标后【轮询到就绪】再返回(带超时)。仅用于 preview/smoke/默认
// 取景这类「需要保证拿到一张图」的离屏/初始化场景——交互路径绝不用此(会卡主线程)。
std::size_t viewRefreshBlocking(ViewState* st, int maxTries = 3000,
int sleepMs = 2) {
st->source->update(st->cam); // 提交目标
for (int i = 0; i < maxTries; ++i) {
if (viewPickLatest(st)) return 1;
if (st->currentImg != nullptr) return 1; // 已有上一就绪且无新结果
std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs));
}
return st->currentImg != nullptr ? 1 : 0;
}
// interactor 回调:每次交互(旋转/缩放)结束后重选 LOD + 刷新 fps 文本。 // interactor 回调:每次交互(旋转/缩放)结束后重选 LOD + 刷新 fps 文本。
// //
// fps 修复Task 12d-fix3之前用 frameTimer上次回调到本次的墙钟算 fps // fps 修复Task 12d-fix3之前用 frameTimer上次回调到本次的墙钟算 fps
@ -2842,6 +2863,25 @@ void viewOnInteract(vtkObject*, unsigned long, void* clientData, void*) {
st->inCb = false; st->inCb = false;
} }
// C3-2 拖动跟手核心:交互进行中(旋转/缩放每次相机变化)只【提交目标】(非阻塞),
// 绝不在主线程重组——主线程立刻继续响应输入,画面用上一张已就绪纹理(跟手)。
void viewOnInteracting(vtkObject*, unsigned long, void* clientData, void*) {
auto* st = static_cast<ViewState*>(clientData);
st->source->update(st->cam); // 提交最新视野目标立即返回supersede 旧目标)
}
// C3-2 定时器:周期性非阻塞拉取后台已就绪的新纹理换上 → 拖动中/松手后新 LOD 备好
// 即自然显示,主线程从不被重组卡住。无新结果则什么也不做(不重渲、不抖)。
void viewOnTimer(vtkObject* caller, unsigned long, void* clientData, void*) {
auto* st = static_cast<ViewState*>(clientData);
if (st->inCb) return; // 与 fps 探针回调互斥,避免重入
if (viewPickLatest(st)) {
st->ren->ResetCameraClippingRange();
st->rw->Render(); // 仅在确有新纹理时重渲
}
(void)caller;
}
// 默认取景宽度:沿测线取约 256 道(=4 brick 列×64)的一段作首帧局部段。整线横截面 // 默认取景宽度:沿测线取约 256 道(=4 brick 列×64)的一段作首帧局部段。整线横截面
// 相对长度 1:34框整卷只会看到一条隐形细带框这个局部段层状结构才充满视野 // 相对长度 1:34框整卷只会看到一条隐形细带框这个局部段层状结构才充满视野
// (用户可再滚轮拉远看整体——细带是物理真实,拉近看细节)。段越宽 X 越细长、截面 // (用户可再滚轮拉远看整体——细带是物理真实,拉近看细节)。段越宽 X 越细长、截面
@ -2891,8 +2931,8 @@ std::size_t viewSetupDefaultFrame(ViewState* st, vtkRenderer* ren) {
st->cam->SetViewUp(0, 0, 1); st->cam->SetViewUp(0, 0, 1);
ren->ResetCameraClippingRange(); ren->ResetCameraClippingRange();
// 源选层选区 + 重组单图喂 mapper。 // 源选层选区 + 重组单图喂 mapper。初始化场景需保证拿到首图 → 阻塞轮询到就绪。
const std::size_t blocks = viewRefreshSingle(st); const std::size_t blocks = viewRefreshBlocking(st);
// 框住局部段:用无参 ResetCamera按 actor 的【已 SetScale(1,exagg,exagg)】缩放 // 框住局部段:用无参 ResetCamera按 actor 的【已 SetScale(1,exagg,exagg)】缩放
// 后包围盒框),相机角度沿用能看出结构的 Elevation/Azimuth再 Zoom 拉近填满画面。 // 后包围盒框),相机角度沿用能看出结构的 Elevation/Azimuth再 Zoom 拉近填满画面。
@ -3162,12 +3202,12 @@ int cmdView(int argc, char** argv) {
std::size_t warm = viewSetupDefaultFrame(&st, ren); std::size_t warm = viewSetupDefaultFrame(&st, ren);
rw->Render(); rw->Render();
// 拉近预览:在默认取景基础上拉近相机,再走 viewRefreshSingle与真窗口缩放后 // 拉近预览:在默认取景基础上拉近相机,再走阻塞刷新(与真窗口缩放后完全相同的
// 完全相同的单纹理路径,level0 局部子区域),验证「拉近后」单图非空、完整。 // 单纹理选区路径level0 局部子区域),轮询到就绪验证「拉近后」单图非空、完整。
if (nearPreview) { if (nearPreview) {
st.cam->Dolly(2.5); // 拉近 st.cam->Dolly(2.5); // 拉近
ren->ResetCameraClippingRange(); ren->ResetCameraClippingRange();
warm = viewRefreshSingle(&st); warm = viewRefreshBlocking(&st);
rw->Render(); rw->Render();
} }
@ -3250,12 +3290,12 @@ int cmdView(int argc, char** argv) {
const int lvlNear = st.lastLevel; const int lvlNear = st.lastLevel;
st.cam->Dolly(0.02); // 大幅拉远 → 期望切到粗 LOD整卷粗层单纹理 st.cam->Dolly(0.02); // 大幅拉远 → 期望切到粗 LOD整卷粗层单纹理
ren->ResetCameraClippingRange(); ren->ResetCameraClippingRange();
const std::size_t blocksFar = viewRefreshSingle(&st); const std::size_t blocksFar = viewRefreshBlocking(&st);
const int lvlFar = st.lastLevel; const int lvlFar = st.lastLevel;
rw->Render(); rw->Render();
st.cam->Dolly(50.0); // 拉近回来 → 期望切回细 LODlevel0 局部子区域) st.cam->Dolly(50.0); // 拉近回来 → 期望切回细 LODlevel0 局部子区域)
ren->ResetCameraClippingRange(); ren->ResetCameraClippingRange();
viewRefreshSingle(&st); viewRefreshBlocking(&st);
const int lvlNear2 = st.lastLevel; const int lvlNear2 = st.lastLevel;
rw->Render(); rw->Render();
const vtkIdType nb2 = countNonBlackPixels(rw.Get(), winW, winH); const vtkIdType nb2 = countNonBlackPixels(rw.Get(), winW, winH);
@ -3290,13 +3330,26 @@ int cmdView(int argc, char** argv) {
vtkNew<vtkCallbackCommand> cb; vtkNew<vtkCallbackCommand> cb;
cb->SetCallback(viewOnInteract); cb->SetCallback(viewOnInteract);
cb->SetClientData(&st); cb->SetClientData(&st);
// EndInteraction旋转/缩放松手后重选 LOD + 刷 fps仅松手触发一次不自激 // EndInteraction旋转/缩放松手后提交新目标 + 刷 fps仅松手触发一次不自激
// 注意:绝不可在 rw 的 EndEvent 上注册——回调内部 Render() 会再触发 EndEvent // 注意:绝不可在 rw 的 EndEvent 上注册——回调内部 Render() 会再触发 EndEvent
// 形成无限递归重渲窗口卡死、fps≈0。fps 文本在松手时刷新即可。 // 形成无限递归重渲窗口卡死、fps≈0。fps 文本在松手时刷新即可。
iren->AddObserver(vtkCommand::EndInteractionEvent, cb); iren->AddObserver(vtkCommand::EndInteractionEvent, cb);
// C3-2拖动进行中持续提交目标非阻塞主线程不被重组卡住 → 跟手。
vtkNew<vtkCallbackCommand> cbInteract;
cbInteract->SetCallback(viewOnInteracting);
cbInteract->SetClientData(&st);
iren->AddObserver(vtkCommand::InteractionEvent, cbInteract);
// C3-2周期定时器非阻塞拉取后台已就绪纹理换上新 LOD 备好即显示,拖动不卡)。
vtkNew<vtkCallbackCommand> cbTimer;
cbTimer->SetCallback(viewOnTimer);
cbTimer->SetClientData(&st);
iren->AddObserver(vtkCommand::TimerEvent, cbTimer);
std::cout << "[view] 打开真窗口。左键旋转 / 滚轮缩放(切 LOD) / q 退出。\n"; std::cout << "[view] 打开真窗口。左键旋转 / 滚轮缩放(切 LOD) / q 退出。\n";
iren->Initialize(); iren->Initialize();
iren->CreateRepeatingTimer(33); // ~30Hz 拉取后台就绪纹理(不阻塞主线程)
rw->Render(); rw->Render();
iren->Start(); iren->Start();