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