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
8 changed files with 321 additions and 74 deletions
Showing only changes of commit cec41e3539 - Show all commits

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

View File

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

View File

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

View File

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

View File

@ -1,14 +1,19 @@
#include "source/ViewAdaptiveVolumeSource.hpp"
#include <vtkCamera.h>
#include <utility>
#include "source/RegionReorganizer.hpp"
#include <vtkCamera.h>
namespace geopro::render {
ViewAdaptiveVolumeSource::ViewAdaptiveVolumeSource(const std::string& storeDir,
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 v{};
@ -29,10 +34,7 @@ VolumeView ViewAdaptiveVolumeSource::volumeView() const {
}
void ViewAdaptiveVolumeSource::update(vtkCamera* cam) {
if (cam == nullptr) {
current_ = nullptr;
return;
}
if (cam == nullptr) return; // 无相机:不提交目标(保留上一就绪结果)
CameraView c{};
cam->GetPosition(c.pos);
cam->GetFocalPoint(c.focal);
@ -47,12 +49,12 @@ void ViewAdaptiveVolumeSource::updateView(const CameraView& cam,
const VolumeView& vol) {
const LodSelection sel = selectLod(vol, cam, maxTextureDim_);
if (sel.empty) {
current_ = nullptr;
// 空选区(体在视锥外):不提交目标,保留上一就绪结果(拖动出界时不闪空)。
return;
}
lastLevel_ = sel.level;
// C2 重组改委托公共重组核 reorganizeRegionDRYC3 AsyncRegionBuilder 同源)。
// C3-2只【提交目标】给后台 builder不在主线程重组不阻塞。维度/裁剪由
// reorganizeRegion 内按 store.dims 处理;本侧只传 brick 区间 + level + exagg。
RegionTarget target{};
target.level = sel.level;
target.bx0 = sel.bx0;
@ -62,13 +64,29 @@ void ViewAdaptiveVolumeSource::updateView(const CameraView& cam,
target.bz0 = sel.bz0;
target.bz1 = sel.bz1;
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>>
ViewAdaptiveVolumeSource::currentImages() const {
pullLatest();
if (current_ == nullptr) return {};
return {current_};
}
vtkImageData* ViewAdaptiveVolumeSource::sliceSource() const {
pullLatest();
return current_.Get();
}
} // namespace geopro::render

View File

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

View File

@ -1,23 +1,31 @@
// ViewAdaptiveVolumeSource(C2) headless 测试:用 C1 selectLod 选层选区,从分块
// 存储重组当前视野区域为【单张 VTK_SHORT vtkImageData】各轴 ≤16384世界
// ViewAdaptiveVolumeSource(C2→C3-2) headless 测试:用 C1 selectLod 选层选区,
// 当前视野区域【异步】重组为【单张 VTK_SHORT vtkImageData】各轴 ≤16384世界
// origin/spacing 按 level+exagg。核心 updateView(CameraView,VolumeView) 不需真
// vtkCamera/GL 上下文——构造 vtkImageData 不需渲染管线。
//
// C3-2 异步集成updateView 只提交目标不阻塞、不在主线程重组currentImages
// 取最新已就绪(没就绪用上一张)。测试需在 updateView 后【轮询 currentImages
// 直到非空(带超时)】再断言内容。
//
// 验:远观粗层 / 近观细层 / 各轴 ≤16384 / VTK_SHORT / 重组体素与 store 对应
// level+区间位置一致(不错位)/ empty 情形空。
// level+区间位置一致(不错位)/ empty 情形空 / updateView 不阻塞 /
// 维度取自 store.dims单一真源奇数维一致/ 最终就绪内容 == reorganizeRegion。
#include "render/source/ViewAdaptiveVolumeSource.hpp"
#include "core/algo/GprVolumeBuilder.hpp"
#include "data/store/ChunkedVolumeStore.hpp"
#include "lod/ViewAdaptiveLodPolicy.hpp"
#include "render/source/RegionReorganizer.hpp"
#include <vtkImageData.h>
#include <vtkPointData.h>
#include <vtkShortArray.h>
#include <chrono>
#include <cmath>
#include <filesystem>
#include <thread>
#include <gtest/gtest.h>
using namespace geopro;
@ -92,9 +100,21 @@ CameraView lookFromX(const data::StoreMeta& m, double dist, double fovYDeg = 30.
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
// ── 远观:选粗层、单图、各轴 ≤16384、VTK_SHORT ───────────────────────────────
// ── 远观:选粗层、单图、各轴 ≤16384、VTK_SHORT(异步轮询就绪后断言)─────────
TEST(ViewAdaptiveVolumeSource, FarViewCoarseLevelSingleTexture) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_far").string();
@ -108,12 +128,14 @@ TEST(ViewAdaptiveVolumeSource, FarViewCoarseLevelSingleTexture) {
const CameraView far = lookFromX(src.meta(), 8000.0);
src.updateView(far, vol);
auto img = pollReady(src);
ASSERT_NE(img.Get(), nullptr);
// 就绪后 currentImages 仍是单张。
auto imgs = src.currentImages();
ASSERT_EQ(imgs.size(), 1u);
ASSERT_NE(imgs[0].Get(), nullptr);
EXPECT_EQ(imgs[0]->GetScalarType(), VTK_SHORT);
EXPECT_EQ(img->GetScalarType(), VTK_SHORT);
int d[3];
imgs[0]->GetDimensions(d);
img->GetDimensions(d);
EXPECT_LE(d[0], 16384);
EXPECT_LE(d[1], 16384);
EXPECT_LE(d[2], 16384);
@ -134,11 +156,11 @@ TEST(ViewAdaptiveVolumeSource, NearViewFineLevel) {
const CameraView near = lookFromX(src.meta(), 8.0, 20.0, 1080);
src.updateView(near, vol);
auto imgs = src.currentImages();
ASSERT_EQ(imgs.size(), 1u);
auto img = pollReady(src);
ASSERT_NE(img.Get(), nullptr);
EXPECT_EQ(src.lastLevel(), 0); // 近 → 最细
int d[3];
imgs[0]->GetDimensions(d);
img->GetDimensions(d);
EXPECT_LE(d[0], 16384);
EXPECT_LE(d[1], 16384);
EXPECT_LE(d[2], 16384);
@ -157,9 +179,9 @@ TEST(ViewAdaptiveVolumeSource, ReconstructedVoxelsMatchStore) {
const CameraView far = lookFromX(src.meta(), 8000.0);
src.updateView(far, vol);
auto imgs = src.currentImages();
ASSERT_EQ(imgs.size(), 1u);
vtkImageData* img = imgs[0];
auto pimg = pollReady(src);
ASSERT_NE(pimg.Get(), nullptr);
vtkImageData* img = pimg.Get();
const int level = src.lastLevel();
data::ChunkedVolumeStore store(dir);
@ -241,14 +263,14 @@ TEST(ViewAdaptiveVolumeSource, WorldOriginSpacingWithExagg) {
const CameraView far = lookFromX(src.meta(), 8000.0);
src.updateView(far, vol);
auto imgs = src.currentImages();
ASSERT_EQ(imgs.size(), 1u);
auto img = pollReady(src);
ASSERT_NE(img.Get(), nullptr);
const data::StoreMeta& m = src.meta();
const int level = src.lastLevel();
const double sc = static_cast<double>(1 << level);
double sp[3], org[3];
imgs[0]->GetSpacing(sp);
imgs[0]->GetOrigin(org);
img->GetSpacing(sp);
img->GetOrigin(org);
// spacingx 不夸张y/z ×exagg。
EXPECT_DOUBLE_EQ(sp[0], m.spacing[0] * sc);
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]);
}
// ── empty体完全在视锥外 → currentImages 空 ───────────────────────────────
// ── empty体完全在视锥外 → currentImages 空(从未就绪)──────────────────────
TEST(ViewAdaptiveVolumeSource, EmptyWhenBehindCamera) {
const auto dir =
(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体在身后 → 视锥外
src.updateView(c, vol);
// empty 选区 → 不提交目标;从未就绪 → currentImages 恒空(短轮询确认无意外结果)。
for (int i = 0; i < 20; ++i) {
EXPECT_TRUE(src.currentImages().empty());
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
EXPECT_EQ(src.sliceSource(), nullptr);
}
@ -288,8 +314,125 @@ TEST(ViewAdaptiveVolumeSource, SingleLevelStore) {
const CameraView c = lookFromX(src.meta(), 500.0);
src.updateView(c, vol);
auto imgs = src.currentImages();
ASSERT_EQ(imgs.size(), 1u);
auto img = pollReady(src);
ASSERT_NE(img.Get(), nullptr);
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 基准
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdlib>
@ -20,6 +21,7 @@
#include <iostream>
#include <map>
#include <string>
#include <thread>
#include <vector>
#include "Probe.hpp"
@ -2792,22 +2794,41 @@ struct ViewState {
bool inCb = false;
};
// 单纹理刷新source->update(cam) 选 LOD + 重组当前视野区域单图,喂单
// SmartVolumeMapper。返回喂入的块数恒为 1 单纹理;视锥外 → 0 不渲)。
// 同步刷新 st->lastLevelfps 文本用)。
std::size_t viewRefreshSingle(ViewState* st) {
st->source->update(st->cam);
st->lastLevel = st->source->lastLevel();
auto imgs = st->source->currentImages();
if (imgs.empty() || imgs[0] == nullptr) {
return 0; // 视锥外:不更新输入(保留上一帧),由调用方决定是否渲。
}
// C3-2 非阻塞拉取:把最新已就绪单图喂 mapper若有新结果。不阻塞主线程——
// 后台 builder 没新结果就沿用上一帧(拖动跟手的关键)。返回 1=喂了新图0=无变化。
std::size_t viewPickLatest(ViewState* st) {
auto imgs = st->source->currentImages(); // 内部 takeLatest非阻塞
if (imgs.empty() || imgs[0] == nullptr) return 0; // 无新结果:保留上一帧
if (imgs[0] == st->currentImg) return 0; // 同一张:无需重喂
st->currentImg = imgs[0];
st->lastLevel = st->source->lastLevel();
st->mapper->SetInputData(st->currentImg);
st->mapper->Update();
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 文本。
//
// fps 修复Task 12d-fix3之前用 frameTimer上次回调到本次的墙钟算 fps
@ -2842,6 +2863,25 @@ void viewOnInteract(vtkObject*, unsigned long, void* clientData, void*) {
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)的一段作首帧局部段。整线横截面
// 相对长度 1:34框整卷只会看到一条隐形细带框这个局部段层状结构才充满视野
// (用户可再滚轮拉远看整体——细带是物理真实,拉近看细节)。段越宽 X 越细长、截面
@ -2891,8 +2931,8 @@ std::size_t viewSetupDefaultFrame(ViewState* st, vtkRenderer* ren) {
st->cam->SetViewUp(0, 0, 1);
ren->ResetCameraClippingRange();
// 源选层选区 + 重组单图喂 mapper。
const std::size_t blocks = viewRefreshSingle(st);
// 源选层选区 + 重组单图喂 mapper。初始化场景需保证拿到首图 → 阻塞轮询到就绪。
const std::size_t blocks = viewRefreshBlocking(st);
// 框住局部段:用无参 ResetCamera按 actor 的【已 SetScale(1,exagg,exagg)】缩放
// 后包围盒框),相机角度沿用能看出结构的 Elevation/Azimuth再 Zoom 拉近填满画面。
@ -3162,12 +3202,12 @@ int cmdView(int argc, char** argv) {
std::size_t warm = viewSetupDefaultFrame(&st, ren);
rw->Render();
// 拉近预览:在默认取景基础上拉近相机,再走 viewRefreshSingle与真窗口缩放后
// 完全相同的单纹理路径,level0 局部子区域),验证「拉近后」单图非空、完整。
// 拉近预览:在默认取景基础上拉近相机,再走阻塞刷新(与真窗口缩放后完全相同的
// 单纹理选区路径level0 局部子区域),轮询到就绪验证「拉近后」单图非空、完整。
if (nearPreview) {
st.cam->Dolly(2.5); // 拉近
ren->ResetCameraClippingRange();
warm = viewRefreshSingle(&st);
warm = viewRefreshBlocking(&st);
rw->Render();
}
@ -3250,12 +3290,12 @@ int cmdView(int argc, char** argv) {
const int lvlNear = st.lastLevel;
st.cam->Dolly(0.02); // 大幅拉远 → 期望切到粗 LOD整卷粗层单纹理
ren->ResetCameraClippingRange();
const std::size_t blocksFar = viewRefreshSingle(&st);
const std::size_t blocksFar = viewRefreshBlocking(&st);
const int lvlFar = st.lastLevel;
rw->Render();
st.cam->Dolly(50.0); // 拉近回来 → 期望切回细 LODlevel0 局部子区域)
ren->ResetCameraClippingRange();
viewRefreshSingle(&st);
viewRefreshBlocking(&st);
const int lvlNear2 = st.lastLevel;
rw->Render();
const vtkIdType nb2 = countNonBlackPixels(rw.Get(), winW, winH);
@ -3290,13 +3330,26 @@ int cmdView(int argc, char** argv) {
vtkNew<vtkCallbackCommand> cb;
cb->SetCallback(viewOnInteract);
cb->SetClientData(&st);
// EndInteraction旋转/缩放松手后重选 LOD + 刷 fps仅松手触发一次不自激
// EndInteraction旋转/缩放松手后提交新目标 + 刷 fps仅松手触发一次不自激
// 注意:绝不可在 rw 的 EndEvent 上注册——回调内部 Render() 会再触发 EndEvent
// 形成无限递归重渲窗口卡死、fps≈0。fps 文本在松手时刷新即可。
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";
iren->Initialize();
iren->CreateRepeatingTimer(33); // ~30Hz 拉取后台就绪纹理(不阻塞主线程)
rw->Render();
iren->Start();