feat(render): C3-1 AsyncRegionBuilder 后台异步重组 + 双缓冲交接

抽公共重组核 reorganizeRegion(RegionReorganizer.{hpp,cpp})为单一真源:
C2 ViewAdaptiveVolumeSource 改委托、C3 AsyncRegionBuilder 也调它(真 DRY)。

AsyncRegionBuilder:后台 worker 调重组核构建 vtkImageData,主线程非阻塞
takeLatest 取最新就绪;requestTarget supersede 收敛最新不堆积。线程安全:
vtkImageData refcount 增减全在 mutex 内/单线程独占,析构置 stop 唤醒干净 join。

测试:5 个异步用例(同步一致/supersede 收敛/析构忙时干净 join/并发 400 次不崩
不死锁/takeLatest 非阻塞)全绿;C1+C2 的 17 个 ViewAdaptive* 抽核后回归全绿。
This commit is contained in:
gaozheng 2026-06-24 09:56:55 +08:00
parent fc9ea58cb5
commit fa34cb0bc3
8 changed files with 554 additions and 104 deletions

View File

@ -5,7 +5,7 @@ add_library(geopro_render STATIC
interact/SlicePlaneMath.cpp interact/SliceTool.cpp interact/PickInteractorStyle.cpp interact/InteractionManager.cpp interact/AnomalyDrawTool.cpp interact/SlicePlaneMath.cpp interact/SliceTool.cpp interact/PickInteractorStyle.cpp interact/InteractionManager.cpp interact/AnomalyDrawTool.cpp
ground/TileMath.cpp ground/TileMath.cpp
lod/ViewAdaptiveLodPolicy.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_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(geopro_render PUBLIC geopro_core geopro_store ${VTK_LIBRARIES} GDAL::GDAL) target_link_libraries(geopro_render PUBLIC geopro_core geopro_store ${VTK_LIBRARIES} GDAL::GDAL)
target_compile_features(geopro_render PUBLIC cxx_std_17) target_compile_features(geopro_render PUBLIC cxx_std_17)

View File

@ -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

View File

@ -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 在自己线程内调 reorganizeRegionC2/C3 共用重组核)构建出一个
// vtkSmartPointer<vtkImageData>(全程不被主线程触碰)。
// - 完成后在 mutex 下把它移交到成员 ready_。主线程在同一 mutex 下取走(移动出)。
// 所有 vtkImageData 引用计数的增减都发生在锁内/单线程独占——避免两线程同时碰
// 同一对象的 refcount。worker 构建期间主线程看不到它publish 后所有权经锁交给
// 主线程。
// - 请求合并/取最新supersedeworker 在建 A 时若 requestTarget(B),完成 A 后
// 去建最新期望B不堆积旧请求——主线程高频 update 不致排队爆炸。
class AsyncRegionBuilder {
public:
// 起 worker 线程。storeDir含金字塔的分块 store。
explicit AsyncRegionBuilder(const std::string& storeDir);
// 停 workerjoin。析构忙时干净 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

View File

@ -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^levely/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

View File

@ -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 区间 + 垂向夸张)。
// levelLOD 层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^levely/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

View File

@ -1,26 +1,11 @@
#include "source/ViewAdaptiveVolumeSource.hpp" #include "source/ViewAdaptiveVolumeSource.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <vtkCamera.h> #include <vtkCamera.h>
#include <vtkNew.h>
#include <vtkPointData.h> #include "source/RegionReorganizer.hpp"
#include <vtkShortArray.h>
namespace geopro::render { namespace geopro::render {
namespace {
// 该 level 某轴体素维度 = ceil(n / 2^level),至少 1与 C1/store 同口径)。
int dimAtLevel(int n, int level) {
const int d = (n + (1 << level) - 1) >> level;
return d > 0 ? d : 1;
}
} // namespace
ViewAdaptiveVolumeSource::ViewAdaptiveVolumeSource(const std::string& storeDir, ViewAdaptiveVolumeSource::ViewAdaptiveVolumeSource(const std::string& storeDir,
double exagg) double exagg)
: store_(storeDir), meta_(store_.meta()), exagg_(exagg > 0 ? exagg : 1.0) {} : store_(storeDir), meta_(store_.meta()), exagg_(exagg > 0 ? exagg : 1.0) {}
@ -65,93 +50,19 @@ void ViewAdaptiveVolumeSource::updateView(const CameraView& cam,
current_ = nullptr; current_ = nullptr;
return; return;
} }
const int level = sel.level; lastLevel_ = sel.level;
lastLevel_ = level;
const int brick = meta_.brick; // C2 重组改委托公共重组核 reorganizeRegionDRYC3 AsyncRegionBuilder 同源)。
const int dimLx = dimAtLevel(meta_.nx, level); RegionTarget target{};
const int dimLy = dimAtLevel(meta_.ny, level); target.level = sel.level;
const int dimLz = dimAtLevel(meta_.nz, level); target.bx0 = sel.bx0;
target.bx1 = sel.bx1;
// 重组单纹理某轴范围(与 C1 hpp 契约逐字一致): target.by0 = sel.by0;
// 起点 = b0*brick终点 = min(b1*brick, dimL, 起点 + maxTextureDim)。 target.by1 = sel.by1;
// 即使 brick > maxTextureDim单块超限也按体素上限再裁 → 恒 ≤ maxTextureDim。 target.bz0 = sel.bz0;
const int gi0 = sel.bx0 * brick; target.bz1 = sel.bz1;
const int gj0 = sel.by0 * brick; target.exagg = exagg_;
const int gk0 = sel.bz0 * brick; current_ = reorganizeRegion(store_, target, maxTextureDim_);
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^levely/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;
} }
std::vector<vtkSmartPointer<vtkImageData>> std::vector<vtkSmartPointer<vtkImageData>>

View File

@ -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) 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 # 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) 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}) target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES})
vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES}) vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES})

View File

@ -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
}