docs(gpr): 三维体三方案 spec(A/B/C) + POC 实现计划

B/C 对等双方案(用户运行时按需切换),A 并入 B;含 opus 评审修订
(VTKHDF Writer 写不了规则体→裸分块落盘、量化贯穿、最小真实核外分页器)。
This commit is contained in:
gaozheng 2026-06-23 09:38:28 +08:00
parent 12813bd8d0
commit b509795ffd
4 changed files with 710 additions and 0 deletions

View File

@ -0,0 +1,383 @@
# 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 已盯。

View File

@ -0,0 +1,99 @@
# GPR 三维体 · 方案 A整卷上纹理不用金字塔复用现有管线最简基线
- 日期2026-06-23
- 范围:把 GPR探地雷达阵列数据插值成三维体并在 VTK 中渲染/切片,**直接复用现有剖面三维体管线**,整卷一次性进显存,不做分块/金字塔/核外。
- 定位:三选一中的**最小改动基线**。用于评估"现有架构原样接雷达,能做到什么、卡在哪"。
- **⚠ 评审结论2026-06-23opusA 不应作为独立交付步,建议并入 B。** 唯一值得从 A 单独先做的是"三方案共有的地基"§2。`double`+400³+暴力 IDW 三条硬约束使 A 产出的 GPR 体既无业务分辨率(沿线被强制粗化到 5.5m vs 物理 5cm、又无法落盘秒开。详见 §5/§6。
- 测试数据明星路450MHz 阵列 GPR14 通道,每道 821 采样,单线 ~45306 道20 线int16合计 13.6GB;路长 2223m测幅 1.37m,深 ~8m。
---
## 1. 设计意图
不引入任何新渲染/存储机制。把雷达数据**喂进现有体素管线**,让它走和反演剖面三维体完全一样的路:
```
.iprb/.iprh → PointSet → core::IdwInterpolator → ScalarVolume(double)
→ data::VolumeGrid → render::buildVoxel → vtkSmartVolumeMapper整卷进显存
切片render::interact::SliceToolvtkImagePlaneWidget / vtkImageResliceCPU 重采样)
持久化VolumeBuildParams参数必存+ 可选明细缓存(现内存 mock
```
现有落点(实证):
- `core::ScalarVolume` = `std::vector<double>`,行优先(`src/core/model/Field.hpp:8-26`)。
- `render::buildVoxel``vtkImageData`+`vtkDoubleArray`+`vtkSmartVolumeMapper`,整卷上传(`src/render/actors/VoxelActor.cpp:41-79`)。
- 体素维度上限 `kMaxVolumeDim = 400``src/core/algo/VolumeBuilder.hpp:8`)。
- 切片 CPU 重采样(`src/render/interact/SliceTool.cpp:24-39`)。
- 插值 `IdwInterpolator`,单线程三重循环,且**无空间索引——每体素全点集线性扫描O(体素数×点数) 暴力**`src/core/algo/IdwInterpolator.cpp:15-33`,评审实证)。雷达级点集下即便 400³ 也是分钟级甚至卡死,不只是"偏慢"。
- 持久化 `Api3dRepository::StoredVolume`,纯内存(`src/data/api/Api3dRepository.hpp:112-119`),重算逻辑已就绪(`Api3dRepository.cpp:212-225`)。
- **注意(评审)**:现有 `loadVolume` 的散点来源硬绑 `loadSection`/`appendGridPoints`ERT 反演帘面,`Api3dRepository.cpp:146-171`**雷达没有现成喂入路径**。"复用现有管线"实际仍须新写 GPR→`buildVolume` 接入§2 已含§1 流程图的"原样复用"措辞偏乐观。
---
## 2. 新增工作(雷达接入,三方案共有的地基)
A/B/C 都绕不开这块A 用最朴素实现:
1. **`.iprb`/`.iprh` 解析器**(新):`.iprh` 文本头取 `SAMPLES/LAST TRACE/CHANNELS/TIMEWINDOW/SOIL VELOCITY/DISTANCE INTERVAL``.iprb` 读 int16 B-scan`samples × traces`,校验 `samples×traces×2 == 文件大小`)。
2. **地理配准**`.ord` 取 14 通道横向偏移;`.gps`/`.cor` 取每道经纬度/RTK深度 = `time × soilVelocity / 2`
3. **GPR→PointSet 适配器**:把"14 通道 × N 道 × 821 采样"摊成 `PointSet{x,y,z,v}`(局部坐标)。**注意横向只有 14 个真实样本**,是稀疏维。
4. **数据集接入**:新增 ddCode`dd_gpr_volume`),在维度分类(`Api3dRepository.cpp:30-45`、`LocalSample3dRepository.cpp:43-58`)归 3DDTO 解析器放 `src/data/dto/`
---
## 3. 关键约束与后果A 的硬边界)
现有管线是 **double + 400³ 上限**。这两条直接决定 A 能做什么:
| 约束 | 数值 | 后果 |
|---|---|---|
| 标量 dtype | `double`8 字节/体素) | 同样体素数,内存是 int16 的 **4 倍** |
| 维度上限 | 400³ | 整卷 ≤ 400³×8 ≈ **512MB**;放不下全路段 |
| 整卷进显存 | 一次性 | 体大小受限于显存 |
| IDW | 单线程 + 无空间索引暴力 | 大点集插值分钟级/卡死(明星路单线 ~5亿采样点级 |
> **`fitAxis` 行为(评审实证 `VolumeBuilder.cpp:16-26`**:格数超 400 时**不裁剪范围**,而是 `outCell=ext/(400-1)` 把 400 格摊满整个包络。所以 A 不是"丢掉远端",而是"强制粗化"——沿线细节被低分辨率抹平。
**全路段在 A 下做不到原始分辨率。** 明星路需 ~22000(沿线)×270(横)×400(深),远超 400³。在 A 下只能:
- **重度降采样到 ≤400³**:沿线 2223m/400 ≈ 5.5m 网格 → 沿线细节全毁;或
- **按单条测线/短段分别建小体**(单线降到 400³ 仍偏粗),多体并排显示(类似现有 2D 足迹平铺)。**但**评审20 体 × 400³ × double ≈ 10GB 整卷同驻显存,比单大体更易爆显存;且各体独立 `GridSpec`/origin**跨体的全路段连续切片做不到**(用户想沿全路一刀切无法实现)。
---
## 4. 持久化(沿用 2026-06-17 §7 策略)
- **必存**`VolumeBuildParams`(源数据引用 + 插值模型/参数 + 色阶)+ `GridSpec`origin/spacing/dims锚定切片/异常坐标)。
- **可选明细**`ScalarVolume`(double)。A 阶段仍是内存 mock`StoredVolume.cachedGrid`**未真实落盘**——这是 A 与用户"保存插值后体"诉求的**主要差距**。
- 用户要的两种保存:①参数 ②插值后明细 —— A 的 ②目前只有内存缓存,需补一段最朴素的 double 体落盘raw + sidecar才算满足但 double 全路段落盘巨大,不实用。
---
## 5. 评估
**优点**
- 改动最小:渲染/切片/异常/详情**全部现成**,只加雷达解析与适配。
- 路径已验证,风险低,可最快出"雷达能进三维场景"的可见效果。
**缺点/限制**
- `double` + 400³ → **撑不起全路段原始分辨率**,只能粗览或分段小体。
- 明细落盘不实用double 体积过大),用户"算一次秒开"诉求难真正成立。
- 单线程 IDW 在雷达量级偏慢。
- 把结构化的雷达数据(沿线/深度本就规则)当无结构散点做 3D IDW**算力浪费**(见方案 B 的结构化插值优化)。
**适用**
- 单条短测线 / 粗分辨率概览 / 快速打通链路的第一步。
- **不适合**作为全路段完整体验的最终方案。
---
## 6. 工作量与落地顺序
1. `.iprb`/`.iprh` 解析 + 地理配准 + GPR→PointSet地基~中)。
2. 雷达 ddCode 接入维度分类 + DTO~小)。
3. 直接复用 `IdwInterpolator`/`buildVoxel`/`SliceTool`,按 ≤400³ 降采样建体(~小)。
4. 可选double 明细落盘最朴素实现(~小,但不推荐用于全路段)。
**结论(修订,评审定)****no-go作为独立交付步并入 B。** A 没有任何 B 不需要的独立资产,渲染/切片/异常/持久化骨架 A、B 共享地基§2也共享。`double`+400³+暴力 IDW 三条硬约束使 A 的 GPR 产物既无业务分辨率、又无法落盘秒开,连"最小基线该兑现的可用产物"都达不到。
- **唯一抽出先做的独立里程碑 = §2 共有地基**`.iprb`/`.iprh` 解析 + 14 通道配准 + GPR→PointSet + ddCode 接入A/B/C 都要。
- "复用 double+400³ 管线建退化体"这一步**不单独交付**,直接在 B 的 int16+结构化建体上落地,避免做一遍注定被 B 推翻的降级体。
- **全路段完整体验走 B。**

View File

@ -0,0 +1,120 @@
# GPR 三维体 · 方案 B全路段 int16 整卷上 GPU升级现有管线推荐
- 日期2026-06-23
- 范围:把全路段 GPR 按**物理分辨率5~10cm**插值成**单个 int16 体(~5~10GB**,整卷传成 GPU 3D 纹理,切片/体绘制都丝滑。**不做金字塔/核外**,靠"右尺寸 + int16"让单体进显存。
- 定位2026-06-23 用户定):**与 C 对等的两条已承诺路线之一,两者都做、用户运行时按需切换**。B 走"整卷进显存"路线(在现有管线上有针对性升级),适合能装进显存的体;超显存的体走 C。经同一 `IVolumeRenderSource` 接口切换。
- **✅ 评审结论2026-06-23opusGo条件式。** 体积测算、int16+结构化插值、渲染/切片复用均成立。**开工前必改 3 项**①落盘方案VTKHDF Writer 写不了 ImageData必须改裸分块见 §3②量化贯穿传递函数/色阶/反量化(见 §3.5);③一个 vtkShortArray→GPU 体绘制的小验证 spike。
- 测试数据(明星路):同方案 A。物理分辨率依据450MHz、土速 0.1m/ns → 波长 λ≈0.22m、垂向分辨率 ≈5cm网格细过 ~5cm 即过采样。
---
## 1. 设计意图
A 的瓶颈是 `double` + 400³ 上限撑不起全路段。B 针对性拆掉这两条,**保持"整卷进显存"这一最省力的渲染架构不变**
```
.iprb/.iprh → 结构化建体(仅横向插值)→ ScalarVolumeI16int16
→ vtkImageData + vtkShortArray → vtkSmartVolumeMapper整卷进显存
切片:复用 SliceToolreslice 对 int16 image 同样工作)
持久化VolumeBuildParams + int16 明细【真实落盘 + 分块压缩】
```
体积测算(明星路全路段,依据真实头文件):
| 网格 (横×纵×深) | 体素数 | int16 体积 | 进显存? |
|---|---|---|---|
| 10cm×10cm×2cm | 22230×270×400 ≈ 2.4G | **4.8GB** | 12GB+ 显卡可 |
| 10cm×10cm×原生821 | ≈ 4.9G | **9.8GB** | 16~24GB 显卡可 |
| 5cm×5cm×5cm | 44460×540×160 ≈ 3.8G | **7.7GB** | 16GB+ 显卡可 |
**关键5~10GB 全部在单显卡可承载区间——不需要金字塔/核外。** 那个 39TB 是 cm 级横向过采样的产物,物理无意义。
---
## 2. 三处核心升级(相对 A
### 2.1 dtype引入 int16 体4× 内存削减)
- 现 `ScalarVolume` 全仓库是 `double``Field.hpp:8-26`),直接改全局风险大。**方案**:新增并行的 `ScalarVolumeI16``std::vector<int16_t>` + 同样行优先布局 + 量化标定 `scale/offset` 把物理值映射到 int16雷达走 int16 路径,反演剖面仍走 double。
- 渲染:`buildVoxel` 增加 int16 重载 → `vtkImageData` + `vtkShortArray`。**评审已证实** GPU 体绘制原生支持 short`vtkSmartVolumeMapper`→`vtkOpenGLGPUVolumeRayCastMapper`→`vtkVolumeTexture` 走 GL 16-bit 整型纹理。NaN/空值改用 int16 哨兵(如 `INT16_MIN`+ 不透明度传递函数透明(与现 `VoxelActor.cpp:23-24,68-72` 同构)。
- 收益:同体素数内存/显存/磁盘 = double 的 1/4是"让全路段进显存"的关键杠杆。雷达原始本就是 int16**无精度损失**。
- **适配面比"加个重载"大(评审 HIGH**`ScalarVolume`(double) 被 `VolumeGrid`/`buildVoxel`/`finalizeVolume`/`Api3dRepository`(`StoredVolume.cachedGrid`、`loadVolume` 回调签名、`VolumeInfo` 统计) 一路引用。int16 体需让这些**要么模板化、要么并行一套带量化 meta 的变体**。隔离方向(雷达 int16 / 反演剖面仍 double但工作量按"中"算偏乐观§6 已上调。
### 2.2 维度上限:由物理分辨率决定,拆掉 400³ 死值
- 移除/放宽 `kMaxVolumeDim=400``VolumeBuilder.hpp:8`),改为按 `cellXY/cellZ` 与场景范围算出 dims并加**显存预算守卫**(建体前估算 `nx·ny·nz·2B`**并留余量**:实际还要叠加传递函数纹理 + 颜色/深度 FBO按裸标量算偏紧——评审 MEDIUM
- **显存探测无可靠跨厂商 API评审 MEDIUM**OpenGL 无统一"可用显存"查询。实践只能 try-upload-on-fail 或留保守阈值。
- **免费兜底评审发现spec 原漏报)**`vtkSmartVolumeMapper` 自带 `MaxMemoryInBytes` + `LowResResample``vtkSmartVolumeMapper.h:194-211,373-379`),体超显存时**自动降采样重采样到可容纳**——等于"概览体"免费实现。目标机显存小时优先用它 + 按区域细化,**仍在 B 框架内,不必转 C**。
- 默认网格由雷达物理分辨率给(横 5~10cm、深 2~5cm不让用户填出过采样网格。
### 2.3 插值:结构化建体,不做 3D 散点 IDW
- **重要架构洞察**:雷达数据沿测线(X)、深度(Z)**本就是规则密采样**,只有横向(Y)的 14 通道是稀疏的。所以"插值成体"≠ 3D 无结构散点插值,而是:
- X、Z 方向按道距/采样直接落格(重采样/最近邻,廉价);
- **只在 Y 方向对 14 通道做 1D 插值**填充横向空隙。
- 这比 A 复用的全 3D IDW **快一两个数量级**且单线程可接受若仍慢Y 向插值天然可并行QtConcurrent/std::thread按 X 切片并行)。
- 保留 `IInterpolator` 抽象,新增 `GprStructuredBuilder` 实现,与 IDW 并列。
---
## 3. 持久化(真正满足"算一次、之后秒开"
用户要两种保存B 把第二种做实:
- **方式一(参数档)**`VolumeBuildParams`(源 .iprb 引用 + 建体参数 + 色阶 + 量化 scale/offset+ `GridSpec`。小、可复算、详情面板展示。
- **方式二(明细缓存,升级为真实落盘)**int16 体 **分块写盘 + 逐块压缩**
- **⚠ 不能用 `vtkHDFWriter`(评审 CRITICAL两个评审独立证实**VTK 9.6 的 `vtkHDFWriter` **写不了 `vtkImageData`/规则体**——它只支持 PolyData/UnstructuredGrid/Partitioned/MultiBlock`vtkHDFWriter.h:6-9,232-235`,无 ImageData 写重载)。`vtkHDFReader` 能**读** ImageData但 Writer 不能**写**,读写不对称。"补个 IOHdf 组件就能 VTKHDF 原生落盘体"的说法**错误**。
- **首选(改正后)****自定义 raw int16 分块 + sidecar(GridSpec/量化 scale·offset/vmin·vmax/分块索引) + 逐块 zlibVTK 自带 `vtkzlib`,无需新依赖)**。分块布局从一开始就设计好C 的"切片核外"可几乎免费复用同一格式。
- 备选:直接用底层 `vtkhdf5` C API 自写 chunked dataset绕过 `vtkHDFWriter`),获得 HDF5 生态兼容;成本高于 raw 分块。
- 不引入独立 zstd/bloscvcpkg 未含;如需更高压缩比再加)。
- **加载**:有明细 → 读盘(可 mmap)→ 整卷上显存;无明细 → 按参数后台线程重算落缓存(复用现有重算逻辑 `Api3dRepository.cpp:212-225`,从 mock 升级为真实落盘)。
- **后台重算不阻塞 UI评审 MEDIUM**:现 `loadVolume` 回调**在主线程**mock 同步)。改为工作线程建体/重算后,回调要**跨线程编组**回 UIQt 信号 / `QMetaObject::invokeMethod`),这是线程模型改动,需显式设计。
- **加载耗时别承诺"秒开"(评审 LOW**5~10GB 上传 GPU(约 1~5s) + 压缩明细解压,实际**约 10s 量级**。明星路单体压缩后约 2~6GB读盘+解压秒~十秒级——比每次重算快得多,用户"算一次之后快读"诉求成立。
### 3.5 量化贯穿(评审 HIGH正确性问题必做
int16 渲染标量是量化域 `q = round((v_phys - offset)/scale)`,不是物理值。必须把量化贯穿全链,否则色阶/读数全错:
- **传递函数 / 不透明度**:现 `VoxelActor.cpp:62-72` 用物理 `vmin/vmax` 加控制点 → int16 路径必须改成在**量化域 `qmin/qmax`** 采样。
- **切片色阶 LUT**`buildLut(cs,vmin,vmax)``SliceTool.cpp:37`)同理喂量化域。
- **反量化显示**:取值光标 / 异常详情 / 数据详情面板展示给用户的值必须 `v_phys = q*scale + offset` 反量化回物理量。
- `scale/offset` 存入 `VolumeBuildParams` 并随 `VolumeInfo` 传递。
---
## 4. 渲染与交互(基本复用,验证为主)
- 整卷 `vtkSmartVolumeMapper`现有int16 image 直接喂;确认 9.6 的 GPU ray cast 对 short 标量正常。
- 切片 `SliceTool``vtkImagePlaneWidget`/`vtkImageReslice`)对 int16 image 同样工作CPU reslice 与 dtype 无关);丝滑度由"整卷已在显存"保证。
- 异常/详情/色阶:复用现有 3D 分析栏链路。
---
## 5. 评估
**优点**
- **全路段完整连续体 + 最好体验**,切片/体绘制丝滑,且**不必上金字塔/核外**。
- 复用现有渲染/切片/异常/详情,主要新增 = int16 路径 + 结构化建体 + 真实落盘。
- int16 + 结构化插值同时解决"内存/显存/磁盘大"和"插值慢"。
- 明细真实落盘,"算一次秒开"成立。
**缺点/风险(评审分级)**
- **CRITICAL已在 §3 修正)**`vtkHDFWriter` 写不了 ImageData → 落盘改裸 int16 分块+zlib。有现成退路非方案推翻但落盘是"自写格式"的中等工程,非"补组件"。
- **HIGH**:量化未贯穿传递函数/色阶/反量化会导致颜色与读数错§3.5 已补设计)。
- **HIGH**int16 适配面被低估(`VolumeGrid`/`loadVolume` 回调/`StoredVolume`/`VolumeInfo` 均需带量化 meta非单点重载。
- **MEDIUM**后台重算从主线程改工作线程跨线程回调编组需设计显存无可靠查询、预算按裸标量偏紧结构化落格假设道近似等距GPS 抖动需沿弧长重采样。
- **LOW**5~10GB 加载约 10s 级UX 别承诺"秒开"。
- 单巨体无部分加载:打开即载全量。
- int16 路径是对核心类型的扩展,需谨慎不污染 double 主路径(用并行类型隔离)。
**适用**
- 当前明星路这一档(及绝大多数单路段工程)。**这是默认推荐。**
---
## 6. 工作量与落地顺序
0. **【开工前】验证 spike~半天)**`vtkShortArray` 填小 `vtkImageData``vtkSmartVolumeMapper` 跑通 GPU ray cast + 量化域传递函数,确认 GPU 路径与颜色正确。
1. 地基(同 A §2`.iprb`/`.iprh` 解析 + 配准 + 接入(~中)。
2. `ScalarVolumeI16` + `buildVoxel` int16 重载 + 哨兵透明 + **量化贯穿传递函数/LUT/反量化§3.5**~中大,评审上调:适配面比单点重载大)。
3. `GprStructuredBuilder`X/Z 落格 + Y 向插值可并行GPS 抖动需沿弧长重采样)替代全 3D IDW~中)。
4. 显存预算守卫(留 FBO/传函余量)+ `LowResResample` 概览兜底 + 物理分辨率默认网格(~小)。
5. 明细真实落盘(**raw int16 分块 + sidecar + zlib不用 vtkHDFWriter**+ 后台重算(**含跨线程回调编组**~中大,评审上调:自写分块格式 + 线程模型)。
6. 渲染/切片对 int16 的验证(~小)。
**结论评审Go 条件式 / 用户定:与 C 都做)**B 用"右尺寸 + int16 + 结构化插值"在现有架构上拿到全路段完整体验。**前置条件**§3 落盘章节已从 VTKHDF 改为裸分块、§3.5 量化设计已补、第 0 步 spike 通过——满足后即可进入实现。B 与 C 经同一 `IVolumeRenderSource` 并存,用户按数据规模在两者间切换(能进显存走 B、超显存走 C落盘格式两者共用B 的裸分块是 C 分块/金字塔的基座)。

View File

@ -0,0 +1,108 @@
# GPR 三维体 · 方案 C分块 + 金字塔 + 核外(应对超大数据量)
- 日期2026-06-23
- 范围:当单个三维体在**合理分辨率下仍超显存/内存**时(几十公里测线、多工区合并、或必须超精网格),采用业界处理 TB 级体的标准架构:**分块(bricking) + 多分辨率金字塔(LOD) + 逐块压缩 + 核外按需加载(out-of-core)**。
- 定位2026-06-23 用户定):**与 B 对等的两条已承诺路线之一,两者都做、用户运行时按需切换**(不是 B 的兜底/预案)。对标地震(OpenVDS/ZGY)、数字病理/显微(OME-Zarr)。B 适合能整卷进显存的体、C 适合超显存/超大范围的体,由用户按数据选择,经同一 `IVolumeRenderSource` 接口切换。
- POC用户定C 的 POC **含"最小但真实的核外分页器"**,正面验证最高风险点(分页器在 VTK 上可行性、块边接缝、LOD 闪烁、热路径解压),"POC 过 ⇒ 可落地"。
- 前置:地基(`.iprb` 解析/配准/接入) 与 int16 + 结构化建体 与方案 B 共用。
- **⚠ 评审结论2026-06-23opus+ 用户决策****Go——C 是已承诺路线,与 B 都做。** 架构对标业界无误。开工注意:①`vtkHDFWriter` 写不了规则体(与 B 同源 CRITICAL§2.2 已改正为裸 HDF5/分块);②整卷核外分页器无 VTK 开箱基础CRITICAL月级**POC 即用最小真实分页器正面验证**);③`vtkSmartVolumeMapper` 自带 `LowResResample` 仅作 C 内的降质兜底手段,不替代 C。落地顺序裸分块格式 → 切片核外 → 整卷核外分页器。
---
## 1. C 的适用场景(用户在 B/C 间按需选择的依据,非"门槛"
C 与 B 并存,用户对某个体选 C 而非 B典型是
- 合理分辨率(5~10cm)下单体 int16 体积 **超过本机显存**
- 测线长一个数量级(几十 km或多工区拼接成连续大体
- 需要在内存/显存恒定下浏览任意大的体。
B 与 C 同为成品、经同一 `IVolumeRenderSource` 切换;小体走 B整卷最省力、大体走 C核外不爆内存**由用户按数据选**。
---
## 2. 架构
```
建体(int16) → 分块(brick 64³/128³) → 逐块压缩 + 每块 min/max
→ 多分辨率金字塔(全分辨率 / 1/2 / 1/4 / 1/8 …)
→ 写入分块格式文件(离线/后台一次)
渲染 → 核外分页器:按相机视野 + LOD 选块 → 解压载入显存 → 相机移动换入换出
内存/显存只驻留当前所需块(数 GB与总体积无关
切片 → 只读切面相交的块(最便宜的子集);等值面靠每块 min/max 剔除
交互 → 拖动用粗 LOD停下加载全分辨率沿拖动方向预取
```
### 2.1 存储格式(分块 + 金字塔 + 压缩)
- **⚠ 不能依赖 `vtkHDFWriter`(评审 CRITICAL与 B 同源)**VTK 9.6 的 `vtkHDFWriter` **写不了 `vtkImageData`/规则体**`vtkHDFWriter.h:6-9,232-235`,仅 PolyData/UnstructuredGrid/composite且**无规则体的多分辨率 overview**(头文件唯一多级机制是 AMR 层级 `vtkHDFReader.h:203-209`,非金字塔)。所以"补 IOHdf 组件即可 VTKHDF 落盘 + overview"**不成立**——原 spec 的"待验证"实为"基本不支持"。
- **首选(改正后)****裸 `vtkhdf5` C API 自写 chunked dataset**HDF5 原生 chunking + zlib 逐块压缩 + 随机访问),金字塔层作为多个 HDF5 dataset **自行组织**;或自定义 brick 文件(裸分块 + 索引),每块 zlib + 头存 min/max + LOD 偏移表。**与 B 的落盘层统一**B 本就要改成裸分块,正好一并设计成可分块/可多级)。
- 不引入独立 zstd/bloscvcpkg 未含);如压缩比不足再评估加入。
### 2.2 渲染端核外分页C 的真正难点)
- **VTK 不开箱提供大体的 out-of-core GPU 体绘制(评审证实)。** 注意两个相关但不够用的开箱件:
- `vtkMultiBlockVolumeMapper` **存在但不是分页器**`vtkMultiBlockVolumeMapper.h:14-21`:试图"同时加载所有块",仅 GPU 分配失败才退化逐块重载)——它给的是"多块同时渲染 + 抖动抗块边接缝"**没有按视野换入换出/LOD 选块/预取**。要复用它做渲染层,须在喂数据前自己完成"只放视野块"的筛选。
- `vtkSmartVolumeMapper``LowResResample`(见 B §2.2)是"自动降质看全貌"**不是核外**——它是 C 之前的免费兜底,不是 C 的实现。
- 整卷核外分页器须**从零自建**(选块/LOD/LRU/解压/换入换出/预取)。三条路:
1. **切片优先(推荐先做)**:切片只需读相交的块,复用 `vtkImageReslice` 对"当前块集合"重采样。**这条最易落地**,能先拿到"超大体看切片"的能力,不碰整卷核外。
2. **自建 LOD + brick 分页**:在 `vtkSmartVolumeMapper` 之上,把视野内块按 LOD 作为多个 `vtkImageData`/`vtkMultiBlockDataSet` 动态加载/淘汰。整卷透明体绘制走这条,**工作量最大**。
3. **集成 OpenVDS**(地震库):能力最全但**重依赖**vcpkg 未含,需自带 + 适配 VTK适配成本高。
- 建议:**先做 1切片核外整卷体绘制核外(2)列为后续**。
### 2.3 建体/金字塔流水线
- 复用方案 B 的 int16 + 结构化建体产出全分辨率体 → 分块 → 逐级降采样建金字塔 → 逐块压缩落盘。
- **离线/后台执行一次**结果即持久化产物C 的"保存插值后体"天然就是这个分块金字塔文件)。
---
## 3. 持久化
- C 的存储格式**本身就是方式二(明细缓存)**:分块 + 金字塔 + 压缩,是"算一次、之后秒开"的载体,且读取只碰视野块、内存可控。
- 方式一(参数档 `VolumeBuildParams`+`GridSpec`)仍保留,用于复算/详情/校验。
- 切片/异常坐标仍锚定 `GridSpec`(与 A/B 一致),保证跨 LOD 一致。
---
## 4. 评估
**优点**
- **不设规模上限**:任意大小(几十 GB ~ TB皆可内存/显存恒定在数 GB。
- 切片只读相交块、等值面块级剔除、拖动 LOD 降级 —— 大体交互可做到流畅。
- 与业界(地震/医学)成熟路径同构,可借鉴现成设计。
**缺点/风险(评审分级)**
- **CRITICAL**`vtkHDFWriter` 写不了规则体 → 落盘须自写裸 HDF5/分块§2.1 已改),是中大件,非"补组件"。
- **CRITICAL**:整卷核外分页器**无 VTK 开箱基础**,从零自建(选块/LOD/LRU/预取),月级且风险集中;或集成重依赖 OpenVDS。
- **HIGH**VTKHDF 无规则体多分辨率 overview金字塔须全自管。
- **HIGH**:压缩块解压在**交互热路径**拖动每帧换块解压CPU 可能成瓶颈——不止 IO 放大。
- **MEDIUM**LOD 降采样的半像素偏移 → 异常拾取**跨层落点漂移**"GridSpec 锚定保证一致"未覆盖此情形)。
- **MEDIUM**块边接缝MultiBlock 抖动可部分缓解)/ LOD 切换闪烁(需 morphing/淡入自解决)。
- 复杂度高 → 维护成本与缺陷面大。
- **依赖断点(评审)**C 声称"复用 B 的落盘",而 B 那层须先从 VTKHDF 改成裸分块 HDF5——C 的"地基已就绪"前提依赖 B 先这么做。
**适用**
- 仅当数据真正超出方案 B 的单显卡承载。**当前明星路用不到。**
---
## 5. 工作量与落地顺序(仅在需要时启动)
1. 地基 + int16 + 结构化建体(与 B 共用,若已做则复用)。
2. 分块格式 + 逐块压缩 + 每块 min/maxVTKHDF 或自定义)(~中大)。
3. 多分辨率金字塔生成流水线(后台一次)(~中)。
4. **切片核外**:按切面读相交块重采样(~中)—— 先交付这条。
5. 整卷体绘制核外分页器 + LOD 拖动降级 + 预取(~大,后续)。
6. 显存/内存缓存与淘汰策略(~中)。
---
## 6. 与 A/B 的关系
- 能力A ⊂ B ⊂ C成本同序递增。
- A、B 共享渲染/切片现有架构;**C 是不同的存储+渲染架构**(分块+核外),是 B 撞到显存天花板后的演进,不是平行替代。
- **推荐策略**:现在做 B 满足明星路与多数工程;把 C 的"分块格式 + 切片核外"作为**预案**,待出现真正超大数据再启动;整卷体绘制核外列为最后一档。
> **切片核外"最易落地"有隐藏前提(评审)**:它要求分块格式**已做完**才能"读相交块"。所以"先交付切片核外"≠ 可跳过分块——§5 顺序(先分块格式再切片核外)正确,别误读为切片核外是独立小工程。
**结论用户定GoC 与 B 都做)**C 的总体架构成立、对标 OpenVDS/ZGY/OME-Zarr 无误,是与 B 对等的已承诺成品;工程最重、渲染端整卷核外非开箱、落盘须裸 HDF5已改
- **落盘与 B 统一**:裸分块格式(不依赖 vtkHDFWriterB 落盘本就改裸分块C 的分块/切片核外在同一格式上增量获得。
- **整卷核外分页器**是 C 的最高风险件(两个 CRITICAL + 热路径解压 HIGH月级**POC 即以"最小真实分页器"正面验证**,确保"过了能落地"。
- **B/C 切换**:经 `IVolumeRenderSource` 运行时切换,用户按数据选;`LowResResample` 是 C 内的降质手段,不替代 C 也不替代用户选择。