// 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 } // ── C3-3:轮询某 target 的 getReady 直到非空(带超时)────────────────────── vtkSmartPointer waitGetReady(AsyncRegionBuilder& b, const RegionTarget& t, int maxTries = 800, int stepMs = 5) { vtkSmartPointer img; int lv = -1; for (int i = 0; i < maxTries && img == nullptr; ++i) { img = b.getReady(t, lv); if (img == nullptr) sleepMs(stepMs); } return img; } // ── C3-3:预取的目标建好后即刻命中(无需再等)───────────────────────────── TEST(AsyncRegionBuilder, PrefetchedTargetReadyImmediately) { const auto dir = (std::filesystem::temp_directory_path() / "gpr_arb_prefetch").string(); makePyramidStore(dir, 200, 80, 60, 64, 3); const RegionTarget cur = makeTarget(1, 0, 2, 0, 1, 0, 1); const RegionTarget nxt = makeTarget(1, 1, 3, 0, 2, 0, 1); AsyncRegionBuilder b(dir); b.requestTarget(cur); b.prefetch({nxt}); // 等两者都进缓存就绪。 ASSERT_NE(waitGetReady(b, cur).Get(), nullptr); ASSERT_NE(waitGetReady(b, nxt).Get(), nullptr); // 此后 getReady(nxt) 即刻命中(无需再等;计时极短)。 const auto t0 = std::chrono::steady_clock::now(); int lv = -1; auto hit = b.getReady(nxt, lv); const auto dtMs = std::chrono::duration_cast( std::chrono::steady_clock::now() - t0) .count(); ASSERT_NE(hit.Get(), nullptr); EXPECT_EQ(lv, nxt.level); EXPECT_LT(dtMs, 50); // 内容 == 同步重组(缓存不改内容)。 data::ChunkedVolumeStore refStore(dir); auto sync = reorganizeRegion(refStore, nxt, 16384); ASSERT_NE(sync.Get(), nullptr); EXPECT_TRUE(imagesEqual(hit.Get(), sync.Get())); } // ── C3-3:缓存有界 LRU——请求/预取 > 容量个不同 target → size ≤ N ──────────── TEST(AsyncRegionBuilder, CacheIsBoundedLru) { const auto dir = (std::filesystem::temp_directory_path() / "gpr_arb_lru").string(); makePyramidStore(dir, 200, 80, 60, 64, 3); const std::size_t cap = 3; AsyncRegionBuilder b(dir, cap); // 提交 6 个不同 target(> 容量),逐个等就绪。 std::vector ts = { makeTarget(0, 0, 1, 0, 1, 0, 1), makeTarget(0, 1, 2, 0, 1, 0, 1), makeTarget(0, 2, 3, 0, 1, 0, 1), makeTarget(1, 0, 1, 0, 1, 0, 1), makeTarget(1, 1, 2, 0, 1, 0, 1), makeTarget(2, 0, 1, 0, 1, 0, 1), }; for (const auto& t : ts) { b.requestTarget(t); ASSERT_NE(waitGetReady(b, t).Get(), nullptr) << "target 未就绪"; } // 缓存有界:size ≤ 容量。 EXPECT_LE(b.cacheSize(), cap); // 最久未用的(第一个)应被淘汰:getReady 即刻 nullptr(不重建——非阻塞)。 int lv = -1; EXPECT_EQ(b.getReady(ts.front(), lv).Get(), nullptr) << "最久未用项应已被 LRU 淘汰"; // 最近的几个仍命中。 EXPECT_NE(b.getReady(ts.back(), lv).Get(), nullptr); } // ── C3-3:相同请求短路——同一 target 连请求多次不重复建(建一次后命中缓存)──── TEST(AsyncRegionBuilder, IdenticalRequestShortCircuits) { const auto dir = (std::filesystem::temp_directory_path() / "gpr_arb_short").string(); makePyramidStore(dir, 200, 80, 60, 64, 3); AsyncRegionBuilder b(dir); const RegionTarget t = makeTarget(1, 0, 2, 0, 1, 0, 1); b.requestTarget(t); ASSERT_NE(waitGetReady(b, t).Get(), nullptr); // 已就绪后再连发同一 target:短路(不应再有 pending/在建),缓存仍 1 条。 for (int i = 0; i < 10; ++i) b.requestTarget(t); // 给 worker 一点时间(若误重建会出现 pending/building)。 sleepMs(50); EXPECT_FALSE(b.hasPending()) << "同一已就绪 target 不应触发重建"; EXPECT_EQ(b.cacheSize(), 1u) << "同一 target 不应在缓存里重复占位"; // 短路时主目标仍可经 takeLatest 取到(兼容 C3-1/C3-2)。 int lv = -1; EXPECT_NE(b.getReady(t, lv).Get(), nullptr); } // ── C3-3:预取不饿死主目标——大量 prefetch + 一个主 requestTarget → 主先就绪 ── TEST(AsyncRegionBuilder, PrefetchDoesNotStarveMainTarget) { const auto dir = (std::filesystem::temp_directory_path() / "gpr_arb_starve").string(); makePyramidStore(dir, 200, 80, 60, 64, 3); AsyncRegionBuilder b(dir, /*cap*/ 6); // 先灌大量预取(不同 target),再设主目标。 std::vector many; for (int i = 0; i < 12; ++i) many.push_back(makeTarget(0, i % 3, (i % 3) + 1, 0, 1, 0, 1, 1.0 + i)); b.prefetch(many); const RegionTarget main = makeTarget(2, 0, 1, 0, 1, 0, 1); b.requestTarget(main); // 主目标应优先就绪(轮询 getReady(main) 命中)。 ASSERT_NE(waitGetReady(b, main, 800, 2).Get(), nullptr) << "预取饿死了主目标"; }