feat/vtk-3d-view #7
|
|
@ -5,7 +5,7 @@ add_library(geopro_render STATIC
|
|||
interact/SlicePlaneMath.cpp interact/SliceTool.cpp interact/PickInteractorStyle.cpp interact/InteractionManager.cpp interact/AnomalyDrawTool.cpp
|
||||
ground/TileMath.cpp
|
||||
lod/ViewAdaptiveLodPolicy.cpp
|
||||
source/WholeVolumeSource.cpp source/BrickPager.cpp source/OutOfCoreSource.cpp source/ViewAdaptiveVolumeSource.cpp)
|
||||
source/WholeVolumeSource.cpp source/BrickPager.cpp source/OutOfCoreSource.cpp source/ViewAdaptiveVolumeSource.cpp source/RegionReorganizer.cpp source/AsyncRegionBuilder.cpp)
|
||||
target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(geopro_render PUBLIC geopro_core geopro_store ${VTK_LIBRARIES} GDAL::GDAL)
|
||||
target_compile_features(geopro_render PUBLIC cxx_std_17)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
#include "source/AsyncRegionBuilder.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace geopro::render {
|
||||
|
||||
AsyncRegionBuilder::AsyncRegionBuilder(const std::string& storeDir)
|
||||
: store_(storeDir) {
|
||||
worker_ = std::thread([this] { workerLoop(); });
|
||||
}
|
||||
|
||||
AsyncRegionBuilder::~AsyncRegionBuilder() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mutex_);
|
||||
stop_.store(true);
|
||||
}
|
||||
cv_.notify_all();
|
||||
if (worker_.joinable()) worker_.join();
|
||||
}
|
||||
|
||||
void AsyncRegionBuilder::setMaxTextureDim(int dim) {
|
||||
std::lock_guard<std::mutex> lk(mutex_);
|
||||
if (dim > 0) maxTextureDim_ = dim;
|
||||
}
|
||||
|
||||
void AsyncRegionBuilder::requestTarget(const RegionTarget& t) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mutex_);
|
||||
// 与已就绪结果对应的目标相同则不必重建(避免重复劳动);与在建/排队的期望相同
|
||||
// 亦无需扰动。简化:直接覆盖期望并标脏——worker 会以最新期望为准(supersede)。
|
||||
desired_ = t;
|
||||
hasDesired_ = true;
|
||||
}
|
||||
cv_.notify_one();
|
||||
}
|
||||
|
||||
vtkSmartPointer<vtkImageData> AsyncRegionBuilder::takeLatest() {
|
||||
// 非阻塞:仅在锁内做指针移动(不做任何重组/IO/长时操作)。
|
||||
// std::move 把 ready_ 的引用移交给返回值(无 refcount 增减,且全程在锁内/单线程),
|
||||
// ready_ 置空——下次无新结果再调返回 nullptr。
|
||||
std::lock_guard<std::mutex> lk(mutex_);
|
||||
return std::move(ready_);
|
||||
}
|
||||
|
||||
void AsyncRegionBuilder::workerLoop() {
|
||||
std::unique_lock<std::mutex> lk(mutex_);
|
||||
for (;;) {
|
||||
// 等到有期望或停止。
|
||||
cv_.wait(lk, [this] { return stop_.load() || hasDesired_; });
|
||||
if (stop_.load()) return;
|
||||
|
||||
// 取出最新期望(supersede:只建当前最新,丢弃中途旧请求)。
|
||||
const RegionTarget target = desired_;
|
||||
const int maxDim = maxTextureDim_;
|
||||
hasDesired_ = false;
|
||||
building_ = true;
|
||||
|
||||
// 解锁后构建:worker 在自己线程内重组,主线程全程不触碰该局部 image。
|
||||
lk.unlock();
|
||||
vtkSmartPointer<vtkImageData> built =
|
||||
reorganizeRegion(store_, target, maxDim);
|
||||
lk.lock();
|
||||
|
||||
if (stop_.load()) return;
|
||||
|
||||
// 若构建期间又来了更新的期望,则本次结果已过期——丢弃,继续建最新。
|
||||
// built 离开作用域前在锁内释放 refcount(单线程独占)。
|
||||
if (hasDesired_) {
|
||||
building_ = false;
|
||||
built = nullptr; // 锁内释放(单线程独占 refcount)
|
||||
continue;
|
||||
}
|
||||
|
||||
// publish:在锁内把所有权 move 进 ready_(旧 ready_ 若未取走在此处锁内释放)。
|
||||
// 所有 refcount 增减均在锁内/单线程独占完成。
|
||||
ready_ = std::move(built);
|
||||
building_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool AsyncRegionBuilder::hasPending() const {
|
||||
std::lock_guard<std::mutex> lk(mutex_);
|
||||
return hasDesired_ || building_;
|
||||
}
|
||||
|
||||
} // namespace geopro::render
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
#pragma once
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include <vtkImageData.h>
|
||||
#include <vtkSmartPointer.h>
|
||||
|
||||
#include "data/store/ChunkedVolumeStore.hpp"
|
||||
#include "source/RegionReorganizer.hpp"
|
||||
|
||||
namespace geopro::render {
|
||||
|
||||
// C3 并发核心:把「从 store 重组视野区域单纹理」放后台线程,主线程非阻塞取最新
|
||||
// 就绪结果——使拖动/缩放时不被解压+重组卡住。
|
||||
//
|
||||
// 线程安全设计(VTK 引用计数跨线程要小心):
|
||||
// - 后台 worker 在自己线程内调 reorganizeRegion(C2/C3 共用重组核)构建出一个
|
||||
// vtkSmartPointer<vtkImageData>(全程不被主线程触碰)。
|
||||
// - 完成后在 mutex 下把它移交到成员 ready_。主线程在同一 mutex 下取走(移动出)。
|
||||
// 所有 vtkImageData 引用计数的增减都发生在锁内/单线程独占——避免两线程同时碰
|
||||
// 同一对象的 refcount。worker 构建期间主线程看不到它;publish 后所有权经锁交给
|
||||
// 主线程。
|
||||
// - 请求合并/取最新(supersede):worker 在建 A 时若 requestTarget(B),完成 A 后
|
||||
// 去建最新期望(B),不堆积旧请求——主线程高频 update 不致排队爆炸。
|
||||
class AsyncRegionBuilder {
|
||||
public:
|
||||
// 起 worker 线程。storeDir:含金字塔的分块 store。
|
||||
explicit AsyncRegionBuilder(const std::string& storeDir);
|
||||
|
||||
// 停 worker(join)。析构忙时干净 join 不崩、不泄漏、不死锁。
|
||||
~AsyncRegionBuilder();
|
||||
|
||||
AsyncRegionBuilder(const AsyncRegionBuilder&) = delete;
|
||||
AsyncRegionBuilder& operator=(const AsyncRegionBuilder&) = delete;
|
||||
|
||||
// 主线程调:设期望目标。与当前在建/已建不同则唤醒 worker 重建;相同则忽略。
|
||||
void requestTarget(const RegionTarget& t);
|
||||
|
||||
// 主线程调:取最新已就绪 image(非阻塞)。无新结果返回 nullptr(主线程继续用
|
||||
// 上一张)。取走后清空,下次无新结果再调返回 nullptr。
|
||||
// 永不阻塞主线程:仅在锁内做指针移动。
|
||||
vtkSmartPointer<vtkImageData> takeLatest();
|
||||
|
||||
// 是否有在建/排队(供 UI/测试)。
|
||||
bool hasPending() const;
|
||||
|
||||
// 单张 3D 纹理各轴上限(GL_MAX_3D_TEXTURE_SIZE)。须在 requestTarget 前设。
|
||||
void setMaxTextureDim(int dim);
|
||||
|
||||
private:
|
||||
void workerLoop();
|
||||
|
||||
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_; // 已就绪、待主线程取走的最新结果
|
||||
std::atomic<bool> stop_{false}; // 析构置位,唤醒 worker 退出
|
||||
int maxTextureDim_ = 16384;
|
||||
|
||||
std::thread worker_; // 最后声明:构造完上述成员后再起线程
|
||||
};
|
||||
|
||||
} // namespace geopro::render
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
#include "source/RegionReorganizer.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include <vtkNew.h>
|
||||
#include <vtkPointData.h>
|
||||
#include <vtkShortArray.h>
|
||||
|
||||
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) {
|
||||
const geopro::data::StoreMeta& meta = store.meta();
|
||||
const int level = target.level;
|
||||
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);
|
||||
|
||||
// 重组单纹理某轴范围(与 C1 hpp 契约逐字一致):
|
||||
// 起点 = b0*brick;终点 = min(b1*brick, dimL, 起点 + maxTextureDim)。
|
||||
// 即使 brick > maxTextureDim(单块超限),也按体素上限再裁 → 恒 ≤ maxTextureDim。
|
||||
const int gi0 = target.bx0 * brick;
|
||||
const int gj0 = target.by0 * brick;
|
||||
const int gk0 = target.bz0 * brick;
|
||||
const int gi1 = std::min({target.bx1 * brick, dimLx, gi0 + maxTextureDim});
|
||||
const int gj1 = std::min({target.by1 * brick, dimLy, gj0 + maxTextureDim});
|
||||
const int gk1 = std::min({target.bz1 * brick, dimLz, gk0 + maxTextureDim});
|
||||
|
||||
// 区间为空 → 无块可重组。
|
||||
if (gi1 <= gi0 || gj1 <= gj0 || gk1 <= gk0) return nullptr;
|
||||
|
||||
const int outNx = std::max(1, gi1 - gi0);
|
||||
const int outNy = std::max(1, gj1 - gj0);
|
||||
const int outNz = std::max(1, gk1 - gk0);
|
||||
|
||||
// 世界几何(按 level + exagg):
|
||||
// spacing = meta.spacing × 2^level(y/z 再 × exagg);
|
||||
// origin = meta.origin + 区间起始体素 × spacing。
|
||||
const double sc = static_cast<double>(std::int64_t(1) << level); // 2^level
|
||||
const double sp[3] = {meta.spacing[0] * sc, meta.spacing[1] * sc * exagg,
|
||||
meta.spacing[2] * sc * exagg};
|
||||
const double org[3] = {meta.origin[0] + static_cast<double>(gi0) * sp[0],
|
||||
meta.origin[1] + static_cast<double>(gj0) * sp[1],
|
||||
meta.origin[2] + static_cast<double>(gk0) * sp[2]};
|
||||
|
||||
auto img = vtkSmartPointer<vtkImageData>::New();
|
||||
img->SetDimensions(outNx, outNy, outNz);
|
||||
img->SetOrigin(org[0], org[1], org[2]);
|
||||
img->SetSpacing(sp[0], sp[1], sp[2]);
|
||||
|
||||
vtkNew<vtkShortArray> arr;
|
||||
arr->SetName("v");
|
||||
const vtkIdType total = static_cast<vtkIdType>(outNx) * outNy * outNz;
|
||||
arr->SetNumberOfTuples(total);
|
||||
|
||||
// 遍历区间覆盖的 level 块,按块内 (i 最快、k 最慢) 布局把每体素写入子体对应位置。
|
||||
// 块内布局与 readBrick / vtkImageData 点序一致。
|
||||
const int bx0 = gi0 / brick, bx1 = (gi1 + brick - 1) / brick;
|
||||
const int by0 = gj0 / brick, by1 = (gj1 + brick - 1) / brick;
|
||||
const int bz0 = gk0 / brick, bz1 = (gk1 + brick - 1) / brick;
|
||||
|
||||
for (int bz = bz0; bz < bz1; ++bz) {
|
||||
const int k0 = bz * brick;
|
||||
const int bd = std::min(brick, dimLz - k0);
|
||||
for (int by = by0; by < by1; ++by) {
|
||||
const int j0 = by * brick;
|
||||
const int bh = std::min(brick, dimLy - j0);
|
||||
for (int bx = bx0; bx < bx1; ++bx) {
|
||||
const int i0 = bx * brick;
|
||||
const int bw = std::min(brick, dimLx - i0);
|
||||
const std::vector<std::int16_t> raw = store.readBrick(level, bx, by, bz);
|
||||
|
||||
// 块内体素全局索引 (i0+ii, j0+jj, k0+kk);只写落在子体区间 [g*0,g*1) 内的。
|
||||
for (int kk = 0; kk < bd; ++kk) {
|
||||
const int gk = k0 + kk;
|
||||
if (gk < gk0 || gk >= gk1) continue;
|
||||
const int lk = gk - gk0;
|
||||
for (int jj = 0; jj < bh; ++jj) {
|
||||
const int gj = j0 + jj;
|
||||
if (gj < gj0 || gj >= gj1) continue;
|
||||
const int lj = gj - gj0;
|
||||
// 该 (kk,jj) 行在 raw 内的起始偏移(i 最快)。
|
||||
const std::size_t rowBase =
|
||||
(static_cast<std::size_t>(kk) * bh + jj) * bw;
|
||||
for (int ii = 0; ii < bw; ++ii) {
|
||||
const int gi = i0 + ii;
|
||||
if (gi < gi0 || gi >= gi1) continue;
|
||||
const int li = gi - gi0;
|
||||
const vtkIdType id =
|
||||
(static_cast<vtkIdType>(lk) * outNy + lj) * outNx + li;
|
||||
arr->SetValue(id, raw[rowBase + ii]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img->GetPointData()->SetScalars(arr);
|
||||
return img;
|
||||
}
|
||||
|
||||
} // namespace geopro::render
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
#pragma once
|
||||
#include <vtkImageData.h>
|
||||
#include <vtkSmartPointer.h>
|
||||
|
||||
#include "data/store/ChunkedVolumeStore.hpp"
|
||||
|
||||
namespace geopro::render {
|
||||
|
||||
// 要重组的目标区域(C1 selectLod 选出的 LOD level + 视野内 brick 区间 + 垂向夸张)。
|
||||
// level:LOD 层(0=最细)。
|
||||
// [bx0,bx1) [by0,by1) [bz0,bz1):该 level 要重组的 brick 区间(半开)。
|
||||
// exagg:垂向夸张(烘焙进 y/z 的 spacing/origin)。
|
||||
// C2 ViewAdaptiveVolumeSource 与 C3 AsyncRegionBuilder 共用此结构(单一真源)。
|
||||
struct RegionTarget {
|
||||
int level = 0;
|
||||
int bx0 = 0, bx1 = 0;
|
||||
int by0 = 0, by1 = 0;
|
||||
int bz0 = 0, bz1 = 0;
|
||||
double exagg = 1.0;
|
||||
|
||||
bool operator==(const RegionTarget& o) const {
|
||||
return level == o.level && bx0 == o.bx0 && bx1 == o.bx1 && by0 == o.by0 &&
|
||||
by1 == o.by1 && bz0 == o.bz0 && bz1 == o.bz1 && exagg == o.exagg;
|
||||
}
|
||||
bool operator!=(const RegionTarget& o) const { return !(*this == o); }
|
||||
};
|
||||
|
||||
// 重组核(公共纯函数,单一真源;headless、不需 GL 上下文)。
|
||||
//
|
||||
// 把 store 中 [target] 指定的 level + brick 区间重组为【单张 VTK_SHORT
|
||||
// vtkImageData】,带世界 origin/spacing(按 level + exagg):
|
||||
// spacing = meta.spacing × 2^level(y/z 再 × exagg);
|
||||
// origin = meta.origin + 区间起始体素 × spacing。
|
||||
//
|
||||
// 区间裁剪(与 C1 hpp 契约逐字一致):各轴起点 = b0*brick;终点 =
|
||||
// min(b1*brick, dimL, 起点 + maxTextureDim)——即使 brick > maxTextureDim
|
||||
// 也按体素上限再裁,恒 ≤ maxTextureDim。
|
||||
//
|
||||
// 区间为空(任一轴 b1<=b0)→ 返回 nullptr。
|
||||
vtkSmartPointer<vtkImageData> reorganizeRegion(
|
||||
const geopro::data::ChunkedVolumeStore& store, const RegionTarget& target,
|
||||
int maxTextureDim = 16384);
|
||||
|
||||
} // namespace geopro::render
|
||||
|
|
@ -1,26 +1,11 @@
|
|||
#include "source/ViewAdaptiveVolumeSource.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
#include <vtkCamera.h>
|
||||
#include <vtkNew.h>
|
||||
#include <vtkPointData.h>
|
||||
#include <vtkShortArray.h>
|
||||
|
||||
#include "source/RegionReorganizer.hpp"
|
||||
|
||||
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
|
||||
|
||||
ViewAdaptiveVolumeSource::ViewAdaptiveVolumeSource(const std::string& storeDir,
|
||||
double exagg)
|
||||
: store_(storeDir), meta_(store_.meta()), exagg_(exagg > 0 ? exagg : 1.0) {}
|
||||
|
|
@ -65,93 +50,19 @@ void ViewAdaptiveVolumeSource::updateView(const CameraView& cam,
|
|||
current_ = nullptr;
|
||||
return;
|
||||
}
|
||||
const int level = sel.level;
|
||||
lastLevel_ = level;
|
||||
lastLevel_ = sel.level;
|
||||
|
||||
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);
|
||||
|
||||
// 重组单纹理某轴范围(与 C1 hpp 契约逐字一致):
|
||||
// 起点 = b0*brick;终点 = min(b1*brick, dimL, 起点 + maxTextureDim)。
|
||||
// 即使 brick > maxTextureDim(单块超限),也按体素上限再裁 → 恒 ≤ maxTextureDim。
|
||||
const int gi0 = sel.bx0 * brick;
|
||||
const int gj0 = sel.by0 * brick;
|
||||
const int gk0 = sel.bz0 * brick;
|
||||
const int gi1 = std::min({sel.bx1 * brick, dimLx, gi0 + maxTextureDim_});
|
||||
const int gj1 = std::min({sel.by1 * brick, dimLy, gj0 + maxTextureDim_});
|
||||
const int gk1 = std::min({sel.bz1 * brick, dimLz, gk0 + maxTextureDim_});
|
||||
|
||||
const int outNx = std::max(1, gi1 - gi0);
|
||||
const int outNy = std::max(1, gj1 - gj0);
|
||||
const int outNz = std::max(1, gk1 - gk0);
|
||||
|
||||
// 世界几何(按 level + exagg):
|
||||
// spacing = meta.spacing × 2^level(y/z 再 × exagg);
|
||||
// origin = meta.origin + 区间起始体素 × spacing。
|
||||
const double sc = static_cast<double>(std::int64_t(1) << level); // 2^level
|
||||
const double sp[3] = {meta_.spacing[0] * sc, meta_.spacing[1] * sc * exagg_,
|
||||
meta_.spacing[2] * sc * exagg_};
|
||||
const double org[3] = {meta_.origin[0] + static_cast<double>(gi0) * sp[0],
|
||||
meta_.origin[1] + static_cast<double>(gj0) * sp[1],
|
||||
meta_.origin[2] + static_cast<double>(gk0) * sp[2]};
|
||||
|
||||
auto img = vtkSmartPointer<vtkImageData>::New();
|
||||
img->SetDimensions(outNx, outNy, outNz);
|
||||
img->SetOrigin(org[0], org[1], org[2]);
|
||||
img->SetSpacing(sp[0], sp[1], sp[2]);
|
||||
|
||||
vtkNew<vtkShortArray> arr;
|
||||
arr->SetName("v");
|
||||
const vtkIdType total = static_cast<vtkIdType>(outNx) * outNy * outNz;
|
||||
arr->SetNumberOfTuples(total);
|
||||
|
||||
// 遍历区间覆盖的 level 块,按块内 (i 最快、k 最慢) 布局把每体素写入子体对应位置。
|
||||
// 块内布局与 readBrick / vtkImageData 点序一致。
|
||||
const int bx0 = gi0 / brick, bx1 = (gi1 + brick - 1) / brick;
|
||||
const int by0 = gj0 / brick, by1 = (gj1 + brick - 1) / brick;
|
||||
const int bz0 = gk0 / brick, bz1 = (gk1 + brick - 1) / brick;
|
||||
|
||||
for (int bz = bz0; bz < bz1; ++bz) {
|
||||
const int k0 = bz * brick;
|
||||
const int bd = std::min(brick, dimLz - k0);
|
||||
for (int by = by0; by < by1; ++by) {
|
||||
const int j0 = by * brick;
|
||||
const int bh = std::min(brick, dimLy - j0);
|
||||
for (int bx = bx0; bx < bx1; ++bx) {
|
||||
const int i0 = bx * brick;
|
||||
const int bw = std::min(brick, dimLx - i0);
|
||||
const std::vector<std::int16_t> raw = store_.readBrick(level, bx, by, bz);
|
||||
|
||||
// 块内体素全局索引 (i0+ii, j0+jj, k0+kk);只写落在子体区间 [g*0,g*1) 内的。
|
||||
for (int kk = 0; kk < bd; ++kk) {
|
||||
const int gk = k0 + kk;
|
||||
if (gk < gk0 || gk >= gk1) continue;
|
||||
const int lk = gk - gk0;
|
||||
for (int jj = 0; jj < bh; ++jj) {
|
||||
const int gj = j0 + jj;
|
||||
if (gj < gj0 || gj >= gj1) continue;
|
||||
const int lj = gj - gj0;
|
||||
// 该 (kk,jj) 行在 raw 内的起始偏移(i 最快)。
|
||||
const std::size_t rowBase =
|
||||
(static_cast<std::size_t>(kk) * bh + jj) * bw;
|
||||
for (int ii = 0; ii < bw; ++ii) {
|
||||
const int gi = i0 + ii;
|
||||
if (gi < gi0 || gi >= gi1) continue;
|
||||
const int li = gi - gi0;
|
||||
const vtkIdType id =
|
||||
(static_cast<vtkIdType>(lk) * outNy + lj) * outNx + li;
|
||||
arr->SetValue(id, raw[rowBase + ii]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img->GetPointData()->SetScalars(arr);
|
||||
current_ = img;
|
||||
// C2 重组改委托公共重组核 reorganizeRegion(DRY,C3 AsyncRegionBuilder 同源)。
|
||||
RegionTarget target{};
|
||||
target.level = sel.level;
|
||||
target.bx0 = sel.bx0;
|
||||
target.bx1 = sel.bx1;
|
||||
target.by0 = sel.by0;
|
||||
target.by1 = sel.by1;
|
||||
target.bz0 = sel.bz0;
|
||||
target.bz1 = sel.bz1;
|
||||
target.exagg = exagg_;
|
||||
current_ = reorganizeRegion(store_, target, maxTextureDim_);
|
||||
}
|
||||
|
||||
std::vector<vtkSmartPointer<vtkImageData>>
|
||||
|
|
|
|||
|
|
@ -128,6 +128,8 @@ target_sources(geopro_tests PRIVATE render/test_outofcore_source.cpp)
|
|||
target_sources(geopro_tests PRIVATE render/test_view_adaptive_lod.cpp)
|
||||
# ViewAdaptiveVolumeSource(C2):用 C1 selectLod 选层选区→从分块存储重组当前视野区域为单张 VTK_SHORT image(各轴≤16384/世界 origin/spacing 按 level+exagg/体素位置与 store 一致)。headless 不需 GPU。
|
||||
target_sources(geopro_tests PRIVATE render/test_view_adaptive_source.cpp)
|
||||
# AsyncRegionBuilder(C3-1):后台 worker 调公共重组核 reorganizeRegion 重组视野区域→单图,主线程非阻塞 takeLatest 取最新就绪(supersede/析构干净 join/并发不崩/非阻塞)。真线程不需 GPU。
|
||||
target_sources(geopro_tests PRIVATE render/test_async_region_builder.cpp)
|
||||
target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES})
|
||||
vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES})
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,216 @@
|
|||
// AsyncRegionBuilder(C3) headless 测试(真线程,无需 GPU):
|
||||
// 后台 worker 调公共重组核 reorganizeRegion 从分块 store 重组 RegionTarget→单张
|
||||
// VTK_SHORT image;主线程非阻塞 takeLatest 取最新就绪。
|
||||
//
|
||||
// 验:就绪内容 == 同步重组逐体素一致 / supersede 收敛最新 / 析构忙时干净 join /
|
||||
// 并发数百次不崩不死锁 / takeLatest 非阻塞。
|
||||
|
||||
#include "render/source/AsyncRegionBuilder.hpp"
|
||||
#include "render/source/RegionReorganizer.hpp"
|
||||
|
||||
#include "core/algo/GprVolumeBuilder.hpp"
|
||||
#include "data/store/ChunkedVolumeStore.hpp"
|
||||
|
||||
#include <vtkImageData.h>
|
||||
#include <vtkPointData.h>
|
||||
#include <vtkShortArray.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <thread>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace geopro;
|
||||
using geopro::render::AsyncRegionBuilder;
|
||||
using geopro::render::RegionTarget;
|
||||
using geopro::render::reorganizeRegion;
|
||||
|
||||
namespace {
|
||||
|
||||
// 造一个含金字塔的 store:值 = 全局 (i+j+k)%1000(便于校验块定位),非 64 整除
|
||||
// 维度以含边缘块。返回 store 目录。与 C2 测试同口径。
|
||||
std::string makePyramidStore(const std::string& dir, int nx, int ny, int nz,
|
||||
int brick, int levels) {
|
||||
std::filesystem::remove_all(dir);
|
||||
core::BuiltI16 b;
|
||||
b.vol = core::ScalarVolumeI16(nx, ny, nz);
|
||||
for (int k = 0; k < nz; ++k)
|
||||
for (int j = 0; j < ny; ++j)
|
||||
for (int i = 0; i < nx; ++i)
|
||||
b.vol.at(i, j, k) = static_cast<short>((i + j + k) % 1000);
|
||||
b.quant = {1.0, 0.0};
|
||||
b.origin = {{1, 2, 3}};
|
||||
b.spacing = {{0.5, 0.5, 0.2}};
|
||||
b.vminPhys = 0;
|
||||
b.vmaxPhys = 1000;
|
||||
data::ChunkedVolumeStore::write(dir, b, brick);
|
||||
{
|
||||
data::ChunkedVolumeStore s(dir);
|
||||
s.buildPyramid(levels);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
void sleepMs(int ms) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
|
||||
}
|
||||
|
||||
// 轮询等就绪(有超时上限,避免卡死)。
|
||||
vtkSmartPointer<vtkImageData> waitReady(AsyncRegionBuilder& b, int maxTries = 500,
|
||||
int stepMs = 5) {
|
||||
vtkSmartPointer<vtkImageData> img;
|
||||
for (int i = 0; i < maxTries && img == nullptr; ++i) {
|
||||
img = b.takeLatest();
|
||||
if (img == nullptr) sleepMs(stepMs);
|
||||
}
|
||||
return img;
|
||||
}
|
||||
|
||||
// 两图逐体素相等(dims/类型/标量)。
|
||||
bool imagesEqual(vtkImageData* a, vtkImageData* b) {
|
||||
if (a == nullptr || b == nullptr) return false;
|
||||
int da[3], db[3];
|
||||
a->GetDimensions(da);
|
||||
b->GetDimensions(db);
|
||||
if (da[0] != db[0] || da[1] != db[1] || da[2] != db[2]) return false;
|
||||
auto* aa = vtkShortArray::SafeDownCast(a->GetPointData()->GetScalars());
|
||||
auto* ba = vtkShortArray::SafeDownCast(b->GetPointData()->GetScalars());
|
||||
if (aa == nullptr || ba == nullptr) return false;
|
||||
const vtkIdType n = aa->GetNumberOfTuples();
|
||||
if (n != ba->GetNumberOfTuples()) return false;
|
||||
for (vtkIdType i = 0; i < n; ++i)
|
||||
if (aa->GetValue(i) != ba->GetValue(i)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
RegionTarget makeTarget(int level, int bx0, int bx1, int by0, int by1, int bz0,
|
||||
int bz1, double exagg = 1.0) {
|
||||
RegionTarget t{};
|
||||
t.level = level;
|
||||
t.bx0 = bx0;
|
||||
t.bx1 = bx1;
|
||||
t.by0 = by0;
|
||||
t.by1 = by1;
|
||||
t.bz0 = bz0;
|
||||
t.bz1 = bz1;
|
||||
t.exagg = exagg;
|
||||
return t;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ── 就绪内容 == 同步重组逐体素一致 ─────────────────────────────────────────
|
||||
TEST(AsyncRegionBuilder, ReconstructsRequestedTargetMatchesSync) {
|
||||
const auto dir =
|
||||
(std::filesystem::temp_directory_path() / "gpr_arb_match").string();
|
||||
makePyramidStore(dir, 200, 80, 60, 64, 3);
|
||||
|
||||
const RegionTarget t = makeTarget(1, 0, 2, 0, 1, 0, 1, 1.0);
|
||||
|
||||
// 同步参考:同一重组核对同一 target。
|
||||
data::ChunkedVolumeStore refStore(dir);
|
||||
vtkSmartPointer<vtkImageData> sync = reorganizeRegion(refStore, t, 16384);
|
||||
ASSERT_NE(sync.Get(), nullptr);
|
||||
|
||||
AsyncRegionBuilder b(dir);
|
||||
b.requestTarget(t);
|
||||
vtkSmartPointer<vtkImageData> async = waitReady(b);
|
||||
ASSERT_NE(async.Get(), nullptr);
|
||||
|
||||
EXPECT_EQ(async->GetScalarType(), VTK_SHORT);
|
||||
EXPECT_TRUE(imagesEqual(async.Get(), sync.Get()));
|
||||
}
|
||||
|
||||
// ── supersede:连发多个不同 target,最终收敛到最后一个,不崩不死锁 ────────────
|
||||
TEST(AsyncRegionBuilder, SupersedesStaleRequests) {
|
||||
const auto dir =
|
||||
(std::filesystem::temp_directory_path() / "gpr_arb_super").string();
|
||||
makePyramidStore(dir, 200, 80, 60, 64, 3);
|
||||
|
||||
AsyncRegionBuilder b(dir);
|
||||
|
||||
// 连发若干不同 target,最后一个为期望最终态。
|
||||
std::vector<RegionTarget> seq = {
|
||||
makeTarget(2, 0, 1, 0, 1, 0, 1),
|
||||
makeTarget(1, 0, 2, 0, 1, 0, 1),
|
||||
makeTarget(0, 0, 1, 0, 1, 0, 1),
|
||||
makeTarget(1, 1, 3, 0, 2, 0, 1), // 最终态
|
||||
};
|
||||
const RegionTarget last = seq.back();
|
||||
for (const auto& t : seq) b.requestTarget(t);
|
||||
|
||||
// 同步参考 = 最后一个 target。
|
||||
data::ChunkedVolumeStore refStore(dir);
|
||||
vtkSmartPointer<vtkImageData> sync = reorganizeRegion(refStore, last, 16384);
|
||||
ASSERT_NE(sync.Get(), nullptr);
|
||||
|
||||
// 轮询直到 pending 清空且取到与最终态一致的结果(收敛)。
|
||||
vtkSmartPointer<vtkImageData> latest;
|
||||
for (int i = 0; i < 800; ++i) {
|
||||
auto img = b.takeLatest();
|
||||
if (img != nullptr) latest = img;
|
||||
if (latest != nullptr && !b.hasPending()) break;
|
||||
sleepMs(5);
|
||||
}
|
||||
ASSERT_NE(latest.Get(), nullptr);
|
||||
EXPECT_FALSE(b.hasPending());
|
||||
EXPECT_TRUE(imagesEqual(latest.Get(), sync.Get()))
|
||||
<< "最终就绪结果应收敛到最后一个 target";
|
||||
}
|
||||
|
||||
// ── 析构忙时干净 join:建后立即析构(worker 在忙)应不崩不死锁不泄漏 ──────────
|
||||
TEST(AsyncRegionBuilder, DestructorJoinsCleanly) {
|
||||
const auto dir =
|
||||
(std::filesystem::temp_directory_path() / "gpr_arb_dtor").string();
|
||||
makePyramidStore(dir, 200, 80, 60, 64, 3);
|
||||
|
||||
for (int rep = 0; rep < 20; ++rep) {
|
||||
AsyncRegionBuilder b(dir);
|
||||
// 立刻发请求让 worker 忙起来,随即析构。
|
||||
b.requestTarget(makeTarget(0, 0, 3, 0, 2, 0, 1));
|
||||
b.requestTarget(makeTarget(1, 0, 2, 0, 1, 0, 1));
|
||||
// 不等待,直接离开作用域析构(必须干净 join)。
|
||||
}
|
||||
SUCCEED(); // 到这里没死锁/崩溃即通过。
|
||||
}
|
||||
|
||||
// ── 并发压力:主线程循环 requestTarget + takeLatest 数百次,worker 并发,不崩 ──
|
||||
TEST(AsyncRegionBuilder, ConcurrentStress) {
|
||||
const auto dir =
|
||||
(std::filesystem::temp_directory_path() / "gpr_arb_stress").string();
|
||||
makePyramidStore(dir, 200, 80, 60, 64, 3);
|
||||
|
||||
AsyncRegionBuilder b(dir);
|
||||
int gotCount = 0;
|
||||
for (int i = 0; i < 400; ++i) {
|
||||
const int lvl = i % 3;
|
||||
const int bx1 = 1 + (i % 2);
|
||||
b.requestTarget(makeTarget(lvl, 0, bx1, 0, 1, 0, 1));
|
||||
auto img = b.takeLatest(); // 非阻塞
|
||||
if (img != nullptr) ++gotCount;
|
||||
}
|
||||
// 排空:等到最后一批就绪。
|
||||
auto fin = waitReady(b);
|
||||
EXPECT_NE(fin.Get(), nullptr);
|
||||
// 过程中至少取到过若干结果(功能层证明 worker 在产出)。
|
||||
EXPECT_GE(gotCount, 0); // 不强求次数,关键是不崩不死锁
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
// ── takeLatest 非阻塞:无结果时立即返回 nullptr,不等待 worker ───────────────
|
||||
TEST(AsyncRegionBuilder, TakeLatestNonBlockingWhenEmpty) {
|
||||
const auto dir =
|
||||
(std::filesystem::temp_directory_path() / "gpr_arb_nb").string();
|
||||
makePyramidStore(dir, 128, 64, 48, 64, 2);
|
||||
|
||||
AsyncRegionBuilder b(dir);
|
||||
// 未发任何请求 → 立即 nullptr(计时应极短)。
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
auto img = b.takeLatest();
|
||||
const auto dtMs = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - t0)
|
||||
.count();
|
||||
EXPECT_EQ(img.Get(), nullptr);
|
||||
EXPECT_LT(dtMs, 100); // 远小于任一次重组耗时,证明未阻塞等 worker
|
||||
}
|
||||
Loading…
Reference in New Issue