Files
XCEngine/docs/used/NanoVDB体积云加载阻塞与Runtime上传修复计划_完成归档_2026-04-10.md

486 lines
16 KiB
Markdown
Raw Permalink Normal View History

2026-04-11 00:27:23 +08:00
# NanoVDB 体积云加载阻塞与 Runtime 上传修复计划
文档日期2026-04-10
适用范围:当前 `XCEngine``Resources / Asset / Rendering / RHI / Editor` 主线,目标问题为 editor 打开主场景后 `cloud.nvdb` 体积云加载时间长、首帧解锁后再次长时间卡死,而 `mvs/VolumeRenderer` 使用同一份 `cloud.nvdb` 仅约 3 秒即可完成加载与显示。
文档目标:把当前 editor 中 `NanoVDB` 体积云的加载链路从“CPU 异步读完后在首个可见渲染帧同步创建并写入大体积 GPU 资源,导致主线程长时间阻塞”的错误运行模式,重构为接近 `mvs/VolumeRenderer` 的正确模式即“CPU 异步读取 + GPU 本地 buffer 上传 + 上传完成前不阻塞编辑器交互 + 运行时不再把大体积 payload 留在 draw path 上处理”。
---
## 1. 问题结论
当前问题不是单点 bug而是三段成本叠加
1. `AssetDatabase``.nvdb``main.xcvol` artifact 只是“metadata + 原始 NanoVDB payload”包装几乎没有降低运行时 payload 成本。
2. `VolumeRendererComponent` 的 deferred scene load 只把 CPU 资源异步读到 `VolumeField`,没有提前完成 GPU 上传。
3. `BuiltinVolumetricPass` 首次真正消费 `VolumeField` 时,`RenderResourceCache::UploadVolumeField()` 在渲染线程同步创建 `StorageBuffer` 并调用 `SetData()` 写入整个 payload直接把首个可见帧变成一次大块同步上传。
最关键的问题在第 3 条。
当前 D3D12 后端里:
- `BufferType::Storage` 默认走 `UPLOAD heap`
- `SetData()``Map + memcpy`
- 于是体积云最终 shader 访问的不是 GPU 本地 `DEFAULT heap` buffer而是 CPU 可写的 upload buffer
这与 `mvs/VolumeRenderer` 的链路根本不同。`mvs` 是:
1. 创建最终 `DEFAULT heap` buffer
2. 创建临时 `UPLOAD heap` staging buffer
3. CPU 只写 staging
4. 通过 `CopyBufferRegion` 拷到默认堆
5. 之后 shader 从 GPU 本地 buffer 读取
因此,第一阶段收益最大的修复,不是继续优化 artifact 文件,而是把 editor/runtime 的 volume buffer 上传路径改成和 `mvs` 同构。
---
## 2. 现状与根因拆解
## 2.1 当前 editor 链路
### 场景打开阶段
1. `SceneManager::LoadScene()` 在 deferred scene load 作用域中恢复场景结构。
2. `VolumeRendererComponent` 仅恢复 `volumeRef -> path`,不立即同步 load。
3. 直到渲染抽取阶段调用 `GetVolumeField()` 时,才触发 `LoadAsync()`
4. editor 状态栏中的 `Runtime streaming scene assets...` 只反映 CPU 侧异步资源请求计数。
### CPU 资源完成阶段
1. `LoadAsync()` 完成后,`VolumeField` 已在 CPU 内存中可用。
2. 此时 editor 可以结束“streaming”状态但 GPU 尚未完成 volume payload 驻留。
3. 首个真正绘制体积云的 pass 进入 `BuiltinVolumetricPass::DrawVisibleVolume()`
4. `RenderResourceCache::GetOrCreateVolumeField()` 发现无缓存,触发 `UploadVolumeField()`
5. `UploadVolumeField()` 在渲染线程里同步分配 buffer / SRV并写入整个 payload。
### 结果
1. 用户前面数秒可以交互,因为 CPU 异步读取没有阻塞主线程。
2. 当 CPU 资源可用且首次 draw 发生时,主线程突然承担完整 GPU 上传。
3. 由于 payload 约 `590 MB`,首个可见渲染帧被长时间卡死。
## 2.2 当前 artifact 链路的问题边界
`cloud.nvdb` 已经命中 `Library/Artifacts/.../main.xcvol`,因此问题不是“每次重导入”。
但当前 `xcvol` 也没有真正消除运行时成本:
1. 写 artifact 时直接写出 `VolumeField` payload。
2. 读 artifact 时重新读入整个 payload 到 CPU 数组。
3. 运行时依然需要再把整块 payload 上传到 GPU。
换言之,当前 artifact 的价值主要是:
- 导入结果稳定
- metadata 结构化
- 允许项目资产走统一 `AssetRef` / `Library` 流程
它还没有做到:
- 运行时零拷贝或近零拷贝装载
- GPU 驻留态预烘焙
- 直接针对 volume draw path 的运行时加速
## 2.3 与 `mvs/VolumeRenderer` 的本质差异
不是“都在传同一个 buffer所以理论上应该一样快”而是当前两者的最终资源模型不同
### `mvs/VolumeRenderer`
- 最终资源:`DEFAULT heap` GPU 本地 buffer
- 中转资源:临时 `UPLOAD heap`
- 上传模式copy queue / direct queue 提交拷贝后等待完成
- draw path只消费已上传完成的 GPU buffer
### 当前 editor
- 最终资源:`UPLOAD heap` buffer
- 中转资源:无专门 staging
- 上传模式draw path 内同步 `Map + memcpy`
- draw path首次消费时同时承担资源上传职责
这意味着:
1. editor 首帧 draw path 的职责过重
2. volume payload 的最终落点错误
3. 即使 CPU 读取时间相近GPU 上传和后续 shader 读取性能仍会明显落后于 `mvs`
---
## 3. 修复目标
本次修复分为三个层级目标。
## 3.1 一级目标先把“8 秒后突然卡死十几秒”彻底打掉
要求:
1. 打开主场景后editor 在 volume payload 首次可见前后都不出现长时间主线程冻结。
2. `BuiltinVolumetricPass` 不再承担大体积同步上传职责。
3. `StorageBuffer` 不再默认把 volume payload 留在 `UPLOAD heap` 作为最终运行时资源。
## 3.2 二级目标:让 editor 的 volume GPU 上传路径和 `mvs` 同构
要求:
1. D3D12 下 volume payload 最终驻留在 `DEFAULT heap`
2. CPU 只写 staging / upload 资源。
3. GPU 通过 copy 提交完成真正拷贝。
4. shader 后续只读取 GPU 本地资源。
## 3.3 三级目标:继续把总加载时间向 `mvs` 靠近
要求:
1. 逐步削减 `xcvol -> CPU payload` 的运行时装载成本。
2. 未来允许 volume artifact 直接流向 GPU upload 路径,而不是“先完整常驻 CPU再完整复制到 GPU”。
3. 在保持引擎正式资源体系一致性的前提下,把总时间尽量压向 `mvs` 的约 3 秒基线。
---
## 4. 非目标
本轮不做以下内容,避免修复方向失焦:
1. 不重写 NanoVDB ray marching 算法本身。
2. 不把正式主线路径退化回 `mvs/VolumeRenderer` 的孤立 sample 结构。
3. 不先做 volume 压缩格式、体素裁剪重编码、分块稀疏 streaming。
4. 不先重构完整 SRP / render graph。
5. 不以“删除 Library 重建”作为修复方案。
6. 不为体积云单独发明 editor 私有旁路渲染器。
---
## 5. 正式修复方向
## 5.1 方向一:补齐“不可变 GPU 本地 buffer + 初始数据上传”能力
这是本轮最高优先级。
### 当前缺口
当前 `RHIDevice::CreateBuffer(const BufferDesc&)` 只描述“创建一个 buffer”但没有能力表达
- 最终资源要落在 GPU 本地内存
- 初始数据通过 staging copy 进入
- 创建后立即进入某个最终状态
因此 `RenderResourceCache::UploadVolumeField()` 只能:
1. `CreateBuffer()`
2. `CreateShaderResourceView()`
3. `SetData()`
对 D3D12 volume 来说,这条路是错的。
### 目标能力
新增正式 buffer 创建能力,语义类似:
- `CreateBuffer(const BufferDesc& desc, const void* initialData, size_t initialDataSize, ResourceStates finalState)`
- `CreateInitializedBuffer(...)`
要求:
1. D3D12 volume storage buffer 走 `DEFAULT heap`
2. 使用 upload staging 完成拷贝
3. 拷贝后转为 `GenericRead` 或对应 shader 可读状态
4. 返回对象仍然是统一 `RHIBuffer`
### 设计原则
1. 不要全局修改现有 `CreateBuffer(BufferType::Storage)` 的默认语义
2. 仅给需要“设备本地 + 初始数据上传”的资源新增专用路径
3. 保持旧代码依赖 `SetData()` 的场景继续可用
这一步是最大收益点,因为它同时解决:
1. 首帧主线程同步大 memcpy
2. volume 最终资源落在 upload heap 的错误模型
## 5.2 方向二:把 volume GPU 上传从 draw path 前移
仅修正 D3D12 buffer 落点,还不够。
如果 volume 仍在 `BuiltinVolumetricPass::DrawVisibleVolume()` 首次执行时触发上传,那么:
1. 即使上传路径更正确
2. 首个可见帧依旧要等待 GPU upload 完成
3. editor 仍会出现明显顿挫
因此还需要把职责改成:
### 正确职责分层
#### CPU 异步资源层
- `VolumeRendererComponent` 异步拿到 `VolumeField`
#### GPU 上传调度层
- 检测到 `VolumeField` CPU 资源完成后,提交 GPU upload 请求
- volume cache 进入 `Uploading` 状态
#### 渲染消费层
- `BuiltinVolumetricPass` 只消费 `GpuReady` 的 volume
- 对尚未就绪的 volume 不绘制,不再临时上传
### 目标状态机
建议为 volume runtime cache 明确引入:
1. `Uninitialized`
2. `CpuReady`
3. `Uploading`
4. `GpuReady`
5. `Failed`
首帧 draw path 不再负责从 `Uninitialized/CpuReady` 直接推进到 `GpuReady`
## 5.3 方向三:给 volume 增加正式上传队列或帧外预热入口
本轮至少需要一个最小正式机制,用于承接 GPU 上传工作。
### 最小可落地形式
1. 在渲染系统或 `RenderResourceCache` 外围增加 volume upload service
2. 在主循环中轮询已完成的 CPU async load
3. 将 volume GPU upload 提交给渲染设备层
4. 上传完成后切换到 `GpuReady`
### 推荐正式方向
统一成“渲染资源上传服务”,后续 mesh / large texture 也可以逐步收口到这里。
本次 volume 修复可以先做 volume-only 版本,但接口命名不要把未来扩展堵死。
## 5.4 方向四:削减 `xcvol` 的运行时 CPU 装载成本
这是第二优先级的大项。
当前 artifact 仍然要求:
1. 打开文件
2. 读 header
3. 分配 payload 数组
4. 把整个 payload 读入 CPU
5. 再把整个 payload 上传到 GPU
要接近 `mvs` 的总时间,后面必须继续推进:
### 正式方向
1. volume artifact header 与 payload 更明确分段
2. 支持 volume payload memory-mapping 或流式读入 upload buffer
3. 非必要时不长期保留 `590 MB` CPU payload 常驻
### 本轮边界
本轮不要求一步做到 memory-mapped 零拷贝,但文档和接口设计必须为此留口。
---
## 6. 分阶段实施计划
## Phase 0指标固化与基线采样
### 目标
在改代码前先量化基线,避免后续只凭体感判断。
### 任务
1. 记录当前 editor 打开 `Main.xc` 的三个时间点:
- 场景结构恢复完成
- `Runtime streaming scene assets...` 结束
- volume 真正可见
2. 记录当前 `mvs/VolumeRenderer` 从启动到 volume 可见耗时。
3. 为 volume load / upload 补 focused trace区分
- CPU artifact load
- GPU upload begin
- GPU upload complete
4. 记录 D3D12 volume buffer 创建类型与 heap 类型。
### 交付
1. 一组可复现实测数字
2. 一组可复现日志样本
## Phase 1RHI 补齐 initialized GPU-local buffer 能力
### 目标
让 D3D12 volume storage buffer 走 `DEFAULT heap + staging copy`
### 任务
1.`RHIDevice` 层新增 initialized buffer 创建接口。
2. D3D12 实现复用现有纹理上传思路,构建:
- upload queue
- upload allocator
- upload command list
- fence / idle wait
3. volume 使用新接口,不再 `CreateBuffer + SetData()`
4. 保证 SRV 创建逻辑不变shader 读取接口不变。
### 验收标准
1. D3D12 下 volume payload 最终 buffer 不再是 upload heap。
2. `RenderResourceCache::UploadVolumeField()` 不再通过 `SetData()` 写入大 payload。
3. 场景首次可见时的卡顿时长明显下降。
## Phase 2把 GPU 上传从 draw path 中移除
### 目标
首个可见帧只消费 ready 资源,不执行大资源上传。
### 任务
1. 在 volume runtime cache 层引入上传状态。
2. 在 CPU async load 完成后提交 GPU upload 请求。
3. `BuiltinVolumetricPass` 遇到 `Uploading` 状态时跳过该 volume。
4. editor 状态条可选择扩展为同时显示:
- CPU streaming count
- GPU upload pending count
### 验收标准
1. 打开场景后不再出现“streaming 结束后突然长时间卡死”。
2. editor 在 volume GPU 上传期间依旧保持交互。
3. volume 在 upload 完成后自然出现。
## Phase 3降低 `xcvol` 运行时 CPU 成本
### 目标
继续逼近 `mvs` 的总时间,而不只是解决阻塞。
### 任务
1. 评估 `VolumeFieldLoader` 是否允许 payload 延迟所有权或映射式读取。
2.`xcvol` 的 metadata 和 payload 读取职责分层。
3. 评估“直接读入 upload staging”路径减少中间副本。
### 验收标准
1. 打开主场景后的总 volume 可见时间继续下降。
2. CPU 峰值内存和中间复制次数下降。
## Phase 4正式化验证与回归保护
### 目标
确保修复不会在后续 volume / mesh / texture 路径上回归。
### 任务
1. 增加 volume upload 相关单测或最小集成验证。
2. 补 D3D12 路径日志断言或 profiling 钩子。
3. 验证 Scene View / Game View / 运行时体积渲染路径一致。
### 验收标准
1. 主场景 volume 打开稳定
2. `mvs` 与 editor 行为差距有明确量化解释
3. 后续可以继续迭代到更强的 runtime streaming 架构
---
## 7. 涉及模块
本轮实现预计涉及以下模块:
### RHI
- `engine/include/XCEngine/RHI/RHIDevice.h`
- `engine/src/RHI/D3D12/D3D12Device.cpp`
- 必要时:
- `engine/include/XCEngine/RHI/RHITypes.h`
- `engine/include/XCEngine/RHI/RHIEnums.h`
- `engine/include/XCEngine/RHI/D3D12/D3D12Buffer.h`
- `engine/src/RHI/D3D12/D3D12Buffer.cpp`
### Rendering
- `engine/src/Rendering/Caches/RenderResourceCache.cpp`
- `engine/include/XCEngine/Rendering/Caches/RenderResourceCache.h`
- `engine/src/Rendering/Passes/BuiltinVolumetricPass.cpp`
- 必要时新增 volume upload service 或相关状态结构
### Resources / Components
- `engine/src/Components/VolumeRendererComponent.cpp`
- `engine/include/XCEngine/Components/VolumeRendererComponent.h`
- `engine/src/Resources/Volume/VolumeFieldLoader.cpp`
- `engine/include/XCEngine/Resources/Volume/VolumeField.h`
### Editor / Telemetry
- `editor/src/Managers/SceneManager.cpp`
- `editor/src/Viewport/ViewportHostService.h`
### Tests
- `tests/Resources/Volume/`
- `tests/Components/test_volume_renderer_component.cpp`
- 必要时新增 rendering / integration 回归验证
---
## 8. 风险与约束
## 8.1 不能粗暴全局改 `StorageBuffer = DEFAULT heap`
原因:
1. 现有其他调用方可能默认依赖 `SetData()`
2. 全局改语义会引入隐藏回归
3. 本轮应以“新增 initialized immutable path”为主
## 8.2 首先只确保 D3D12 主线正确
当前用户问题发生在 editor D3D12 主线,因此:
1. 本轮优先保证 D3D12 正确和快
2. Vulkan / OpenGL 保持兼容,不要求一步做到同等级优化
3. 但接口设计不要阻断后续跨后端统一
## 8.3 不能让 draw path 同时承担恢复和上传职责
这条是硬约束。
只要 draw path 里仍然存在“第一次看到资源就同步上传 590MB”这件事问题就没有从根上解决。
---
## 9. 完成标准
认为本轮修复完成,至少要同时满足以下条件:
1. editor 打开 `Main.xc` 时,`Runtime streaming scene assets...` 结束后不再出现十几秒主线程卡死。
2. `cloud.nvdb` 的 volume payload 在 D3D12 下最终驻留于 GPU 本地 buffer而不是 upload heap。
3. `BuiltinVolumetricPass` 不再在首次 draw 时同步执行大体积上传。
4. editor 的 volume 可见总时间相比当前主线显著下降。
5. 修复后的行为可以用日志和代码路径明确解释,而不是只靠体感判断“快了”。
---
## 10. 本轮执行顺序
严格按以下顺序推进:
1. 固化指标与日志
2. 补 RHI initialized GPU-local buffer 能力
3. volume 改走新上传路径
4. 把 GPU upload 从 draw path 前移
5. 再评估 `xcvol` 运行时 CPU 装载优化
不跳步骤,不同时展开多个大方向,先把最大收益点打掉。