Files
XCEngine/docs/used/NanoVDB体积云场景首帧阻塞根因修复计划_2026-04-10.md

651 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# NanoVDB 体积云场景首帧阻塞根因修复计划
日期2026-04-10
## 1. 文档定位
这份计划只解决一个具体问题:
-`project` 项目中打开带有 `cloud.nvdb` 体积云对象的 `Main.xc` 场景时Editor 在前期可交互,但在约 8 秒后出现一次明显的整窗卡死,随后再经过十多秒体积云才真正显示出来。
这份计划不讨论以下内容:
- 不讨论 build/package/runtime 发布格式。
- 不讨论 NanoVDB 渲染算法本身是否继续升级。
- 不讨论多后端 rollout。
- 不讨论重新设计整个 `Library` 体系。
这份计划关注的是:
- 为什么当前 `Library` 已经存在,但含 `.nvdb` 的场景打开仍然会在首帧附近严重阻塞。
- 为什么 `mvs/VolumeRenderer` 用同一份 `cloud.nvdb` 可以约 3 秒完成显示,而 Editor 要慢很多。
- 应该如何把当前这条链路收口成一套真正可用的正式方案。
---
## 2. 问题现象
当前复现现象已经很明确:
1. 直接打开 `project` 项目。
2. 打开 `project/Assets/Scenes/Main.xc`
3. 界面会显示 `Runtime streaming scene assets...`,这段时间窗口还能继续操作。
4. 在大约 8000 ms 左右Editor 突然开始明显卡死。
5. 卡死十多秒后,体积云对象才真正显示出来,随后窗口恢复。
对比样本:
- `mvs/VolumeRenderer` 使用的是同一份 `cloud.nvdb`
- 运行 `mvs/VolumeRenderer/run.bat` 时,大约 3 秒左右即可加载并显示。
这说明:
- 慢点不在“这份 `.nvdb` 根本无法解析”。
- 慢点也不在“当前机器根本无法承载这份体积数据”。
- 真正的问题出在 Editor 主线里的额外同步收口路径。
---
## 3. 当前已经确认的事实
### 3.1 当前 `Library` 对体积资源做的不是“运行时加速缓存”
当前源文件:
- `project/Assets/cloud.nvdb`
- 文件大小:`590,241,000` bytes
当前 artifact
- `project/Library/Artifacts/.../main.xcvol`
- 文件大小:`590,240,896` bytes
这说明当前 `.xcvol` 基本就是:
- 一个较小的 header
- 加上一份几乎原样的 NanoVDB payload
也就是说,当前 `Library` 在体积资源这条链路上做的是:
- 导入身份缓存
- metadata 缓存
- source -> artifact 统一入口
但它还没有做到:
- 为运行时准备更轻的 cooked payload
- 为 GPU 上传准备更直接的 runtime-ready 数据
- 为首帧显示准备真正低成本的预热结果
结论:
- 当前 `Library``.nvdb` 是“导入缓存”,不是“运行时性能缓存”。
### 3.2 当前 VolumeField CPU 侧存在多次大拷贝
当前体积资源进入运行时时,大致会经历:
1.`.xcvol` 读取整个 payload 到临时缓冲。
2. `VolumeField::Create(...)` 再把 payload 拷贝进 `VolumeField::m_payload`
3. `RenderResourceCache::UploadVolumeField(...)` 再构造一个新的 `uploadData`
4. 再把这份数据写入 RHI buffer。
对 590MB 级别的体积数据来说,这不是“小开销”,而是主路径上的重负担。
### 3.3 当前 GPU 上传不是在后台完成,而是在第一次真正绘制时同步触发
当前体积云真正进入渲染时,会在 `BuiltinVolumetricPass::DrawVisibleVolume(...)` 中走:
- `RenderResourceCache::GetOrCreateVolumeField(...)`
- `UploadVolumeField(...)`
也就是说:
- 体积资源即使 CPU 侧已经异步读好了,
- GPU 侧 residency 的真正建立仍然是在第一次真正绘制该体积对象时才触发,
- 而且当前实现是同步发生在渲染路径里。
这正好解释了现在的现象:
- 前期 `Runtime streaming scene assets...` 时还能操作。
- 到第一次真正要画体积云时,主线程/渲染线程突然进入大开销同步路径。
### 3.4 当前体积 shader / PSO 首次创建也会叠加到这次阻塞里
当前体积材质使用的是:
- `builtin://shaders/volumetric`
该 builtin shader 最终会加载:
- `engine/assets/builtin/shaders/volumetric.shader`
这个 shader 直接包含:
- `PNanoVDB.hlsl`
而当前 D3D12 shader 编译路径明确使用:
- `D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION`
这意味着:
- 体积 pass 第一次真正创建 pipeline state 时,
- 很可能还会在主线程/渲染线程上同步现编一个包含 `PNanoVDB` 的大 shader 变体。
因此这次卡死不是单一开销,而是两类重活叠加:
1. 590MB 体积 payload 的运行时实现化
2. 体积 shader / PSO 的首次真实可绘制化
### 3.5 当前场景加载状态文本会误导真实阶段
当前 `Runtime streaming scene assets...` 的状态主要依赖:
- `ResourceManager::GetAsyncPendingCount()`
它只能说明:
- 异步 loader 队列还有没有待完成任务
它不能说明:
- GPU 上传是否已经完成
- 体积 shader 是否已经编好
- PSO 是否已经 ready
- 体积对象是否真的达到 render-ready
所以现在 UI 上看到的“streaming 快结束了”并不等于体积云真的快 ready 了。
---
## 4. 最根本的原因
把这次问题压缩成一句话:
**当前 `Library` 缓存住的是体积资源的“导入结果”,但没有缓存住体积云真正昂贵的“运行时实现化结果”;而这部分实现化又被推迟到了第一次真正绘制时同步完成。**
更展开一点,根因可以拆成四层:
### 4.1 第一层根因:缓存层次不对
现在缓存住的是:
- source asset 身份
- artifact 文件
- metadata
没有缓存住的是:
- 低拷贝可消费的 volume payload 视图
- GPU-ready residency
- shader / PSO 可直接绘制状态
### 4.2 第二层根因:时机不对
当前重活发生在:
- 不是项目打开时
- 不是场景结构恢复后后台预热时
- 而是在第一次真正绘制体积云时
这导致体感非常差,因为卡顿被集中释放在用户已经开始操作视口之后。
### 4.3 第三层根因:实现路径太重
当前 590MB 体积数据在 Editor 里会经历多次 CPU 侧复制和一次同步 GPU 上传。
对这种量级的数据,这本身就足以形成长时间阻塞。
### 4.4 第四层根因:渲染首帧还叠加了 shader / PSO 首编
体积 pass 的首次真正绘制不是“只差最后一次 draw call”而是还可能同时触发
- shader variant 首编
- pipeline layout 创建
- PSO 创建
- descriptor set 初始化
所以这次卡死是“重资源 + 重 shader”叠加不是单点故障。
---
## 5. 为什么 `mvs/VolumeRenderer` 更快
`mvs/VolumeRenderer` 当前更快,不是因为它“缓存更高级”,而是因为它路径短得多。
它做的是:
- 直接读源 `.nvdb`
- 直接生成 GPU buffer
- 直接编少量 shader
- 直接渲染
它没有承担下面这些 Editor 主线额外职责:
- `AssetDatabase`
- `AssetRef -> path` 解析
- scene/component 反序列化恢复
- `VolumeField` 运行时抽象包装
- `RenderResourceCache` 的通用缓存层
- `BuiltinVolumetricPass` 的通用 descriptor / pass / pipeline 约束
- 视口首帧时的 editor 额外渲染流
所以 MVS 快,说明的是:
- 这份 `.nvdb` 并不是天然慢到 15 秒
而不是说明:
- 现在的 Editor 路线只需要继续堆更多导入缓存就能变快
---
## 6. 修复目标
本轮修复目标不是“让 `.xcvol` 体积更小”,也不是“强行把所有工作前置到项目启动”。
本轮的正式目标是:
### 6.1 交互目标
- 打开 `Main.xc` 后,不允许再出现“先可交互,再突然长时间整窗卡死”的体验。
- 一旦场景已经进入可交互状态,后续体积云就只能继续后台预热,不能把窗口重新拖回不可响应。
### 6.2 架构目标
- 把体积资源的“导入缓存”和“运行时实现化”明确分成两个阶段。
- 首次绘制路径不能再承载 590MB 级别的同步重活。
- 首次绘制路径只能消费已经 ready 的资源,或优雅跳过未 ready 的体积对象。
### 6.3 性能目标
在当前开发机和当前 `cloud.nvdb` 样本下:
- warm cache 场景再次打开时:
- 不允许出现超过 `200 ms` 的二次窗口无响应段。
- 体积云从场景打开到可见的时间目标收敛到 `3 s` 以内。
- 该目标以当前 `mvs/VolumeRenderer` 的约 `3 s` 作为对齐基线,正式目标是不慢于 MVS。
- cold import 首次打开时:
- 允许总体耗时更长,
- 但不允许在体积资源真正显示前出现长时间窗口卡死。
### 6.4 诊断目标
- 必须能明确区分:
- scene structure ready
- CPU payload ready
- GPU upload in progress
- shader / PSO prewarm in progress
- render-ready
---
## 7. 本轮明确不做的错误修法
以下方案不能作为本轮主方案:
### 7.1 不能只继续优化 `AssetDatabase::EnsureArtifact()`
原因:
- 这次 warm cache 场景下的主问题已经不是 source import 了。
- 再继续只抠导入判定和 reimport不会解决“第一次真正绘制才卡死”。
### 7.2 不能只加更多“异步读文件”
原因:
- 当前前半段已经异步了。
- 真正卡死点在后半段 render-time realization。
### 7.3 不能一上来就先加第二套磁盘缓存目录
原因:
- 当前最大问题首先是同步时机和多次拷贝。
- 如果不先把“谁在什么时候做重活”改对,再加新 cache 文件夹只是继续堆复杂度。
### 7.4 不能只通过隐藏 UI 文本来掩盖问题
原因:
- 现在不是提示文案不对,而是真有一段重度同步阻塞。
---
## 8. 正式修复方向
本轮采用四条主线并行收口,但执行顺序必须严格分阶段。
### 8.1 主线 A先把真实耗时切开看清楚谁最重
虽然根因已经明确,但时间占比仍需正式打点。
本阶段必须先拿到真实分段耗时,不允许后续继续靠体感猜。
需要新增的时间切片:
1. `Scene Deserialize`
2. `AssetRef Resolve`
3. `VolumeFieldLoader.ReadArtifact`
4. `VolumeField.Create`
5. `Volume GPU Upload`
6. `Volumetric Shader Variant Compile`
7. `Volumetric PSO Create`
8. `First Volume Visible`
需要新增的日志与状态:
- `Main.xc` 打开时,针对 `cloud.nvdb` 输出完整链路耗时。
- 把“异步流式加载完成”和“体积 render-ready”拆开显示。
这一阶段的目的不是修性能,而是:
- 锁死真正的大头时间占比
- 避免后续错误优化无关路径
### 8.2 主线 B去掉体积 payload 的多次大拷贝
这是本轮最核心的工程改动之一。
正式方向:
1. `VolumeField` 不能继续默认把 590MB payload 再拷进一份新的 `m_payload`
2. `.xcvol` 读取后,应该改成:
- 文件映射
- 或共享只读 blob
- 或单所有权 payload 容器
3. `RenderResourceCache::UploadVolumeField(...)` 不能再额外构造一份等体积的 `uploadData` 再拷一次。
4. 上传路径必须直接消费 loader 产出的只读 payload 视图。
本阶段完成后,应达到:
- `.xcvol -> VolumeField` 不再发生无意义的大内存复制。
- `VolumeField -> RenderResourceCache` 也不再产生第二份 590MB 临时副本。
这一步做完,即使还没异步 GPU 上传,卡顿也会先明显下降。
### 8.3 主线 C把 GPU 上传从首次绘制路径里拿出去
当前真正错误的不是“上传很重”,而是“上传发生在第一次真正 draw 的时候”。
正式方向:
1.`VolumeField` 建立明确的 runtime residency 状态机:
- `Unloaded`
- `CpuReady`
- `GpuUploading`
- `GpuReady`
- `Failed`
2. CPU 侧 payload 一旦 ready就立即进入独立的 GPU 预热队列。
3. `BuiltinVolumetricPass::DrawVisibleVolume(...)` 不允许再承担首次重量级上传。
4. draw path 的职责改为:
- 如果 `GpuReady`,正常绘制
- 如果未 ready跳过或显示占位不得同步收口
对 D3D12 的具体要求:
1. `BufferType::Storage` 不能继续把大体积 volume payload 当普通 upload-heap 常驻缓冲来处理。
2. 需要引入正式的:
- staging/upload buffer
- default heap storage buffer
- copy queue 或专用 upload path
3. volume buffer 上传完成后再切换到可读状态,而不是在 draw path 上临时补。
这一步是解决“8000 ms 后突然卡死”的主修复点。
### 8.4 主线 D把体积 shader / PSO 首编从体积首帧里拿出去
当前体积云第一次真正绘制时,渲染链路里还会叠加:
- builtin volumetric shader variant 首次真正编译
- pipeline layout / descriptor layout 构建
- PSO 创建
正式方向:
1. `builtin://shaders/volumetric` 的实际运行时使用变体需要正式预热。
2. 预热时机不放在“第一次真正 draw”。
3. 预热应在以下时机之一完成:
- scene structure ready 之后的后台预热阶段
- volume material 绑定后立刻排队预热
4. `BuiltinVolumetricPass` 首次执行时只能命中已存在的 shader variant / PSO cache。
当前 D3D12 路线还要额外处理一个问题:
- 现在编译 flags 是 `D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION`
这本身会显著拉长编译时间。
本轮需要明确一条正式策略:
1. Debug 能力是否仍要保留。
2. 如果保留,默认 editor 交互路径不能在首帧同步承担这份成本。
3. 可以接受“后台慢编”,不能接受“首帧卡死慢编”。
### 8.5 主线 E修正场景加载进度模型
当前 `Runtime streaming scene assets...` 的语义不完整。
这会直接误导调试,也会误导后续调度逻辑。
正式方向:
1. 把 scene load progress 拆成四段:
- `Structure Ready`
- `CPU Asset Streaming`
- `GPU Residency Prewarm`
- `Render Warmup`
2. `pendingAsyncLoads == 0` 时,只能说明 CPU loader 阶段接近结束。
3. 只有 volume GPU ready 且体积 pass 关键 shader/PSO ready 时,才能算真正 ready。
4. UI 需要把这几个阶段明确显示出来,避免再把后半段卡顿误判成“明明 streaming 都结束了却又卡”。
---
## 9. 执行阶段
### Phase 0诊断与基线固化
目标:
- 用日志和时间切片把当前问题彻底量化。
任务:
1.`Main.xc``cloud.nvdb` 打通完整性能时间线。
2. 输出 warm cache / cold cache 两组基线。
3. 输出 MVS 对比基线。
验收标准:
1. 能明确给出 CPU 读取、CPU 拷贝、GPU 上传、shader 编译、PSO 创建各自耗时。
2. 不再用“感觉像是这里慢”做判断。
### Phase 1体积 payload 低拷贝重构
目标:
- 消除 `.xcvol -> VolumeField -> UploadVolumeField` 链路中的重复大拷贝。
任务:
1. 重构 `VolumeField` 的 payload 持有方式。
2. 重构 `.xcvol` loader 的结果表示。
3. 重构 `RenderResourceCache::UploadVolumeField(...)` 的输入形式。
验收标准:
1. warm cache 情况下CPU 内存峰值明显下降。
2. `VolumeField` 载入完成时不再出现等量级的重复 payload 副本。
### Phase 2异步 GPU 上传与渲染脱钩
目标:
- 第一次绘制体积对象时,不再承担首次 GPU upload。
任务:
1. 增加 volume GPU prewarm 队列。
2. 建立 GPU residency 状态机。
3. 改掉 draw path 的同步上传兜底。
验收标准:
1. 首次打开 `Main.xc` 后,不再在体积第一次出现在视野中时发生长时间整窗阻塞。
2. 体积对象未 ready 时允许暂时不显示,但不允许卡死窗口。
### Phase 3体积 shader / PSO 预热
目标:
- 不再把 `volumetric.shader` 的首次真实可绘制准备放在体积首帧。
任务:
1. 为 active backend 预热体积 shader variant。
2. 为 volume material / pass 预热关键 PSO。
3. 把 shader/PSO 首编首建从 draw path 移走。
验收标准:
1. 首次显示体积云时,不再同时伴随大段 shader/PSO 同步创建时间。
2. 日志能证明首帧命中的是已准备好的可绘制状态。
### Phase 4场景状态机与 UI 收口
目标:
- 让场景打开状态和真实资源准备阶段一致。
任务:
1. 拆分 load status 阶段。
2. 修正 `Runtime streaming scene assets...` 的语义。
3. 在 Project/Viewport/Console 中统一反映同一份阶段状态。
验收标准:
1. UI 文案与真实阶段一致。
2. 不再出现“streaming 结束了但实际上后面还有一次大卡死”的错误认知。
### Phase 5回归、压力样本与收口
目标:
- 用真实样本和自动化验证确认这条路线已经稳定。
任务:
1. 用当前 `cloud.nvdb` 做 warm/cold 双场景回归。
2. 回归 `Main.xc` 打开、关闭、再次打开。
3. 检查体积云显示、Editor 响应性、日志阶段切片。
验收标准:
1. warm cache 打开 `Main.xc` 时无二次长阻塞。
2. cold cache 首次导入时也保持可交互。
3. 体积云显示时间显著收敛,接近 MVS 量级。
---
## 10.1 当前执行进展2026-04-10
当前代码实现已经进入正式重构阶段,已落地的第一批改动如下:
1. `VolumeField` 新增 owned payload 创建路径,`.xcvol` 读取结果不再在 `VolumeField::Create(...)` 内发生第二次整块复制。
2. `VolumeFieldLoader` 的 artifact 载入路径已改为“读入 payload -> 直接 move 进 `VolumeField`”,去掉 artifact load 阶段的重复大拷贝。
3. `RenderResourceCache::UploadVolumeField(...)` 不再构造整块 `uploadData` 临时副本,改为直接消费 `VolumeField` payload。
4. `RenderResourceCache` 已为 volume 引入最小可用的 residency 状态:
- `Uninitialized`
- `Uploading`
- `Ready`
- `Failed`
5. volume GPU 上传已改为分帧推进,当前实现按固定 chunk 预算逐帧写入,避免第一次真正绘制时一次性同步吞下整块 payload。
6. `BuiltinVolumetricPass` 已拆出资源预热步骤:
- 先推进 volume upload
- 再预建 volume pass layout / pipeline
- draw path 只消费 `Ready` 的 volume未 ready 时直接跳过,不再兜底触发重量级上传
7. 当前已补最小日志:
- `Volume GPU upload started`
- `Volume GPU upload ready`
这批改动的意义是:
- 先把 warm cache 路径里最重的两类同步收口拆开:
- `.xcvol -> VolumeField` 的重复 CPU 大拷贝
- 首次 draw path 内的一次性整块 GPU 上传
- 先把“卡死 Editor”问题从根上打散成可推进、可观察、可继续优化的状态。
当前这批改动还没有完成的部分:
1. 还没有把 `.xcvol` 升级成真正 runtime-ready 的 cooked artifact当前只是先把现有 artifact 的运行时消费链路做轻。
2. 还没有补齐完整的分段耗时打点。
3. 还没有把 volumetric shader / PSO 的首编完全前移到更早阶段。
---
## 11. 涉及模块范围
预计会涉及但不限于以下模块:
- `engine/include/XCEngine/Resources/Volume/VolumeField.h`
- `engine/src/Resources/Volume/VolumeField.cpp`
- `engine/src/Resources/Volume/VolumeFieldLoader.cpp`
- `engine/include/XCEngine/Rendering/Caches/RenderResourceCache.h`
- `engine/src/Rendering/Caches/RenderResourceCache.cpp`
- `engine/include/XCEngine/Rendering/Passes/BuiltinVolumetricPass.h`
- `engine/src/Rendering/Passes/BuiltinVolumetricPass.cpp`
- `engine/src/RHI/D3D12/D3D12Device.cpp`
- `engine/src/RHI/D3D12/D3D12Buffer.cpp`
- `engine/src/Resources/BuiltinResources.cpp`
- `editor/src/Managers/SceneManager.cpp`
- `editor/src/Viewport/ViewportHostService.h`
如果 Phase 2 仍然不足,再考虑是否需要新增专门的 runtime-prewarm 辅助模块。
但在本轮中,不应先为了结构好看而过早引入新的大模块。
---
## 12. 风险与边界
### 11.1 风险一:只优化 CPU 拷贝,但仍保留首帧同步 GPU 上传
结果:
- 会变快,但不会从根上解决“突然卡死”。
### 11.2 风险二:只做 GPU 上传异步,但 shader / PSO 首编仍在首帧
结果:
- 体积数据路径变快,但体积首帧依然可能因为 shader 现编而卡死。
### 11.3 风险三:一上来先设计新的磁盘 runtime cache
结果:
- 复杂度先上去了,
- 但如果真正的大头是同步上传和首编,收益会被高估。
因此本轮策略必须是:
1. 先打点
2. 先移走同步重活
3. 再决定是否需要更重的磁盘级 runtime cache
---
## 13. 完成标志
当以下条件同时成立时,这份计划才算完成:
1. 打开 `project/Assets/Scenes/Main.xc`Editor 不再出现二次长时间整窗卡死。
2. `cloud.nvdb` 的 warm cache 路径已经不再依赖首次绘制时的同步重资源收口。
3. 体积 shader / PSO 的首次准备不再挤在体积首帧。
4. 场景进度状态能正确区分 CPU streaming、GPU prewarm 和 render-ready。
5. 当前这条路径的耗时分布可以通过日志直接解释,不再需要靠猜。
---
## 14. 一句话结论
这次问题的根不在“`Library` 有没有命中”,而在“当前 `Library` 只缓存了导入结果,没有缓存运行时真正昂贵的实现化结果,而且这部分实现化被错误地放到了体积第一次真正绘制时同步完成”。
本轮修复必须围绕这个根因展开,而不是继续把注意力放回导入判定本身。