geopro/docs/superpowers/plans/2026-06-23-gpr-volume-poc.md

384 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# GPR 三维体 POCB & 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`GoogleTestnlohmann-jsonsidecar现有 `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-scanmmap/分块读)
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 ModifybuildVoxel 增 int16 重载 + 量化域传函
tests/io/gpr/ tests/core/ tests/data/store/ ← 对应测试
tools/gpr_poc/ ← POC 度量台(建体/加载/显存/fps 探针 + CLI
```
POC 度量统一进 `tools/gpr_poc`(建体耗时、输出维度、落盘体积/压缩比、加载耗时、显存、切片/体绘制 fpsB/C 用同一套指标对照。
> **POC vs 生产**Task 16地基+ 78、1011接口/存储)是**生产代码,走 TDD、有完整代码**。Task 9、1213 是**可行性探针**:给出明确实验、被测未知、通过/失败判据与度量,不预先杜撰我们正要验证的 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) {
// 写 tmpsamples=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/s100 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: 分块存储 ChunkedVolumeStoreB/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 + WholeVolumeSourceB
**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; // B1 个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 18 全部。
**被验证的未知 / 阻塞点:** ①int16 GPU ray cast 在真机正常出图;②真实建体耗时与输出体积;③整卷 5~10GB 加载耗时、显存峰值;④切片拖动 fps、体绘制 fps⑤超显存时 `vtkSmartVolumeMapper``LowResResample`(`MaxMemoryInBytes`) 自动降质观感。
- [ ] **Step 1:** `gpr_poc build 明星路` → 跑 Task 16输出建体耗时、`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: OutOfCoreSourceC— 最高风险探针:核外体绘制
**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:** 逐项记录未知 14 的实测结论接缝截图、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 Bint16 路径(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. 占位符扫描**:地基任务(18,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 只需证可行 + 撞出阻塞,不追求生产质量分页器;但若未知 1VTK 渲染动态工作集)撞墙,是 C 的根本阻塞,须立刻反馈 spec C 并评估替代OpenVDS / 自建 GL
- **真实 13G 在合理分辨率下可能装得进显存** → B 顺过、C 的核外价值要靠"细分辨率/拼全路段大体"的真实配置才能压出Task 9/12 的网格参数留 CLI 可调,用同一份真实数据调出超显存体来考验 C
- **量化贯穿**漏一处即色阶/读数错Global 约束 + Task 4/7Self-Review 已盯。