217 lines
7.7 KiB
C++
217 lines
7.7 KiB
C++
// 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
|
||
}
|