384 lines
23 KiB
Markdown
384 lines
23 KiB
Markdown
# GPR 三维体 POC(B & C 双方案)实现计划
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 用真实 13G 雷达数据为 B(整卷上 GPU)与 C(分块+金字塔+核外)两套对等方案做 POC,验证技术可行性并挖出 spec 未预见的阻塞;POC 代码即生产地基与接口实现,不返工。
|
||
|
||
**Architecture:** 共用地基(解析/几何/结构化建体/int16 量化体/分块存储)+ `IVolumeRenderSource` 渲染接缝;`WholeVolumeSource`(B) 与 `OutOfCoreSource`(C) 是接口下两个永久并存实现,用户运行时按数据规模切换。落盘从第一天就分块,B 的裸分块格式是 C 金字塔/核外的基座。
|
||
|
||
**Tech Stack:** C++17, Qt6, VTK 9.6(`RenderingVolumeOpenGL2` GPU ray cast,自带 `vtkzlib`),GoogleTest,nlohmann-json(sidecar),现有 `src/{core,data,render,app}` 分层。
|
||
|
||
## Global Constraints
|
||
|
||
- **dtype**:雷达体走 **int16**(`vtkShortArray`),不污染反演剖面的 double 主路径(`ScalarVolume` 保持 `std::vector<double>`,`src/core/model/Field.hpp:8-26`)。
|
||
- **量化**:物理值 ↔ int16 经 `scale/offset`,**必须贯穿**传递函数采样、色阶 LUT(`src/render/interact/SliceTool.cpp:37`)、取值/详情反量化(见 spec B §3.5)。
|
||
- **落盘**:**不用 `vtkHDFWriter`**(VTK 9.6 写不了 `vtkImageData`,记忆 `vtk96-hdfwriter-no-imagedata`)。用裸 int16 分块 + sidecar(json) + 逐块 `vtkzlib`。
|
||
- **渲染接缝**:上层(场景/切片)只面向 `IVolumeRenderSource`;B/C 是其两个实现。
|
||
- **结构化建体**:X(沿线)/Z(深度) 规则落格,仅 Y(14 通道) 向 1D 插值;**不**对雷达用全 3D 散点 IDW(现有 `IdwInterpolator` 无空间索引暴力,`src/core/algo/IdwInterpolator.cpp:15-33`)。
|
||
- **真实数据判定**:POC 用 `D:\Downloads\明星路`(450MHz/14通道/821采样/~45306道/线/20线/int16/13.6GB)。POC 过 = 在该真实数据上跑通并达标,不许避重就轻。
|
||
- **测试数据头实证**:`.iprh` 文本键值(`SAMPLES/LAST TRACE/CHANNELS/TIMEWINDOW/SOIL VELOCITY/DISTANCE INTERVAL`);`.iprb` = `int16[samples × traces]`,`samples×traces×2 == 文件大小`;`.ord` = 通道横向偏移(14 有效)。
|
||
|
||
---
|
||
|
||
## 文件结构(决定分解与复用)
|
||
|
||
```
|
||
src/io/gpr/ ← 新增:雷达 IO(共用地基)
|
||
IprHeader.{hpp,cpp} 解析 .iprh → 结构体
|
||
IprbReader.{hpp,cpp} 读 .iprb int16 B-scan(mmap/分块读)
|
||
GprGeometry.{hpp,cpp} .ord 通道偏移 + .gps/.cor 逐道经纬 + 深度轴
|
||
GprSurvey.{hpp,cpp} 一个工区 = 线[]×通道[] + 几何(建体输入)
|
||
src/core/model/
|
||
ScalarVolumeI16.hpp int16 体 + Quant{scale,offset} (新增,与 ScalarVolume 并列)
|
||
src/core/algo/
|
||
GprVolumeBuilder.{hpp,cpp} 结构化建体:X/Z 落格 + Y 向 1D 插值 → ScalarVolumeI16
|
||
src/data/store/
|
||
ChunkedVolumeStore.{hpp,cpp} 分块 int16 + zlib + sidecar;读/写/按块取(B/C 共用,C 加金字塔)
|
||
src/render/source/
|
||
IVolumeRenderSource.hpp 渲染接缝(接口)
|
||
WholeVolumeSource.{hpp,cpp} B:读全块 → 1 个 vtkImageData
|
||
OutOfCoreSource.{hpp,cpp} C:金字塔 + brick 分页 → 工作集
|
||
src/render/actors/
|
||
VoxelActor.cpp Modify:buildVoxel 增 int16 重载 + 量化域传函
|
||
tests/io/gpr/ tests/core/ tests/data/store/ ← 对应测试
|
||
tools/gpr_poc/ ← POC 度量台(建体/加载/显存/fps 探针 + CLI)
|
||
```
|
||
|
||
POC 度量统一进 `tools/gpr_poc`(建体耗时、输出维度、落盘体积/压缩比、加载耗时、显存、切片/体绘制 fps),B/C 用同一套指标对照。
|
||
|
||
> **POC vs 生产**:Task 1–6(地基)+ 7–8、10–11(接口/存储)是**生产代码,走 TDD、有完整代码**。Task 9、12–13 是**可行性探针**:给出明确实验、被测未知、通过/失败判据与度量,不预先杜撰我们正要验证的 VTK 核外内部实现——这是 POC 的本质,强行写"完整代码"等于造假。
|
||
|
||
---
|
||
|
||
## Phase 0 — 地基(共用,生产级 TDD)
|
||
|
||
### Task 1: .iprh 头解析
|
||
|
||
**Files:**
|
||
- Create: `src/io/gpr/IprHeader.hpp`, `src/io/gpr/IprHeader.cpp`
|
||
- Test: `tests/io/gpr/test_ipr_header.cpp`
|
||
|
||
**Interfaces:**
|
||
- Produces: `struct IprHeader { int samples; long lastTrace; int channels; double timeWindowNs; double soilVelocity; double distanceInterval; };` + `IprHeader parseIprHeader(const std::string& text);`
|
||
|
||
- [ ] **Step 1: 写失败测试**
|
||
```cpp
|
||
#include "io/gpr/IprHeader.hpp"
|
||
#include <gtest/gtest.h>
|
||
using geopro::io::gpr::parseIprHeader;
|
||
TEST(IprHeader, ParsesKeyFieldsFromRealSample) {
|
||
const std::string t =
|
||
"SAMPLES: 821\nLAST TRACE: 45305\nCHANNELS: 14\n"
|
||
"TIMEWINDOW: 160.352\nSOIL VELOCITY: 100.000000\nDISTANCE INTERVAL: 0.049084\n";
|
||
auto h = parseIprHeader(t);
|
||
EXPECT_EQ(h.samples, 821);
|
||
EXPECT_EQ(h.lastTrace, 45305);
|
||
EXPECT_EQ(h.channels, 14);
|
||
EXPECT_DOUBLE_EQ(h.timeWindowNs, 160.352);
|
||
EXPECT_DOUBLE_EQ(h.soilVelocity, 100.0);
|
||
EXPECT_NEAR(h.distanceInterval, 0.049084, 1e-9);
|
||
}
|
||
```
|
||
- [ ] **Step 2: 运行确认失败** — `ctest -R IprHeader`,预期 编译/链接失败(未定义)。
|
||
- [ ] **Step 3: 最小实现** — `parseIprHeader` 逐行 `key: value` 拆分,按字段名填结构体;缺字段抛 `std::runtime_error`。
|
||
- [ ] **Step 4: 运行确认通过** — `ctest -R IprHeader`,预期 PASS。
|
||
- [ ] **Step 5: 提交** — `git commit -m "feat(gpr): parse .iprh header fields"`
|
||
|
||
### Task 2: .iprb B-scan 读取
|
||
|
||
**Files:**
|
||
- Create: `src/io/gpr/IprbReader.{hpp,cpp}`
|
||
- Test: `tests/io/gpr/test_iprb_reader.cpp`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `IprHeader`
|
||
- Produces: `struct BScan { int samples; long traces; std::vector<int16_t> data; /* [trace*samples + s] */ };` + `BScan readIprb(const std::string& path, const IprHeader& h);`(校验 `samples*traces*2 == fileSize`)
|
||
|
||
- [ ] **Step 1: 写失败测试**(用临时文件造 4 道×3 采样 int16)
|
||
```cpp
|
||
TEST(IprbReader, ReadsInt16AndValidatesSize) {
|
||
// 写 tmp:samples=3, traces=4 → 24 bytes
|
||
std::vector<int16_t> raw{0,1,2, 10,11,12, 20,21,22, 30,31,32};
|
||
auto path = writeTmp(raw); // helper
|
||
geopro::io::gpr::IprHeader h{}; h.samples=3; h.lastTrace=3; // traces=lastTrace+1=4
|
||
auto b = geopro::io::gpr::readIprb(path, h);
|
||
EXPECT_EQ(b.samples, 3); EXPECT_EQ(b.traces, 4);
|
||
EXPECT_EQ(b.data[1*3 + 2], 12); // 第1道第2采样
|
||
}
|
||
```
|
||
- [ ] **Step 2: 运行确认失败**。
|
||
- [ ] **Step 3: 最小实现** — `traces = lastTrace+1`;读全文件为 int16;不匹配大小抛错。
|
||
- [ ] **Step 4: 通过**。
|
||
- [ ] **Step 5: 提交** — `git commit -m "feat(gpr): read .iprb int16 b-scan with size check"`
|
||
|
||
### Task 3: 几何(通道偏移 + 逐道经纬 + 深度轴)
|
||
|
||
**Files:**
|
||
- Create: `src/io/gpr/GprGeometry.{hpp,cpp}`
|
||
- Test: `tests/io/gpr/test_gpr_geometry.cpp`
|
||
|
||
**Interfaces:**
|
||
- Produces:
|
||
- `std::vector<double> parseChannelXOffsets(const std::string& ordText);`(取第 4 列==1 的有效通道横偏,明星路应得 14 个 -0.686..+0.686)
|
||
- `double depthOfSample(int s, const IprHeader& h);`(`= s * (timeWindowNs/(samples-1)) * soilVelocity*1e-9/2`,单位米;soilVelocity 100 m/µs = 1e8 m/s)
|
||
|
||
- [ ] **Step 1: 写失败测试**
|
||
```cpp
|
||
TEST(GprGeometry, ParsesActiveChannelOffsets) {
|
||
const std::string ord = "0 -0.686000 -1.5 1\n1 -0.581000 -1.5 1\n14 0 -1.5 0\n";
|
||
auto xs = geopro::io::gpr::parseChannelXOffsets(ord);
|
||
EXPECT_EQ(xs.size(), 2u); // 仅 2 个有效(末列=1)
|
||
EXPECT_NEAR(xs[0], -0.686, 1e-6);
|
||
}
|
||
TEST(GprGeometry, DepthOfLastSampleMatchesPhysics) {
|
||
geopro::io::gpr::IprHeader h{}; h.samples=821; h.timeWindowNs=160.352; h.soilVelocity=1e8;
|
||
EXPECT_NEAR(geopro::io::gpr::depthOfSample(820, h), 8.0, 0.05); // ~8m
|
||
}
|
||
```
|
||
> 注:`soilVelocity` 单位换算在 Task 1 读入时统一成 m/s(100 m/µs = 1e8 m/s),在此基础上测试。
|
||
- [ ] **Step 2: 失败**。
|
||
- [ ] **Step 3: 实现** — `.ord` 按空白拆列、末列=="1" 收集第 2 列;`depthOfSample` 按公式。
|
||
- [ ] **Step 4: 通过**。
|
||
- [ ] **Step 5: 提交** — `git commit -m "feat(gpr): channel offsets + depth axis geometry"`
|
||
|
||
### Task 4: int16 量化体类型
|
||
|
||
**Files:**
|
||
- Create: `src/core/model/ScalarVolumeI16.hpp`
|
||
- Test: `tests/core/test_scalar_volume_i16.cpp`
|
||
|
||
**Interfaces:**
|
||
- Produces:
|
||
```cpp
|
||
struct Quant { double scale = 1.0; double offset = 0.0;
|
||
int16_t toQ(double v) const; // round((v-offset)/scale),钳到[INT16_MIN+1,INT16_MAX]
|
||
double toPhys(int16_t q) const; }; // q*scale+offset
|
||
class ScalarVolumeI16 { // 行优先 idx=((k*ny+j)*nx+i),与 vtkImageData 一致
|
||
ScalarVolumeI16(int nx,int ny,int nz);
|
||
int16_t& at(int i,int j,int k); int nx()const; ...; std::vector<int16_t>& data();
|
||
static constexpr int16_t kBlank = INT16_MIN; }; // 空值哨兵→透明
|
||
```
|
||
|
||
- [ ] **Step 1: 写失败测试**(量化往返 + 索引布局 + blank)
|
||
```cpp
|
||
TEST(ScalarVolumeI16, QuantRoundTripAndLayout) {
|
||
geopro::core::Quant q{0.5, -10.0};
|
||
EXPECT_EQ(q.toQ(-10.0), 0); EXPECT_NEAR(q.toPhys(q.toQ(3.0)), 3.0, 0.25);
|
||
geopro::core::ScalarVolumeI16 v(2,2,2);
|
||
v.at(1,0,1) = 7; EXPECT_EQ(v.data()[(1*2+0)*2+1], 7);
|
||
}
|
||
```
|
||
- [ ] **Step 2: 失败** → **Step 3: 实现** → **Step 4: 通过**。
|
||
- [ ] **Step 5: 提交** — `git commit -m "feat(core): int16 scalar volume + quantization"`
|
||
|
||
### Task 5: 结构化建体 GprVolumeBuilder
|
||
|
||
**Files:**
|
||
- Create: `src/core/algo/GprVolumeBuilder.{hpp,cpp}`
|
||
- Test: `tests/core/test_gpr_volume_builder.cpp`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `GprSurvey`(线×通道 BScan + 几何)、`GridSpec`(复用 `src/core/algo/IInterpolator.hpp:7-13`)
|
||
- Produces: `struct BuiltI16 { ScalarVolumeI16 vol; Quant quant; std::array<double,3> origin, spacing; double vminPhys, vmaxPhys; };`
|
||
`BuiltI16 buildGprVolume(const GprSurvey& s, const GridSpec& spec);`
|
||
- **算法**:X(沿线)/Z(深度) 最近邻或线性落格(道已规则);**Y 向**对落在该 (x,z) 的 14 通道值做 1D 线性插值填充横向网格;maxDist/无覆盖 → `kBlank`。量化 scale/offset 由全体 min/max 定。
|
||
|
||
- [ ] **Step 1: 写失败测试**(2 通道、各 1 道×2 采样的人造 survey,验横向中点插值 + 维度)
|
||
```cpp
|
||
TEST(GprVolumeBuilder, InterpolatesAcrossChannelsOnly) {
|
||
auto s = makeTwoChannelSurvey(/*ch0 val=0, ch1 val=100, 横偏 0 和 1m*/);
|
||
geopro::core::GridSpec spec{/*nx=*/3,/*ny=*/1,/*nz=*/1, 0,0,0, 0.5,1,1, 2.0, 9.9};
|
||
auto b = geopro::core::buildGprVolume(s, spec);
|
||
EXPECT_NEAR(b.quant.toPhys(b.vol.at(1,0,0)), 50.0, 1.0); // 横向中点≈50
|
||
}
|
||
```
|
||
- [ ] **Step 2: 失败** → **Step 3: 实现**(先单线程,循环结构留可并行)→ **Step 4: 通过**。
|
||
- [ ] **Step 5: 提交** — `git commit -m "feat(core): structured GPR volume builder (Y-only interp)"`
|
||
|
||
### Task 6: 分块存储 ChunkedVolumeStore(B/C 共用基座)
|
||
|
||
**Files:**
|
||
- Create: `src/data/store/ChunkedVolumeStore.{hpp,cpp}`
|
||
- Test: `tests/data/store/test_chunked_volume_store.cpp`
|
||
|
||
**Interfaces:**
|
||
- Produces:
|
||
```cpp
|
||
struct StoreMeta { int nx,ny,nz; int brick; // e.g. 64
|
||
std::array<double,3> origin, spacing; Quant quant; double vminPhys,vmaxPhys; };
|
||
class ChunkedVolumeStore {
|
||
static void write(const std::string& dir, const BuiltI16& b, int brick=64); // 分块+zlib+sidecar.json
|
||
static StoreMeta readMeta(const std::string& dir);
|
||
std::vector<int16_t> readBrick(int bx,int by,int bz) const; // 解压单块
|
||
// Task 10 追加:pyramid 层;Task 11 用 readBrick 做工作集
|
||
};
|
||
```
|
||
- **格式**:`meta.json`(StoreMeta + 分块索引/偏移/压缩长度) + `data.bin`(逐块 zlib 压缩流)。zlib 用 VTK 自带 `vtkzlib` 或直接 zlib C API。
|
||
|
||
- [ ] **Step 1: 写失败测试**(write→readMeta→readBrick 往返 + 压缩后小于原始)
|
||
```cpp
|
||
TEST(ChunkedVolumeStore, RoundTripBrickAndCompresses) {
|
||
auto b = makeBuilt(128,128,128, /*可压缩模式*/);
|
||
geopro::data::ChunkedVolumeStore::write(tmpDir, b, 64);
|
||
auto m = geopro::data::ChunkedVolumeStore::readMeta(tmpDir);
|
||
EXPECT_EQ(m.nx, 128); EXPECT_EQ(m.brick, 64);
|
||
geopro::data::ChunkedVolumeStore s(tmpDir);
|
||
auto blk = s.readBrick(0,0,0);
|
||
EXPECT_EQ(blk.size(), 64u*64*64);
|
||
EXPECT_LT(fileSize(tmpDir+"/data.bin"), 128u*128*128*2); // 压缩生效
|
||
}
|
||
```
|
||
- [ ] **Step 2: 失败** → **Step 3: 实现** → **Step 4: 通过**。
|
||
- [ ] **Step 5: 提交** — `git commit -m "feat(data): chunked int16 volume store (zlib + sidecar)"`
|
||
|
||
### Task 7: VoxelActor int16 重载 + 量化域传递函数
|
||
|
||
**Files:**
|
||
- Modify: `src/render/actors/VoxelActor.cpp`(增 int16 重载,参照现 double 版 `:41-79`)
|
||
- Test: `tests/render/test_voxel_i16_smoke.cpp`(无窗渲染冒烟 / 或断言 image 标量类型与传函控制点在量化域)
|
||
|
||
**Interfaces:**
|
||
- Produces: `vtkSmartPointer<vtkVolume> buildVoxelI16(const ScalarVolumeI16& vol, const Quant& q, const ColorScale& cs, double ox,..,dz, vtkSmartPointer<vtkImageData>& outImage);`
|
||
- **要点**:`vtkShortArray` 填值;传函/不透明度在**量化域 qmin/qmax** 加点(`q.toQ(vminPhys)`..`q.toQ(vmaxPhys)`),`kBlank→0` 不透明;`vtkSmartVolumeMapper`。
|
||
|
||
- [ ] **Step 1: 写失败测试**(构造小 int16 体,断言 `outImage->GetScalarType()==VTK_SHORT` 且传函在量化域采样不崩)。
|
||
- [ ] **Step 2: 失败** → **Step 3: 实现** → **Step 4: 通过**。
|
||
- [ ] **Step 5: 提交** — `git commit -m "feat(render): int16 voxel actor with quantized transfer fn"`
|
||
|
||
---
|
||
|
||
## Phase 1 — POC-B(整卷上 GPU)
|
||
|
||
### Task 8: IVolumeRenderSource + WholeVolumeSource(B)
|
||
|
||
**Files:**
|
||
- Create: `src/render/source/IVolumeRenderSource.hpp`, `src/render/source/WholeVolumeSource.{hpp,cpp}`
|
||
- Test: `tests/render/test_whole_volume_source.cpp`
|
||
|
||
**Interfaces:**
|
||
- Produces:
|
||
```cpp
|
||
class IVolumeRenderSource { public: virtual ~IVolumeRenderSource()=default;
|
||
virtual StoreMeta meta() const = 0;
|
||
virtual void update(const Camera& cam) = 0; // B:首次载全量;C:按相机换块
|
||
virtual std::vector<vtkSmartPointer<vtkImageData>> currentProps() const = 0; // B:1 个;C:工作集
|
||
virtual vtkImageData* sliceSource() const = 0; }; // 供 SliceTool reslice
|
||
class WholeVolumeSource : public IVolumeRenderSource { // 读全块拼 1 个 int16 vtkImageData
|
||
explicit WholeVolumeSource(const std::string& storeDir); };
|
||
```
|
||
|
||
- [ ] **Step 1: 写失败测试**(从 Task 6 写出的 store 构造 WholeVolumeSource,`currentProps().size()==1`,维度==meta)。
|
||
- [ ] **Step 2: 失败** → **Step 3: 实现**(遍历所有 brick 填进整卷 image)→ **Step 4: 通过**。
|
||
- [ ] **Step 5: 提交** — `git commit -m "feat(render): IVolumeRenderSource + whole-volume source (B)"`
|
||
|
||
### Task 9: POC-B 真实数据度量(探针,含通过判据)
|
||
|
||
**Files:**
|
||
- Create: `tools/gpr_poc/main.cpp`(CLI:`gpr_poc build|renderB <明星路目录>`)、`tools/gpr_poc/Probe.{hpp,cpp}`(计时/显存/fps)
|
||
- 复用:Task 1–8 全部。
|
||
|
||
**被验证的未知 / 阻塞点:** ①int16 GPU ray cast 在真机正常出图;②真实建体耗时与输出体积;③整卷 5~10GB 加载耗时、显存峰值;④切片拖动 fps、体绘制 fps;⑤超显存时 `vtkSmartVolumeMapper` 的 `LowResResample`(`MaxMemoryInBytes`) 自动降质观感。
|
||
|
||
- [ ] **Step 1:** `gpr_poc build 明星路` → 跑 Task 1–6,输出:建体耗时、`nx×ny×nz`、落盘体积、压缩比。记录。
|
||
- [ ] **Step 2:** `gpr_poc renderB` → WholeVolumeSource 上 `vtkSmartVolumeMapper`,量化传函着色,真窗口显示。
|
||
- [ ] **Step 3:** Probe 采集:加载耗时、显存峰值、切片拖动 fps(脚本化相机/切片移动)、体绘制旋转 fps。
|
||
- [ ] **Step 4:** 设 `MaxMemoryInBytes` 低于体大小,验证 `LowResResample` 自动降质路径出图。
|
||
- [ ] **Step 5:** 写 `docs/superpowers/plans/poc-results-B.md`:指标表 + 结论。
|
||
- **B 通过判据**:真实数据建体可完成且体积/耗时可接受;整卷在目标显存内出图;**切片拖动 ≥ 可用帧率(目标 ≥30fps)**;超显存时 LowRes 兜底可用。任一硬阻塞(如 GPU 不吃 short、显存必爆且 LowRes 不可接受)→ 记为 B 的落地风险并反馈 spec。
|
||
- [ ] **Step 6: 提交** — `git commit -m "test(gpr): POC-B real-data metrics harness + results"`
|
||
|
||
---
|
||
|
||
## Phase 2 — POC-C(分块+金字塔+核外,含最小真实分页器)
|
||
|
||
### Task 10: 金字塔生成(ChunkedVolumeStore 增量)
|
||
|
||
**Files:**
|
||
- Modify: `src/data/store/ChunkedVolumeStore.{hpp,cpp}`(加 LOD 层 + 每块 min/max)
|
||
- Test: `tests/data/store/test_pyramid.cpp`
|
||
|
||
**Interfaces:**
|
||
- Produces: `void buildPyramid(int levels);`(逐级 2× 降采样,每层独立分块 + 每块 `int16 min,max` 存 meta);`std::vector<int16_t> readBrick(int level,int bx,int by,int bz);`;`std::pair<int16_t,int16_t> brickRange(int level,int bx,int by,int bz);`
|
||
|
||
- [ ] **Step 1: 写失败测试**(建 1 层金字塔,断言 level1 维度≈半、brickRange 命中真实 min/max)。
|
||
- [ ] **Step 2: 失败** → **Step 3: 实现** → **Step 4: 通过**。
|
||
- [ ] **Step 5: 提交** — `git commit -m "feat(data): volume pyramid + per-brick min/max"`
|
||
|
||
### Task 11: brick 分页器(LRU 工作集,生产级 TDD)
|
||
|
||
**Files:**
|
||
- Create: `src/render/source/BrickPager.{hpp,cpp}`
|
||
- Test: `tests/render/test_brick_pager.cpp`
|
||
|
||
**Interfaces:**
|
||
- Produces:
|
||
```cpp
|
||
class BrickPager { // 内存恒定:驻留 ≤ budgetBricks 个解压块
|
||
BrickPager(const ChunkedVolumeStore& store, size_t budgetBricks);
|
||
void requestVisible(const std::vector<BrickId>& visible, int level); // 载入缺失、LRU 淘汰
|
||
const std::vector<int16_t>* get(BrickId id, int level) const; // 命中返回,未命中 nullptr
|
||
size_t residentCount() const; };
|
||
```
|
||
|
||
- [ ] **Step 1: 写失败测试**(budget=4,请求 6 块 → residentCount==4,最早的被淘汰,命中/未命中正确)。
|
||
- [ ] **Step 2: 失败** → **Step 3: 实现**(LRU + 从 store 解压载入)→ **Step 4: 通过**。
|
||
- [ ] **Step 5: 提交** — `git commit -m "feat(render): bounded-memory brick pager (LRU)"`
|
||
|
||
### Task 12: OutOfCoreSource(C)— 最高风险探针:核外体绘制
|
||
|
||
**Files:**
|
||
- Create: `src/render/source/OutOfCoreSource.{hpp,cpp}`(实现 `IVolumeRenderSource`)
|
||
- 复用:Task 10/11。
|
||
|
||
**被验证的未知 / 阻塞点(C 的命门,必须正面撞):**
|
||
1. **VTK 能否渲染"动态换入换出的块工作集"为体**——把 BrickPager 的工作集作为多个 `vtkImageData` 喂给 `vtkMultiBlockVolumeMapper`(注意其"试图全量加载"语义,须只喂视野块)或多个 `vtkVolume` 叠加;验证可行性与正确性。
|
||
2. **块边接缝**:相邻 brick 渲染交界是否可见;试 `vtkMultiBlockVolumeMapper` 的抖动是否压得住。
|
||
3. **LOD 切换**:相机拉远用粗层、拉近换细层,切换是否闪烁/可接受。
|
||
4. **热路径解压**:拖动每帧换块时 zlib 解压是否拖垮帧率(CPU 瓶颈)。
|
||
|
||
> 本任务**不预写完整实现**——它就是要发现上面四点的真实结论。步骤是受控实验,产物是"能跑的最小版本 + 实测结论"。
|
||
|
||
- [ ] **Step 1:** `update(cam)` 内:算视野相交 brick + 选 LOD → `BrickPager::requestVisible` → `currentProps()` 返回工作集 image。
|
||
- [ ] **Step 2:** 用 `vtkMultiBlockVolumeMapper`(或 N×`vtkVolume`)渲染工作集,量化传函复用 Task 7。先静态相机出图,确认正确。
|
||
- [ ] **Step 3:** 接相机移动 → 动态换块;Probe 测 residentCount/内存恒定、换块 fps。
|
||
- [ ] **Step 4:** 逐项记录未知 1–4 的实测结论(接缝截图、LOD 切换录屏指标、解压占帧时间)。
|
||
- [ ] **Step 5:** 写 `poc-results-C.md`:四个未知的结论 + 是否构成阻塞 + 缓解手段。
|
||
- **C 通过判据**:工作集体绘制能正确出图且**内存恒定**;接缝/闪烁/解压三项**各自有可接受方案或明确缓解**(不可接受则记为阻塞并反馈 spec C,这正是 POC 的目的)。
|
||
- [ ] **Step 6: 提交** — `git commit -m "test(gpr): POC-C out-of-core volume render probe + results"`
|
||
|
||
### Task 13: 切片核外 + B/C 切换贯通
|
||
|
||
**Files:**
|
||
- Modify: `src/render/source/OutOfCoreSource.cpp`(`sliceSource()`:只读切面相交块拼子体供 reslice)
|
||
- 复用现有 `SliceTool`(`src/render/interact/SliceTool.cpp`)对 source 给出的 image 切片。
|
||
|
||
**被验证的未知:** 切片只读相交块时的内存/fps;同一 `IVolumeRenderSource` 下 B↔C 运行时切换无缝。
|
||
|
||
- [ ] **Step 1:** `OutOfCoreSource::sliceSource()` 按当前切面算相交 brick → 拼最小子体 image。
|
||
- [ ] **Step 2:** `SliceTool` 对该 image reslice,拖动切片测 fps、内存。
|
||
- [ ] **Step 3:** POC 台加 `renderC` 与 `--source whole|ooc` 开关,验证同一上层代码切两实现。
|
||
- [ ] **Step 4:** 补 `poc-results-C.md`:切片核外指标 + 切换验证。
|
||
- [ ] **Step 5: 提交** — `git commit -m "feat(render): slice out-of-core + runtime B/C source switch"`
|
||
|
||
---
|
||
|
||
## Self-Review(对照 spec 检查)
|
||
|
||
**1. spec 覆盖**
|
||
- spec B:int16 路径(Task 4/7)、结构化建体(Task 5)、裸分块落盘(Task 6)、量化贯穿(Task 4/7/Global)、整卷渲染(Task 8/9)、LowResResample(Task 9) ✓
|
||
- spec C:分块(Task 6)、金字塔+min/max(Task 10)、brick 分页(Task 11)、核外体绘制(Task 12)、切片核外(Task 13)、B/C 切换(Task 13) ✓
|
||
- 共有地基:.iprb/.iprh 解析(Task 1/2)、几何/配准(Task 3)、GPR→体(Task 5) ✓
|
||
- **缺口(POC 不覆盖,明确记录)**:ddCode 接入数据集树/UI、后端持久化对接、异常/色阶编辑器接线——POC 阶段不做(spec A §2 / B §6 列为成品阶段),不影响复用。
|
||
|
||
**2. 占位符扫描**:地基任务(1–8,10,11)均有完整测试代码与实现描述;POC 探针(9,12,13)按设计**有意不写杜撰实现**,代之以受控实验 + 通过判据(已在任务内注明本质)。无 "TODO/TBD/适当处理" 类空话。
|
||
|
||
**3. 类型一致性**:`ScalarVolumeI16`/`Quant`/`BuiltI16`/`StoreMeta`/`IVolumeRenderSource`/`BrickPager` 在定义任务与消费任务间签名一致;`buildGprVolume`/`buildVoxelI16`/`ChunkedVolumeStore::{write,readMeta,readBrick}`/`requestVisible` 全程同名。
|
||
|
||
---
|
||
|
||
## 关键风险(开工即知)
|
||
|
||
- **Task 12 是月级生产工程的"最小探针"**:POC 只需证可行 + 撞出阻塞,不追求生产质量分页器;但若未知 1(VTK 渲染动态工作集)撞墙,是 C 的根本阻塞,须立刻反馈 spec C 并评估替代(OpenVDS / 自建 GL)。
|
||
- **真实 13G 在合理分辨率下可能装得进显存** → B 顺过、C 的核外价值要靠"细分辨率/拼全路段大体"的真实配置才能压出(Task 9/12 的网格参数留 CLI 可调,用同一份真实数据调出超显存体来考验 C)。
|
||
- **量化贯穿**漏一处即色阶/读数错(Global 约束 + Task 4/7),Self-Review 已盯。
|