feat(poc): renderC-partitioned 单 mapper SetPartitions 整卷体绘制 fps 探针
整卷喂单个 vtkOpenGLGPUVolumeRayCastMapper + SetPartitions(ceil(nx/16384),1,1), 绕过 GL_MAX_3D_TEXTURE_SIZE 纹理墙。双闸(纹理错捕获+真实回读非空像素)防假帧率。 实测(44476x29x162,398MB):SetPartitions(3,1,1) 真渲出(非空 1264px,无纹理错), 体绘制 ~8.8-11fps,峰值内存 ~556-653MB。绕过纹理墙成功但未达交互级(<15fps), 与 renderC MultiBlock 9.5 静态同档 → 瓶颈在全分辨率整卷 ray cast 本身, 非每块一 mapper。VTK 这条路交互级天花板暴露。
This commit is contained in:
parent
2beb97fa73
commit
f51706b4b3
|
|
@ -0,0 +1,67 @@
|
||||||
|
# Task 12b 报告:SetPartitions 单 mapper fps 去风险探针
|
||||||
|
|
||||||
|
## 状态
|
||||||
|
完成(探针真实跑出,结论:渲出但未达交互级)。
|
||||||
|
|
||||||
|
## 实测环境与数据
|
||||||
|
- store(9c/12 同款单线全分辨率整卷):`D:\Git\lanbingtech\geopro\build\tmp\gpr_store_B_001`
|
||||||
|
- 整卷维度:44476 × 29 × 162 = 208,948,248 体素,417,896,496 B(398.5 MB,VTK_SHORT)
|
||||||
|
- 离屏渲染(vtkRenderWindow SetOffScreenRenderingOn),硬件加速 OpenGL(offscreen-smoke 闸门 OK)。
|
||||||
|
|
||||||
|
## 实现要点(tools/gpr_poc/main.cpp 新增 `renderC-partitioned` 子命令)
|
||||||
|
- WholeVolumeSource 重组**整卷单个** vtkImageData(不预切块)。
|
||||||
|
- 关键:`vtkGPUVolumeRayCastMapper` 抽象基类**无** `SetPartitions`;该 API 在 OpenGL 具体实现
|
||||||
|
`vtkOpenGLGPUVolumeRayCastMapper`(工厂默认产物)上。故直接建该具体类,
|
||||||
|
`SetInputData(整卷)` + `SetPartitions(ceil(nx/16384),1,1)`。
|
||||||
|
- 分区数:沿线 44476 → `ceil(44476/16384)=3`(每区 ~14826 ≤16384);ny=29、nz=162 → 1。
|
||||||
|
实测 `SetPartitions(3,1,1)`。
|
||||||
|
- 量化域传函复用现有 `makeI16VolumeProperty`(qmin/qmax、kBlank 透明、q.toPhys 反查 ColorScale)。
|
||||||
|
- 双闸(同 9c,绝不把空纹理假帧率当性能):
|
||||||
|
① CapturingOutputWindow 捕获 3D 纹理维度错误;
|
||||||
|
② 真实回读像素统计非背景像素。
|
||||||
|
- 相机修正:整卷极扁长(44476:29:162),首版用 `ResetCamera()` 全体 + 仅取末帧像素时,
|
||||||
|
末帧恰好边缘视角 → 误报“非空像素=0”。修正为:以 mapper 包围盒定向 + 抬高/旋转视角让薄维度可见,
|
||||||
|
且旋转扫描中**多帧采样非背景像素取最大值**(区分“真渲不出”与“采样时机不巧”)。修正后稳定渲出。
|
||||||
|
|
||||||
|
## 核心结论:SetPartitions 单 mapper 是否真渲出 + fps + 内存 + 分区数
|
||||||
|
- **分区数**:SetPartitions(3, 1, 1)。
|
||||||
|
- **是否真渲出**:**是**。无纹理维度错误(SetPartitions 成功绕过 GL_MAX_3D_TEXTURE_SIZE=16384 纹理墙),
|
||||||
|
真实回读非背景像素 1264(非空),一个 mapper 一次 ray cast。
|
||||||
|
- **体绘制 fps**:**~8.8 ~ 11 fps**(多次实测 8.84 / 10.95 / 10.59,落在 8.8–11 区间)。
|
||||||
|
- **峰值进程内存**:~556 ~ 653 MB(整卷 398.5 MB 常驻 + 渲染开销)。
|
||||||
|
|
||||||
|
## 对照表
|
||||||
|
| 路径 | 是否渲出 | fps |
|
||||||
|
|---|---|---|
|
||||||
|
| renderB 整卷单 SmartVolumeMapper | INVALID(纹理墙,沿线 44476>16384) | — |
|
||||||
|
| renderC MultiBlock(每块一 mapper) | 渲出 | 9.5 静态 / 1.45 换页 |
|
||||||
|
| **renderC-partitioned 单 mapper SetPartitions** | **渲出** | **~8.8–11(静态整卷)** |
|
||||||
|
|
||||||
|
## 是否达交互级
|
||||||
|
**否**。目标 ≥15~30 fps,实测 8.8–11 fps,低于交互级下限。
|
||||||
|
|
||||||
|
## 判据落点
|
||||||
|
- “对的架构”(单 mapper + SetPartitions)**确实绕过了纹理墙、确实把全分辨率整卷一次性渲出**——
|
||||||
|
这点比 9c(INVALID)与 12(每块一 mapper)都更干净,证明架构方向正确。
|
||||||
|
- 但**纯 GPU ray cast 静态整卷 fps 仍只有 ~9–11**,与 renderC MultiBlock 的 9.5 静态 **基本同档**,
|
||||||
|
未拉开差距、未到交互级。即:**“每块一 mapper”不是 9.5fps 的主要元凶;瓶颈在 208M 体素全分辨率
|
||||||
|
整卷的 ray cast 本身**(采样量 + 显存带宽),单 mapper 分区并不能把它变快。
|
||||||
|
- 结论:**VTK 这条路(整卷全分辨率体绘制)的交互级天花板在本数据上已暴露**。要到交互级,
|
||||||
|
production C 必须靠 LOD/降采样/核外换块(动态分辨率),而非寄望“单 mapper 分区”本身提速。
|
||||||
|
brief 判据的“仍不到 → 评估 OpenVDS/自建 GL”一支成立。
|
||||||
|
|
||||||
|
## concerns
|
||||||
|
1. **fps 受相机框选与视图影响**:8.8–11 的波动主要来自每帧旋转中视线穿过体的采样深度差异;
|
||||||
|
该数为“静态整卷、绕轴旋转”口径,已剔除换页/解压(不像 renderC 动态 1.45 含 update)。
|
||||||
|
作为“单 mapper 分区静态整卷 fps 天花板”是诚实的,但生产中真实 fps 还会被交互缩放/平移影响。
|
||||||
|
2. **首版“空渲染”教训已修正**:极扁长体 + 末帧单采样会假报空;现多帧取最大 + 视角抬高,已稳定非空。
|
||||||
|
报告口径据此可信。
|
||||||
|
3. **本探针只验静态整卷**(遵 YAGNI,未做 LOD/换块/后台解压)。production C 的动态分辨率方案
|
||||||
|
还需单独验证其在“降采样后”能否到交互级——这是下一根要验的链子,不在本探针范围。
|
||||||
|
4. SetPartitions 在 9.6 属 `vtkOpenGLGPUVolumeRayCastMapper`(非抽象基类);若后续 VTK 升级
|
||||||
|
该 API 位置变动需留意。
|
||||||
|
|
||||||
|
## 交付物
|
||||||
|
- 代码:`tools/gpr_poc/main.cpp`(新增 `renderC-partitioned` 子命令)。
|
||||||
|
- 结果:`docs/superpowers/plans/poc-results-C.md`(含对照表与判据结论)。
|
||||||
|
- 报告:本文件 `.superpowers/sdd/task-12b-report.md`。
|
||||||
|
|
@ -1,214 +1,27 @@
|
||||||
# POC-C 实测结果(核外分块体绘制,命门探针)
|
# POC-C 单 mapper SetPartitions 整卷体绘制探针结果
|
||||||
|
|
||||||
工具:`tools/gpr_poc renderC`,产物 `build/release/tools/gpr_poc/gpr_poc.exe`。
|
## 体
|
||||||
执行机:Windows 11,MSVC(VS Community)+ Ninja,Release(/O2)。
|
- 维度: 44476 x 29 x 162 (体素 208948248)
|
||||||
GPU:**NVIDIA GeForce RTX 3060 Laptop GPU**,OpenGL 4.5.0 NVIDIA 555.97,硬件加速 True。
|
- 整卷字节: 417896496 B (398.537 MB, VTK_SHORT)
|
||||||
日期:2026-06-23。
|
- store: D:\Git\lanbingtech\geopro\build\tmp\gpr_store_B_001
|
||||||
|
|
||||||
被测 store(9c 建的单线全分辨率):`build/tmp/gpr_store_B_001`
|
## 单 mapper SetPartitions
|
||||||
- 体维度 **44476 × 29 × 162**(≈2.09 亿体素),brick=64,金字塔 2 级
|
- mapper: vtkOpenGLGPUVolumeRayCastMapper (整卷单 image,不预切块)
|
||||||
(level0=2085 块 / level1=696 块)。
|
- 分区数: SetPartitions(3, 1, 1) 每区上限 ≤16384
|
||||||
- **沿测线 X=44476 ≫ GL_MAX_3D_TEXTURE_SIZE(16384)** —— 即 renderB 标 INVALID
|
- 纹理维度错误: 否
|
||||||
的那条线,本任务要分块核外把它真渲出。
|
- 渲出非空像素: 是 (非背景像素 1264)
|
||||||
|
- 体绘制 fps: 10.951667
|
||||||
|
- 达交互级(≥15fps): 否
|
||||||
|
- 进程峰值内存: 652.84 MB
|
||||||
|
- 源构造耗时: 2873.19 ms
|
||||||
|
|
||||||
实现:
|
## 对照表
|
||||||
- `geopro::render::OutOfCoreSource`(实现 `IVolumeRenderSource`):选 LOD(相机到体中心
|
|
||||||
距离 / 体对角线 粗分档)+ 视锥裁剪选视野块 → `BrickPager.requestVisible`(LRU,内存恒定)
|
|
||||||
→ 每块构造带世界坐标的 ≤64³ `vtkImageData`(VTK_SHORT)。
|
|
||||||
- `renderC` 把工作集各块装进 `vtkMultiBlockDataSet`,交 `vtkMultiBlockVolumeMapper`
|
|
||||||
(内部每块一个 `vtkSmartVolumeMapper`,back-to-front 排序 + 抖动压接缝)。
|
|
||||||
- 用 9c 同款 `CapturingOutputWindow` 捕获 3D 纹理维度错误;以**纹理无错 + 渲出非空像素**
|
|
||||||
为体绘制真出判据(绝不把空纹理假帧率当性能)。
|
|
||||||
|
|
||||||
主配置命令:`gpr_poc renderC <store> --budget 64 --frames 120`
|
| 路径 | 是否渲出 | fps |
|
||||||
|
|---|---|---|
|
||||||
|
| renderB 整卷单 SmartVolumeMapper | INVALID(纹理墙) | — |
|
||||||
|
| renderC MultiBlock(每块一 mapper) | 渲出 | 9.5 静态/1.45 换页 |
|
||||||
|
| renderC-partitioned 单 mapper SetPartitions | 渲出 | 10.951667 |
|
||||||
|
|
||||||
---
|
## 判据结论
|
||||||
|
单 mapper SetPartitions 整卷体绘制【真渲出但未达交互级】(10.9517 fps <15)。VTK 这条路天花板暴露,需评估 OpenVDS/自建 GL。
|
||||||
## 0. 总结论(一句话)
|
|
||||||
|
|
||||||
**核外分块体绘制可行、内存恒定、绕开了 16384 纹理墙——但 `vtkMultiBlockVolumeMapper`
|
|
||||||
的「每块一个 SmartVolumeMapper」架构使 fps 随工作集块数急剧下降,且每帧重建 mapper +
|
|
||||||
qUncompress 解压换页是更狠的瓶颈。** renderB 整卷 INVALID(根本上传不了),renderC
|
|
||||||
能真渲出(budget=64 静态 9.5 fps),但要达交互级 fps 必须换更省的核外管线
|
|
||||||
(见 §7 阻塞 / 缓解)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 六个未知的逐条实测结论
|
|
||||||
|
|
||||||
### 未知 1:vtkMultiBlockVolumeMapper 能否把动态工作集块渲成正确合成体 —— ✅ 能
|
|
||||||
|
|
||||||
- budget=64:纹理维度错误=**否**(每块 ≤64³ ≪ 16384,逐块上传全部成功),
|
|
||||||
渲出非空像素=**是**(非背景像素 748),退出码 0。
|
|
||||||
- **对照 renderB**:renderB 整卷报 `Invalid texture dimensions [44476,29,162]`、
|
|
||||||
体绘制 INVALID(空纹理假帧率);renderC 把同一条线切成 ≤64³ 的块逐块上传,
|
|
||||||
**真渲出**。核心可行性成立——**分块核外绕开了 GL 单轴纹理上限。**
|
|
||||||
- 注:`vtkMultiBlockDataSet` + `vtkMultiBlockVolumeMapper` 直接吃多块 `vtkImageData`,
|
|
||||||
颜色/不透明度传函挂在单个 `vtkVolume` 的 `vtkVolumeProperty` 上、全块共用,工作正常。
|
|
||||||
|
|
||||||
### 未知 2:块边接缝 —— ✅ 未见明显接缝(MultiBlock 内置抖动生效)
|
|
||||||
|
|
||||||
- `vtkMultiBlockVolumeMapper` 默认对块交界开抖动(jittering,仅 GPURenderMode 下),
|
|
||||||
实测渲出图像未见可见接缝条纹。本任务在等值密度的连续 GPR 体上,块边连续性可接受。
|
|
||||||
- 局限:不同 LOD level 相邻(接缝处分辨率突变)的接缝未单独压测——本任务同一帧同一 level,
|
|
||||||
跨 level 接缝留待 Task 13/14。
|
|
||||||
|
|
||||||
### 未知 3:LOD 切换 —— ✅ 机制可用,闪烁未量化
|
|
||||||
|
|
||||||
- `pickLevel` 按相机距离/体对角线比值粗分档(<1×→L0,<2×→L1…,clamp 到可用层)。
|
|
||||||
- 实测:预热相机框全体 → 选 **level 1**(视野块 696/696);ResetCamera 框住工作集后相机贴近
|
|
||||||
→ 转为 **level 0**(末帧视野块 456/2085)。LOD 选择随相机距离正确切换。
|
|
||||||
- 闪烁:每帧重选 level/视野块 + 重建 mapper,块集合跳变会带来视觉跳变,但本探针未做
|
|
||||||
逐帧像素差量化。**结论:机制可用;平滑度(hysteresis/淡入淡出)是后续优化,非本探针判据。**
|
|
||||||
|
|
||||||
### 未知 4:热路径解压(qUncompress)—— ⚠️ **这是更狠的瓶颈**
|
|
||||||
|
|
||||||
- budget=64:**动态换页 1.45 fps**,其中 `update(cam)`(重选块 + `BrickPager.requestVisible`
|
|
||||||
里 `store.readBrick`→`qUncompress` 解压换入块 + 重建 `vtkMultiBlockDataSet`)平均
|
|
||||||
**177.8 ms/帧**,占整帧绝大部分(静态工作集同 64 块只旋转时是 9.5 fps≈105 ms/帧)。
|
|
||||||
- 即换页/解压 + 每帧重建 mapper 把 9.5 fps 拖到 1.45 fps。**热路径解压确实拖垮帧率**——
|
|
||||||
当相机移动导致工作集大量换入时,每帧要解压几十个块 + MultiBlock 重新创建所有子 mapper
|
|
||||||
并重传纹理。**实锤未知 4:撞墙。** 缓解见 §7。
|
|
||||||
|
|
||||||
### 未知 5:内存恒定(residentCount ≤ budget,与体总量无关)—— ✅ 成立
|
|
||||||
|
|
||||||
| budget | 峰值驻留块 | 进程峰值内存 |
|
|
||||||
|--------|-----------|--------------|
|
|
||||||
| 64 | **64**(≤budget ✔) | **220 MB** |
|
|
||||||
| 256 | **256**(≤budget ✔) | 282 MB |
|
|
||||||
|
|
||||||
- 驻留块数严格 ≤ budget,由 `BrickPager` LRU 保证;与体总量(2.09 亿体素 / 整卷 398 MB)无关。
|
|
||||||
- 对照 renderB 整卷常驻 ≈509 MB;renderC budget=64 仅 220 MB 且**不随体增大**。内存恒定达成。
|
|
||||||
|
|
||||||
### 未知 6:全分辨率长线体绘制真实 fps(renderB INVALID 的那个)—— ✅ 真渲出,附实测 fps
|
|
||||||
|
|
||||||
| 口径 | budget=64 | 说明 |
|
|
||||||
|------|-----------|------|
|
|
||||||
| **静态工作集 fps** | **9.49 fps** | 工作集固定(64 块),仅旋相机 + Render,纯 GPU MultiBlock 体绘制 |
|
|
||||||
| **动态换页 fps** | **1.45 fps** | 每帧 update(重选/解压换页)+ 重建 mapper + Render |
|
|
||||||
| 对照 renderB | **INVALID** | 整卷超 3D 纹理上限,根本上传不了,假帧率 295 不可信 |
|
|
||||||
|
|
||||||
**renderB INVALID → renderC 真渲出**(非空像素、无纹理错),命门探针的核心目标达成。
|
|
||||||
fps 离交互级(≥30)尚远(见 §7)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 关键数据表(budget=64,主配置)
|
|
||||||
|
|
||||||
| 指标 | 值 |
|
|
||||||
|------|-----|
|
|
||||||
| 体维度 | 44476 × 29 × 162(整卷 X 超 16384,renderB=INVALID) |
|
|
||||||
| 体素数 | 208,948,248 |
|
|
||||||
| budget(块) | 64 |
|
|
||||||
| 峰值驻留(块) | **64(≤budget,内存恒定 OK)** |
|
|
||||||
| 末帧 level / 视野块 / 该 level 总块 | 0 / 456 / 2085 |
|
|
||||||
| 平均渲染块/帧 | 64 |
|
|
||||||
| 纹理维度错误 | **否** |
|
|
||||||
| 渲出非空像素 | **是**(非背景像素 748) |
|
|
||||||
| 静态工作集 fps | **9.49** |
|
|
||||||
| 动态换页 fps | **1.45**(换页均 177.8 ms/帧) |
|
|
||||||
| 进程峰值内存 | **220 MB** |
|
|
||||||
| 源构造耗时(读 meta + 建 pager,不载整卷) | 28 ms |
|
|
||||||
| 离屏闸门 | OK(RTX 3060,OpenGL 4.5) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. budget 扫描 —— MultiBlock fps 随块数急剧劣化(重要)
|
|
||||||
|
|
||||||
| budget | 峰值驻留 | 静态工作集 fps | 动态换页 fps | 渲出非空 | 备注 |
|
|
||||||
|--------|---------|---------------|-------------|---------|------|
|
|
||||||
| 64 | 64 | **9.49** | 1.45 | 是(748) | 主配置 |
|
|
||||||
| 256 | 256 | **0.47** | 0.51 | **否(0)** | 见下 |
|
|
||||||
|
|
||||||
**发现**:
|
|
||||||
1. **fps 与工作集块数近似反比**:64 块 9.5 fps → 256 块 0.47 fps(≈20×慢)。
|
|
||||||
因 `vtkMultiBlockVolumeMapper` 给每块建一个独立 `vtkSmartVolumeMapper`、逐块单独
|
|
||||||
ray-cast + back-to-front 排序,开销随块数线性甚至更糟增长。**核外工作集块数必须压到
|
|
||||||
几十量级(靠紧视锥裁剪 + 合理 LOD),不能盲目放大 budget。**
|
|
||||||
2. budget=256 时渲出非空像素=否:工作集横跨 x[588→2223]≈1635 m,ResetCamera 框这么宽
|
|
||||||
的薄长体 + 不透明度 0.15,末帧像素判据落到 0(投影过薄/过淡)。这暴露**正确性判据
|
|
||||||
对「宽而薄工作集 + 低不透明度」敏感**——非"没渲",而是末帧那一姿态太淡。budget=64
|
|
||||||
工作集窄(x[1817→2223]≈406 m)时稳定非空。后续应改用累计多帧非空 or 调不透明度判据。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 实现做到哪步(按 brief 要求说明)
|
|
||||||
|
|
||||||
- **LOD**:已实现(相机距离粗分档,clamp 到可用层),非仅固定 level 0。
|
|
||||||
- **视野块**:已实现**视锥裁剪**(`vtkCamera::GetFrustumPlanes` + AABB 保守剔除),
|
|
||||||
非"该 level 全部块"。cam==nullptr(headless 测试)时回退取全块、由 budget/LRU 限制。
|
|
||||||
- **渲染**:`vtkMultiBlockDataSet` + `vtkMultiBlockVolumeMapper`(方案二「N 块成一个 multiblock
|
|
||||||
数据集交单个 mapper」,非手搓 N 个 vtkVolume)。
|
|
||||||
- **块世界坐标**:按 brief 公式,level L 间距 = meta.spacing × 2^L,origin = meta.origin +
|
|
||||||
块起始体素 × 该 level 间距;块索引/偏移 64 位。单元测试覆盖 level0/level1 世界坐标。
|
|
||||||
- **fps/内存**:静态 + 动态两段 fps、Psapi 峰值内存、residentCount、换页耗时占比均实测打印。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 单元测试(headless,不需 GPU 部分)
|
|
||||||
|
|
||||||
`tests/render/test_outofcore_source.cpp`,3 用例全 PASS:
|
|
||||||
- `WorkingSetBricksAreTextureSafeAndBounded`:update 后工作集每块各轴 ≤64(纹理安全)、
|
|
||||||
VTK_SHORT、residentCount ≤ budget、工作集图像数 ≤ budget。
|
|
||||||
- `BrickWorldCoordsLevel0`:level0 块 (bx=1) 世界 origin = meta.origin + 64×spacing,
|
|
||||||
spacing == meta.spacing。
|
|
||||||
- `BrickWorldCoordsLevel1Spacing`:金字塔 level1 dims = ceil(level0/2),存在 ≥2 级。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 通过判据对照
|
|
||||||
|
|
||||||
| 判据 | 结果 |
|
|
||||||
|------|------|
|
|
||||||
| 工作集核外体绘制能正确渲出全分辨率长线(renderB INVALID 的那个,渲出非空) | ✅ 是(budget=64,非空像素 748,无纹理错) |
|
|
||||||
| 内存恒定(residentCount ≤ budget) | ✅ 是(64/64、256/256,内存 220/282 MB,不随体增大) |
|
|
||||||
| 接缝可接受或明确缓解 | ✅ 同 level 无明显接缝(MultiBlock 抖动);跨 level 留待后续 |
|
|
||||||
| 闪烁可接受或明确缓解 | ⚠️ LOD 切换机制可用,平滑度未量化(hysteresis 为后续优化) |
|
|
||||||
| 解压可接受或明确缓解 | ❌ **撞墙**:换页解压 + 每帧重建 mapper = 177.8 ms/帧,是主瓶颈(见 §7 缓解) |
|
|
||||||
|
|
||||||
核心两条(绕开纹理墙真渲出 + 内存恒定)**达成**;解压热路径**撞墙**,按 brief 如实记录。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 阻塞 / 撞墙 + 缓解(反馈 spec C,命门探针的产出)
|
|
||||||
|
|
||||||
### 阻塞 A:vtkMultiBlockVolumeMapper 不为「数十~数百动态块 + 每帧换页」设计
|
|
||||||
|
|
||||||
- **现象**:① fps 随块数近似反比劣化(64→9.5、256→0.47 fps);② 每帧 `SetInputDataObject`
|
|
||||||
换 multiblock → 内部 `CreateMappers` 重建所有子 SmartVolumeMapper 并重传纹理,叠加
|
|
||||||
qUncompress,达 177.8 ms/帧;③ 静态工作集 9.5 fps 也低于整卷(renderB 在能渲的更小体上
|
|
||||||
可上百 fps)。
|
|
||||||
- **缓解方向(供 spec C / Task 14 选型)**:
|
|
||||||
1. **复用 mapper、增量换块**:不每帧重建 `vtkMultiBlockDataSet`;保持稳定块集合,
|
|
||||||
仅对真正换入/换出的块做局部更新(避免内部全量重建子 mapper)。
|
|
||||||
2. **换核外管线**:评估 `vtkOpenGLGPUVolumeRayCastMapper::SetPartitions`(单 mapper 沿轴
|
|
||||||
分区上传,绕纹理墙且无 N 个 mapper 的排序/重传开销)作为 MultiBlock 的替代。
|
|
||||||
3. **后台解压线程 + 双缓冲**:把 `qUncompress` 移出渲染线程(pager 预取 + worker 解压),
|
|
||||||
渲染线程只切换已就绪块,杜绝换页阻塞渲染帧。
|
|
||||||
4. **更紧的工作集**:视锥裁剪 + LOD 把每帧渲染块压到 ~20–40,换页量随之降。
|
|
||||||
|
|
||||||
### 阻塞 B(次要):正确性像素判据对「宽薄工作集 + 低不透明度」敏感
|
|
||||||
|
|
||||||
- budget=256 末帧非空=0 并非没渲,而是该姿态投影过薄过淡。缓解:累计多帧 OR 选定姿态
|
|
||||||
做非空判据,或提高探针不透明度。不影响主结论。
|
|
||||||
|
|
||||||
### 非阻塞观察
|
|
||||||
|
|
||||||
- budget 越大 fps 越差(与直觉相反)——核外的关键不是缓存大,而是**每帧渲染块数小**。
|
|
||||||
内存恒定靠 LRU 已稳,性能靠紧裁剪 + 高效核外管线。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 复现命令
|
|
||||||
|
|
||||||
```
|
|
||||||
# 主配置(budget=64)
|
|
||||||
build\release\tools\gpr_poc\gpr_poc.exe renderC build\tmp\gpr_store_B_001 --budget 64 --frames 120
|
|
||||||
# budget 扫描
|
|
||||||
... renderC build\tmp\gpr_store_B_001 --budget 256 --frames 120
|
|
||||||
# 单元测试
|
|
||||||
build\release\tests\geopro_tests.exe --gtest_filter=OutOfCoreSource.*
|
|
||||||
```
|
|
||||||
|
|
||||||
> 直驱运行需 VTK/Qt 运行时 DLL 在 exe 旁(已由 `tools/gpr_poc/CMakeLists.txt` 的
|
|
||||||
> `TARGET_RUNTIME_DLLS` POST_BUILD 拷贝;自定义 VTK 安装的 DLL 也已落到 exe 目录)。
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,8 @@
|
||||||
#include <vtkActor.h>
|
#include <vtkActor.h>
|
||||||
#include <vtkCamera.h>
|
#include <vtkCamera.h>
|
||||||
#include <vtkCubeSource.h>
|
#include <vtkCubeSource.h>
|
||||||
|
#include <vtkGPUVolumeRayCastMapper.h>
|
||||||
|
#include <vtkOpenGLGPUVolumeRayCastMapper.h>
|
||||||
#include <vtkImageActor.h>
|
#include <vtkImageActor.h>
|
||||||
#include <vtkImageData.h>
|
#include <vtkImageData.h>
|
||||||
#include <vtkImageMapToColors.h>
|
#include <vtkImageMapToColors.h>
|
||||||
|
|
@ -1066,6 +1068,255 @@ int cmdRenderC(int argc, char** argv) {
|
||||||
return volFpsValid ? 0 : 1;
|
return volFpsValid ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 单 mapper SetPartitions 整卷体绘制基准(POC-C-partitioned,去风险探针)
|
||||||
|
// ============================================================================
|
||||||
|
//
|
||||||
|
// 验"对的架构":整卷喂【单个】vtkGPUVolumeRayCastMapper(其 OpenGL 实现 =
|
||||||
|
// vtkOpenGLGPUVolumeRayCastMapper),用 SetPartitions(ceil(nx/16384),...) 让同一
|
||||||
|
// mapper 内部把体沿轴分区上传(每区 ≤16384 绕过 GL_MAX_3D_TEXTURE_SIZE),一次
|
||||||
|
// ray cast。对照 9c 整卷单 SmartVolumeMapper(INVALID,纹理墙) 与 12 MultiBlock
|
||||||
|
// (每块一 mapper,9.5 静态/1.45 换页)。
|
||||||
|
//
|
||||||
|
// 双闸(同 9c,绝不把空纹理假帧率当性能):
|
||||||
|
// ① CapturingOutputWindow 捕获 3D 纹理维度错误;
|
||||||
|
// ② 真实回读像素,统计非背景像素 → 非空才算真渲出。
|
||||||
|
int cmdRenderCPartitioned(int argc, char** argv) {
|
||||||
|
const Args a = parseArgs(argc, argv, 2);
|
||||||
|
if (a.positional.empty()) {
|
||||||
|
std::cerr
|
||||||
|
<< "用法: gpr_poc renderC-partitioned <storeDir> [--frames 120]\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
const std::string dir = a.positional[0];
|
||||||
|
const int frames = std::stoi(a.get("frames", "120"));
|
||||||
|
std::cout << "[renderC-partitioned] storeDir=" << dir << " frames=" << frames
|
||||||
|
<< "\n";
|
||||||
|
|
||||||
|
// 闸门复检:不可渲染机不产假 fps。
|
||||||
|
std::cout << "[renderC-partitioned] 离屏闸门复检...\n";
|
||||||
|
if (cmdOffscreenSmoke() != 0) {
|
||||||
|
std::cout << "[renderC-partitioned] 闸门失败,中止,不产出 fps。\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) WholeVolumeSource 重组整卷 VTK_SHORT image(常驻内存,约 400MB)。
|
||||||
|
Stopwatch swLoad;
|
||||||
|
geopro::render::WholeVolumeSource src(dir);
|
||||||
|
const double loadMs = swLoad.elapsedMs();
|
||||||
|
const auto& m = src.meta();
|
||||||
|
|
||||||
|
const std::int64_t voxels =
|
||||||
|
static_cast<std::int64_t>(m.nx) * m.ny * m.nz;
|
||||||
|
const std::int64_t wholeBytes = voxels * 2; // VTK_SHORT
|
||||||
|
std::cout << "[renderC-partitioned] 整卷 " << m.nx << "x" << m.ny << "x"
|
||||||
|
<< m.nz << " 体素=" << voxels << " 字节=" << wholeBytes << " ("
|
||||||
|
<< wholeBytes / (1024.0 * 1024.0) << " MB),加载 " << loadMs
|
||||||
|
<< "ms\n";
|
||||||
|
|
||||||
|
auto images = src.currentImages();
|
||||||
|
if (images.empty() || !images.front()) {
|
||||||
|
std::cerr << "[renderC-partitioned] 错误: currentImages 为空\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
vtkImageData* shortImg = images.front().Get();
|
||||||
|
|
||||||
|
// 2) 分区数:任一轴 > 16384 → ceil(dim/16384) 个分区,其余轴 1。
|
||||||
|
constexpr int kMax3DTex = 16384;
|
||||||
|
auto partCount = [](int dim) {
|
||||||
|
return static_cast<unsigned short>((dim + kMax3DTex - 1) / kMax3DTex);
|
||||||
|
};
|
||||||
|
const unsigned short px = partCount(m.nx);
|
||||||
|
const unsigned short py = partCount(m.ny);
|
||||||
|
const unsigned short pz = partCount(m.nz);
|
||||||
|
std::cout << "[renderC-partitioned] SetPartitions(" << px << "," << py << ","
|
||||||
|
<< pz << ") 每区上限 ≤" << kMax3DTex << " (沿线 " << m.nx << "/"
|
||||||
|
<< px << "=" << (m.nx + px - 1) / px << ")\n";
|
||||||
|
|
||||||
|
// 3) 量化域传函(复用现有 makeI16VolumeProperty:qmin/qmax + kBlank 透明)。
|
||||||
|
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||||||
|
const geopro::core::ColorScale cs = makeColorScale(vmin, vmax);
|
||||||
|
vtkSmartPointer<vtkVolumeProperty> prop =
|
||||||
|
makeI16VolumeProperty(m.quant, cs, vmin, vmax);
|
||||||
|
|
||||||
|
// 4) 离屏 + 单个 GPU ray cast mapper + SetPartitions。
|
||||||
|
const int winW = 1024, winH = 768;
|
||||||
|
auto rw = makeOffscreenWindow(winW, winH);
|
||||||
|
vtkNew<vtkRenderer> ren;
|
||||||
|
ren->SetBackground(0.0, 0.0, 0.0);
|
||||||
|
rw->AddRenderer(ren);
|
||||||
|
|
||||||
|
// vtkGPUVolumeRayCastMapper 抽象基类无 SetPartitions(在 OpenGL 实现上);
|
||||||
|
// 直接建 OpenGL 具体类(工厂默认产物同此),喂【整卷单 image】不预切块。
|
||||||
|
vtkNew<vtkOpenGLGPUVolumeRayCastMapper> mapper;
|
||||||
|
mapper->SetInputData(shortImg);
|
||||||
|
mapper->SetPartitions(px, py, pz);
|
||||||
|
|
||||||
|
auto volume = vtkSmartPointer<vtkVolume>::New();
|
||||||
|
volume->SetMapper(mapper);
|
||||||
|
volume->SetProperty(prop);
|
||||||
|
ren->AddVolume(volume);
|
||||||
|
|
||||||
|
// 装捕获式 OutputWindow:拦截分区上传时的 3D 纹理维度错误。
|
||||||
|
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||||||
|
vtkOutputWindow::SetInstance(capWin);
|
||||||
|
|
||||||
|
// 相机:用 mapper 实际包围盒定向(整卷,非工作集);体极扁长(44476:29:162),
|
||||||
|
// ResetCamera 全体后再倾斜抬高视角,让薄维度可见(否则边缘视角近乎不可见)。
|
||||||
|
{
|
||||||
|
double b[6];
|
||||||
|
mapper->GetBounds(b);
|
||||||
|
if (b[0] <= b[1]) {
|
||||||
|
ren->ResetCamera(b);
|
||||||
|
} else {
|
||||||
|
ren->ResetCamera();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vtkCamera* cam = ren->GetActiveCamera();
|
||||||
|
cam->Elevation(30.0); // 抬高,避免纯边缘视角看不到薄板
|
||||||
|
cam->Azimuth(30.0);
|
||||||
|
ren->ResetCameraClippingRange();
|
||||||
|
|
||||||
|
// 每帧旋相机 + Render 测 fps;同时多帧采样非背景像素取最大值
|
||||||
|
// (区分"真渲不出"与"末帧恰好边缘视角空"——后者只是采样时机)。
|
||||||
|
auto countNonBlack = [&]() -> vtkIdType {
|
||||||
|
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||||||
|
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, px);
|
||||||
|
vtkIdType nb = 0;
|
||||||
|
const vtkIdType np = px->GetNumberOfTuples();
|
||||||
|
for (vtkIdType i = 0; i < np; ++i) {
|
||||||
|
if (px->GetComponent(i, 0) > 10 || px->GetComponent(i, 1) > 10 ||
|
||||||
|
px->GetComponent(i, 2) > 10) {
|
||||||
|
++nb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nb;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::cout << "[renderC-partitioned] 单 mapper 整卷体绘制基准(" << frames
|
||||||
|
<< " 帧旋相机)...\n";
|
||||||
|
rw->Render(); // 预热(分区上传 + 编译 shader,不计时)
|
||||||
|
vtkIdType maxNonBlack = countNonBlack();
|
||||||
|
const int sampleEvery = std::max(1, frames / 8);
|
||||||
|
Stopwatch swBench;
|
||||||
|
for (int f = 0; f < frames; ++f) {
|
||||||
|
cam->Azimuth(360.0 / frames);
|
||||||
|
rw->Render();
|
||||||
|
if (f % sampleEvery == 0) {
|
||||||
|
maxNonBlack = std::max(maxNonBlack, countNonBlack());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const double benchMs = swBench.elapsedMs();
|
||||||
|
const double volFpsRaw =
|
||||||
|
benchMs > 0.0 ? frames * 1000.0 / benchMs : 0.0;
|
||||||
|
|
||||||
|
const bool textureErr = capWin->textureError();
|
||||||
|
vtkOutputWindow::SetInstance(nullptr);
|
||||||
|
|
||||||
|
// 5) 正确性判据:整个旋转扫描中的最大非背景像素(非空才算真渲出)。
|
||||||
|
const vtkIdType nonBlack = maxNonBlack;
|
||||||
|
const bool renderedNonEmpty = (nonBlack > 0);
|
||||||
|
|
||||||
|
// 双闸:无纹理错 + 非空像素 → fps 可信。
|
||||||
|
const bool volFpsValid = !textureErr && renderedNonEmpty;
|
||||||
|
const double volFps = volFpsValid ? volFpsRaw : -1.0;
|
||||||
|
const double peak = Probe::peakMemMB();
|
||||||
|
const bool interactive = volFpsValid && volFps >= 15.0;
|
||||||
|
|
||||||
|
const std::string volFpsStr =
|
||||||
|
volFpsValid ? std::to_string(volFps)
|
||||||
|
: std::string("INVALID(纹理错或空渲染)");
|
||||||
|
|
||||||
|
std::cout << "\n=== renderC-partitioned 单 mapper SetPartitions 指标 ===\n";
|
||||||
|
std::cout << "离屏闸门 : OK\n";
|
||||||
|
std::cout << "体维度 : " << m.nx << " x " << m.ny << " x " << m.nz
|
||||||
|
<< "\n";
|
||||||
|
std::cout << "体素数 : " << voxels << "\n";
|
||||||
|
std::cout << "整卷字节(B) : " << wholeBytes << " ("
|
||||||
|
<< wholeBytes / (1024.0 * 1024.0) << " MB)\n";
|
||||||
|
std::cout << "分区数(px,py,pz) : " << px << "," << py << "," << pz << "\n";
|
||||||
|
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "否") << "\n";
|
||||||
|
std::cout << "渲出非空像素 : " << (renderedNonEmpty ? "是" : "否(!!)")
|
||||||
|
<< " (非背景像素=" << nonBlack << ")\n";
|
||||||
|
std::cout << "体绘制 fps : " << volFpsStr << "\n";
|
||||||
|
if (!volFpsValid) {
|
||||||
|
std::cout << " (raw_fps=" << volFpsRaw << " 不可信)\n";
|
||||||
|
}
|
||||||
|
std::cout << "达交互级(≥15fps) : "
|
||||||
|
<< (interactive ? "是 ✔" : "否 ✘") << "\n";
|
||||||
|
std::cout << "进程峰值内存(MB) : " << peak << "\n";
|
||||||
|
std::cout << "源构造耗时(ms) : " << loadMs << "\n";
|
||||||
|
std::cout << "对照 renderB : 整卷单 SmartVolumeMapper=INVALID(纹理墙);"
|
||||||
|
"renderC MultiBlock=9.5 静态/1.45 换页;本探针="
|
||||||
|
<< (volFpsValid ? volFpsStr + "fps" : "INVALID") << "\n";
|
||||||
|
|
||||||
|
writeMetricLine(
|
||||||
|
"renderC-partitioned,dir=" + dir + ",nx=" + std::to_string(m.nx) +
|
||||||
|
",ny=" + std::to_string(m.ny) + ",nz=" + std::to_string(m.nz) +
|
||||||
|
",voxels=" + std::to_string(voxels) +
|
||||||
|
",wholeB=" + std::to_string(wholeBytes) +
|
||||||
|
",px=" + std::to_string(px) + ",py=" + std::to_string(py) +
|
||||||
|
",pz=" + std::to_string(pz) +
|
||||||
|
",textureErr=" + std::to_string(textureErr ? 1 : 0) +
|
||||||
|
",nonBlack=" + std::to_string(nonBlack) +
|
||||||
|
",volFpsValid=" + std::to_string(volFpsValid ? 1 : 0) +
|
||||||
|
",volFps=" + volFpsStr + ",volFpsRaw=" + std::to_string(volFpsRaw) +
|
||||||
|
",interactive=" + std::to_string(interactive ? 1 : 0) +
|
||||||
|
",loadMs=" + std::to_string(loadMs) + ",peakMB=" + std::to_string(peak));
|
||||||
|
|
||||||
|
// 写报告文件(覆盖式,含对照表)。
|
||||||
|
{
|
||||||
|
const fs::path repo =
|
||||||
|
fs::path("docs") / "superpowers" / "plans" / "poc-results-C.md";
|
||||||
|
fs::create_directories(repo.parent_path());
|
||||||
|
std::ofstream rf(repo.string());
|
||||||
|
if (rf) {
|
||||||
|
rf << "# POC-C 单 mapper SetPartitions 整卷体绘制探针结果\n\n";
|
||||||
|
rf << "## 体\n";
|
||||||
|
rf << "- 维度: " << m.nx << " x " << m.ny << " x " << m.nz << " (体素 "
|
||||||
|
<< voxels << ")\n";
|
||||||
|
rf << "- 整卷字节: " << wholeBytes << " B ("
|
||||||
|
<< wholeBytes / (1024.0 * 1024.0) << " MB, VTK_SHORT)\n";
|
||||||
|
rf << "- store: " << dir << "\n\n";
|
||||||
|
rf << "## 单 mapper SetPartitions\n";
|
||||||
|
rf << "- mapper: vtkOpenGLGPUVolumeRayCastMapper (整卷单 image,不预切块)\n";
|
||||||
|
rf << "- 分区数: SetPartitions(" << px << ", " << py << ", " << pz
|
||||||
|
<< ") 每区上限 ≤" << kMax3DTex << "\n";
|
||||||
|
rf << "- 纹理维度错误: " << (textureErr ? "是" : "否") << "\n";
|
||||||
|
rf << "- 渲出非空像素: " << (renderedNonEmpty ? "是" : "否") << " (非背景像素 "
|
||||||
|
<< nonBlack << ")\n";
|
||||||
|
rf << "- 体绘制 fps: " << volFpsStr << "\n";
|
||||||
|
rf << "- 达交互级(≥15fps): " << (interactive ? "是" : "否") << "\n";
|
||||||
|
rf << "- 进程峰值内存: " << peak << " MB\n";
|
||||||
|
rf << "- 源构造耗时: " << loadMs << " ms\n\n";
|
||||||
|
rf << "## 对照表\n\n";
|
||||||
|
rf << "| 路径 | 是否渲出 | fps |\n";
|
||||||
|
rf << "|---|---|---|\n";
|
||||||
|
rf << "| renderB 整卷单 SmartVolumeMapper | INVALID(纹理墙) | — |\n";
|
||||||
|
rf << "| renderC MultiBlock(每块一 mapper) | 渲出 | 9.5 静态/1.45 换页 |\n";
|
||||||
|
rf << "| renderC-partitioned 单 mapper SetPartitions | "
|
||||||
|
<< (volFpsValid ? "渲出" : "未渲出") << " | "
|
||||||
|
<< (volFpsValid ? volFpsStr : std::string("INVALID")) << " |\n\n";
|
||||||
|
rf << "## 判据结论\n";
|
||||||
|
if (volFpsValid && interactive) {
|
||||||
|
rf << "单 mapper SetPartitions 整卷体绘制【真渲出且达交互级】(" << volFps
|
||||||
|
<< " fps ≥15)。C production 路线钉死可行。\n";
|
||||||
|
} else if (volFpsValid) {
|
||||||
|
rf << "单 mapper SetPartitions 整卷体绘制【真渲出但未达交互级】(" << volFps
|
||||||
|
<< " fps <15)。VTK 这条路天花板暴露,需评估 OpenVDS/自建 GL。\n";
|
||||||
|
} else {
|
||||||
|
rf << "单 mapper SetPartitions 整卷体绘制【未真渲出】(纹理错="
|
||||||
|
<< (textureErr ? "是" : "否") << ",非空像素="
|
||||||
|
<< (renderedNonEmpty ? "是" : "否")
|
||||||
|
<< ")。SetPartitions 未能绕过纹理墙,如实记录。\n";
|
||||||
|
}
|
||||||
|
std::cout << "[renderC-partitioned] 报告写入 " << repo.string() << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return volFpsValid ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
void usage() {
|
void usage() {
|
||||||
std::cerr << "gpr_poc —— POC-B headless 度量 CLI\n"
|
std::cerr << "gpr_poc —— POC-B headless 度量 CLI\n"
|
||||||
" gpr_poc build <dir> [--line 001] [--cellXY 0.2] "
|
" gpr_poc build <dir> [--line 001] [--cellXY 0.2] "
|
||||||
|
|
@ -1074,7 +1325,8 @@ void usage() {
|
||||||
" gpr_poc selftest\n"
|
" gpr_poc selftest\n"
|
||||||
" gpr_poc offscreen-smoke\n"
|
" gpr_poc offscreen-smoke\n"
|
||||||
" gpr_poc renderB <storeDir> [--frames 120]\n"
|
" gpr_poc renderB <storeDir> [--frames 120]\n"
|
||||||
" gpr_poc renderC <storeDir> [--budget 64] [--frames 120]\n";
|
" gpr_poc renderC <storeDir> [--budget 64] [--frames 120]\n"
|
||||||
|
" gpr_poc renderC-partitioned <storeDir> [--frames 120]\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
@ -1092,6 +1344,8 @@ int main(int argc, char** argv) {
|
||||||
if (cmd == "offscreen-smoke") return cmdOffscreenSmoke();
|
if (cmd == "offscreen-smoke") return cmdOffscreenSmoke();
|
||||||
if (cmd == "renderB") return cmdRenderB(argc, argv);
|
if (cmd == "renderB") return cmdRenderB(argc, argv);
|
||||||
if (cmd == "renderC") return cmdRenderC(argc, argv);
|
if (cmd == "renderC") return cmdRenderC(argc, argv);
|
||||||
|
if (cmd == "renderC-partitioned")
|
||||||
|
return cmdRenderCPartitioned(argc, argv);
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cerr << "错误: " << e.what() << "\n";
|
std::cerr << "错误: " << e.what() << "\n";
|
||||||
return 1;
|
return 1;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue