diff --git a/src/render/CMakeLists.txt b/src/render/CMakeLists.txt index 89d7543..57523e3 100644 --- a/src/render/CMakeLists.txt +++ b/src/render/CMakeLists.txt @@ -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) diff --git a/src/render/source/AsyncRegionBuilder.cpp b/src/render/source/AsyncRegionBuilder.cpp new file mode 100644 index 0000000..2efc2f6 --- /dev/null +++ b/src/render/source/AsyncRegionBuilder.cpp @@ -0,0 +1,86 @@ +#include "source/AsyncRegionBuilder.hpp" + +#include + +namespace geopro::render { + +AsyncRegionBuilder::AsyncRegionBuilder(const std::string& storeDir) + : store_(storeDir) { + worker_ = std::thread([this] { workerLoop(); }); +} + +AsyncRegionBuilder::~AsyncRegionBuilder() { + { + std::lock_guard lk(mutex_); + stop_.store(true); + } + cv_.notify_all(); + if (worker_.joinable()) worker_.join(); +} + +void AsyncRegionBuilder::setMaxTextureDim(int dim) { + std::lock_guard lk(mutex_); + if (dim > 0) maxTextureDim_ = dim; +} + +void AsyncRegionBuilder::requestTarget(const RegionTarget& t) { + { + std::lock_guard lk(mutex_); + // 与已就绪结果对应的目标相同则不必重建(避免重复劳动);与在建/排队的期望相同 + // 亦无需扰动。简化:直接覆盖期望并标脏——worker 会以最新期望为准(supersede)。 + desired_ = t; + hasDesired_ = true; + } + cv_.notify_one(); +} + +vtkSmartPointer AsyncRegionBuilder::takeLatest() { + // 非阻塞:仅在锁内做指针移动(不做任何重组/IO/长时操作)。 + // std::move 把 ready_ 的引用移交给返回值(无 refcount 增减,且全程在锁内/单线程), + // ready_ 置空——下次无新结果再调返回 nullptr。 + std::lock_guard lk(mutex_); + return std::move(ready_); +} + +void AsyncRegionBuilder::workerLoop() { + std::unique_lock 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 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 lk(mutex_); + return hasDesired_ || building_; +} + +} // namespace geopro::render diff --git a/src/render/source/AsyncRegionBuilder.hpp b/src/render/source/AsyncRegionBuilder.hpp new file mode 100644 index 0000000..5800afc --- /dev/null +++ b/src/render/source/AsyncRegionBuilder.hpp @@ -0,0 +1,72 @@ +#pragma once +#include +#include +#include +#include +#include + +#include +#include + +#include "data/store/ChunkedVolumeStore.hpp" +#include "source/RegionReorganizer.hpp" + +namespace geopro::render { + +// C3 并发核心:把「从 store 重组视野区域单纹理」放后台线程,主线程非阻塞取最新 +// 就绪结果——使拖动/缩放时不被解压+重组卡住。 +// +// 线程安全设计(VTK 引用计数跨线程要小心): +// - 后台 worker 在自己线程内调 reorganizeRegion(C2/C3 共用重组核)构建出一个 +// vtkSmartPointer(全程不被主线程触碰)。 +// - 完成后在 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 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 ready_; // 已就绪、待主线程取走的最新结果 + std::atomic stop_{false}; // 析构置位,唤醒 worker 退出 + int maxTextureDim_ = 16384; + + std::thread worker_; // 最后声明:构造完上述成员后再起线程 +}; + +} // namespace geopro::render diff --git a/src/render/source/RegionReorganizer.cpp b/src/render/source/RegionReorganizer.cpp new file mode 100644 index 0000000..953ca8c --- /dev/null +++ b/src/render/source/RegionReorganizer.cpp @@ -0,0 +1,119 @@ +#include "source/RegionReorganizer.hpp" + +#include +#include +#include + +#include +#include +#include + +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 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(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(gi0) * sp[0], + meta.origin[1] + static_cast(gj0) * sp[1], + meta.origin[2] + static_cast(gk0) * sp[2]}; + + auto img = vtkSmartPointer::New(); + img->SetDimensions(outNx, outNy, outNz); + img->SetOrigin(org[0], org[1], org[2]); + img->SetSpacing(sp[0], sp[1], sp[2]); + + vtkNew arr; + arr->SetName("v"); + const vtkIdType total = static_cast(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 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(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(lk) * outNy + lj) * outNx + li; + arr->SetValue(id, raw[rowBase + ii]); + } + } + } + } + } + } + + img->GetPointData()->SetScalars(arr); + return img; +} + +} // namespace geopro::render diff --git a/src/render/source/RegionReorganizer.hpp b/src/render/source/RegionReorganizer.hpp new file mode 100644 index 0000000..10d3ec2 --- /dev/null +++ b/src/render/source/RegionReorganizer.hpp @@ -0,0 +1,44 @@ +#pragma once +#include +#include + +#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 reorganizeRegion( + const geopro::data::ChunkedVolumeStore& store, const RegionTarget& target, + int maxTextureDim = 16384); + +} // namespace geopro::render diff --git a/src/render/source/ViewAdaptiveVolumeSource.cpp b/src/render/source/ViewAdaptiveVolumeSource.cpp index 08c4e11..11260cd 100644 --- a/src/render/source/ViewAdaptiveVolumeSource.cpp +++ b/src/render/source/ViewAdaptiveVolumeSource.cpp @@ -1,26 +1,11 @@ #include "source/ViewAdaptiveVolumeSource.hpp" -#include -#include -#include - #include -#include -#include -#include + +#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(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(gi0) * sp[0], - meta_.origin[1] + static_cast(gj0) * sp[1], - meta_.origin[2] + static_cast(gk0) * sp[2]}; - - auto img = vtkSmartPointer::New(); - img->SetDimensions(outNx, outNy, outNz); - img->SetOrigin(org[0], org[1], org[2]); - img->SetSpacing(sp[0], sp[1], sp[2]); - - vtkNew arr; - arr->SetName("v"); - const vtkIdType total = static_cast(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 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(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(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> diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6faff58..e50fdf4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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}) diff --git a/tests/render/test_async_region_builder.cpp b/tests/render/test_async_region_builder.cpp new file mode 100644 index 0000000..b828890 --- /dev/null +++ b/tests/render/test_async_region_builder.cpp @@ -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 +#include +#include + +#include +#include +#include + +#include + +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((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 waitReady(AsyncRegionBuilder& b, int maxTries = 500, + int stepMs = 5) { + vtkSmartPointer 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 sync = reorganizeRegion(refStore, t, 16384); + ASSERT_NE(sync.Get(), nullptr); + + AsyncRegionBuilder b(dir); + b.requestTarget(t); + vtkSmartPointer 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 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 sync = reorganizeRegion(refStore, last, 16384); + ASSERT_NE(sync.Get(), nullptr); + + // 轮询直到 pending 清空且取到与最终态一致的结果(收敛)。 + vtkSmartPointer 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::steady_clock::now() - t0) + .count(); + EXPECT_EQ(img.Get(), nullptr); + EXPECT_LT(dtMs, 100); // 远小于任一次重组耗时,证明未阻塞等 worker +}