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

16 KiB
Raw Permalink Blame History

NanoVDB 体积云加载阻塞与 Runtime 上传修复计划

文档日期2026-04-10

适用范围:当前 XCEngineResources / 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.nvdbmain.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 装载优化

不跳步骤,不同时展开多个大方向,先把最大收益点打掉。