feat/vtk-3d-view #7
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)。
|
||||
|
|
|
|||
|
|
@ -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 重组改委托公共重组核 reorganizeRegion(DRY,C3 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
|
||||
|
|
|
|||
|
|
@ -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 渲染循环单线程)。唯一的跨
|
||||
// 线程边界在内含的 AsyncRegionBuilder:worker 线程独占 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 填 VolumeView(spacing 已含 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_;
|
||||
|
||||
// 最新已就绪单图 + 其 level(mutable:currentImages/sliceSource const 内懒取最新)。
|
||||
mutable vtkSmartPointer<vtkImageData> current_; // 空指针 = 从未就绪
|
||||
mutable int lastLevel_ = 0;
|
||||
};
|
||||
|
||||
} // namespace geopro::render
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
// spacing:x 不夸张,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×61,3 级金字塔。
|
||||
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);
|
||||
// 全卷区间(各轴 ≤16384,store.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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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->lastLevel(fps 文本用)。
|
||||
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); // 拉近回来 → 期望切回细 LOD(level0 局部子区域)
|
||||
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();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue