# POC-C 实测结果(核外分块体绘制,命门探针) 工具:`tools/gpr_poc renderC`,产物 `build/release/tools/gpr_poc/gpr_poc.exe`。 执行机:Windows 11,MSVC(VS Community)+ Ninja,Release(/O2)。 GPU:**NVIDIA GeForce RTX 3060 Laptop GPU**,OpenGL 4.5.0 NVIDIA 555.97,硬件加速 True。 日期:2026-06-23。 被测 store(9c 建的单线全分辨率):`build/tmp/gpr_store_B_001` - 体维度 **44476 × 29 × 162**(≈2.09 亿体素),brick=64,金字塔 2 级 (level0=2085 块 / level1=696 块)。 - **沿测线 X=44476 ≫ GL_MAX_3D_TEXTURE_SIZE(16384)** —— 即 renderB 标 INVALID 的那条线,本任务要分块核外把它真渲出。 实现: - `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 --budget 64 --frames 120` --- ## 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 目录)。