288 lines
8.5 KiB
Markdown
288 lines
8.5 KiB
Markdown
|
|
# Shader 预编译缓存与 D3D12 字节码正式化计划
|
|||
|
|
|
|||
|
|
文档日期:2026-04-10
|
|||
|
|
|
|||
|
|
适用范围:`XCEngine` 当前 `Resources / Asset / Rendering / RHI / Editor` 主线,目标问题为编辑器与运行时虽然已经具备 shader artifact 与 variant 系统,但 D3D12 路径仍普遍在运行时现场 `D3DCompile(...)`,缺少真正可复用的编译字节码缓存链路。
|
|||
|
|
|
|||
|
|
关联归档:
|
|||
|
|
[NanoVDB体积云加载阻塞与Runtime上传修复计划_完成归档_2026-04-10.md](/D:/Xuanchi/Main/XCEngine/docs/used/NanoVDB体积云加载阻塞与Runtime上传修复计划_完成归档_2026-04-10.md)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. 当前结论
|
|||
|
|
|
|||
|
|
本轮 `NanoVDB` 卡顿排查已经把“资源读入/上传”与“shader 编译”分开钉死:
|
|||
|
|
|
|||
|
|
1. `main.xcvol` artifact 载入已经压到亚秒级。
|
|||
|
|
2. `cloud.nvdb` 对应 payload 的 CPU 读入和 GPU 上传不再是主瓶颈。
|
|||
|
|
3. 当前剩余大头是 D3D12 图形管线创建时的运行时 shader 编译。
|
|||
|
|
|
|||
|
|
最新 editor 实测:
|
|||
|
|
|
|||
|
|
- `VolumeFieldLoader Artifact total_ms = 674`
|
|||
|
|
- `AsyncVolumeLoadCompleted elapsed_ms = 909`
|
|||
|
|
- `UploadVolumeField total_ms = 736`
|
|||
|
|
- `D3D12 shader compile MainPS total_ms = 6205`
|
|||
|
|
- `SceneReady elapsed_ms = 8500`
|
|||
|
|
|
|||
|
|
这说明当前主线问题已经从“体积资源路径错误”切换成“D3D12 shader 运行时现场编译没有被缓存机制覆盖”。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. 根本问题
|
|||
|
|
|
|||
|
|
当前工程里虽然有 shader artifact,也预留了 `compiledBinary` 字段,但整条链路没有真正闭合。
|
|||
|
|
|
|||
|
|
### 2.1 已有但未闭环的部分
|
|||
|
|
|
|||
|
|
1. `ShaderStageVariant` 有 `compiledBinary` 字段。
|
|||
|
|
2. shader artifact 文件格式会序列化/反序列化 `compiledBinary`。
|
|||
|
|
3. shader runtime build 也会统计 `compiledBinary` 的内存占用。
|
|||
|
|
|
|||
|
|
### 2.2 真正缺失的部分
|
|||
|
|
|
|||
|
|
1. 没有任何正式步骤把 D3D12 编译结果写入 `variant.compiledBinary`。
|
|||
|
|
2. 没有任何 D3D12 运行时路径优先消费 `compiledBinary`。
|
|||
|
|
3. `D3D12Device::CreatePipelineState(...)` 仍然直接对 `ShaderCompileDesc` 走 `D3DCompile(...)`。
|
|||
|
|
4. shader artifact 现在缓存的是源码展开结果和变体描述,不是可直接用于 D3D12 的 DXBC/DXIL。
|
|||
|
|
|
|||
|
|
因此当前的真实状态不是“缓存偶尔失效”,而是:
|
|||
|
|
|
|||
|
|
`D3D12 shader 预编译缓存机制整体没有真正落地。`
|
|||
|
|
|
|||
|
|
`NanoVDB` 只是把这个系统性缺口最先炸出来,因为它的 `MainPS` 足够重。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. 目标
|
|||
|
|
|
|||
|
|
## 3.1 一级目标
|
|||
|
|
|
|||
|
|
让 D3D12 路径对同一 shader variant 不再反复运行时现场编译。
|
|||
|
|
|
|||
|
|
要求:
|
|||
|
|
|
|||
|
|
1. 首次编译后能够落盘保存 D3D12 可复用字节码。
|
|||
|
|
2. 再次打开 editor / scene / project 时优先命中缓存。
|
|||
|
|
3. `CreatePipelineState(...)` 在缓存命中时不再进入 `D3DCompile(...)`。
|
|||
|
|
|
|||
|
|
## 3.2 二级目标
|
|||
|
|
|
|||
|
|
把这套机制做成通用能力,而不是只给 `volumetric.shader` 打补丁。
|
|||
|
|
|
|||
|
|
要求:
|
|||
|
|
|
|||
|
|
1. 面向所有 D3D12 shader variant。
|
|||
|
|
2. 与现有 shader artifact/variant 系统兼容。
|
|||
|
|
3. 缓存失效规则明确,可随源码、profile、macro、backend、entry point 正确失效。
|
|||
|
|
|
|||
|
|
## 3.3 三级目标
|
|||
|
|
|
|||
|
|
为后续 Vulkan / OpenGL / DXC 路径保留统一设计空间。
|
|||
|
|
|
|||
|
|
本轮不要求一次做完多后端,但数据模型和接口命名不能把后续扩展堵死。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. 非目标
|
|||
|
|
|
|||
|
|
本轮不做以下内容:
|
|||
|
|
|
|||
|
|
1. 不重写整个 shader authoring 系统。
|
|||
|
|
2. 不直接切 DXC 全量替换 FXC。
|
|||
|
|
3. 不先做跨后端统一离线编译工具链。
|
|||
|
|
4. 不先做完整 PSO cache blob 体系。
|
|||
|
|
5. 不先解决所有 shader 首次编译耗时,只先解决“重复现场编译”这个系统性缺口。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. 正式实施方向
|
|||
|
|
|
|||
|
|
## 5.1 方向一:明确 D3D12 预编译产物的数据模型
|
|||
|
|
|
|||
|
|
要先统一“什么叫缓存命中”。
|
|||
|
|
|
|||
|
|
建议把 D3D12 预编译缓存键固定为以下信息的稳定组合:
|
|||
|
|
|
|||
|
|
1. shader 资源路径
|
|||
|
|
2. pass name
|
|||
|
|
3. stage
|
|||
|
|
4. backend
|
|||
|
|
5. source language
|
|||
|
|
6. entry point
|
|||
|
|
7. profile
|
|||
|
|
8. macro 集合
|
|||
|
|
9. 变体关键字集合
|
|||
|
|
10. 源码内容哈希
|
|||
|
|
|
|||
|
|
产物:
|
|||
|
|
|
|||
|
|
1. 对应 stage 的已编译 D3D12 字节码
|
|||
|
|
2. 可选的编译日志/调试信息版本标识
|
|||
|
|
3. 可选的 shader reflection 派生数据版本号
|
|||
|
|
|
|||
|
|
这一步的目的不是立即提速,而是先避免后面做出“缓存看起来有,实际上命不中或误命中”的半成品。
|
|||
|
|
|
|||
|
|
## 5.2 方向二:把 `compiledBinary` 从占位字段变成真实产物
|
|||
|
|
|
|||
|
|
当前 `compiledBinary` 只是格式字段,不是真正能力。
|
|||
|
|
|
|||
|
|
本阶段要做:
|
|||
|
|
|
|||
|
|
1. D3D12 编译成功后把字节码写回 `ShaderStageVariant::compiledBinary`
|
|||
|
|
2. shader artifact 写出时携带该字节码
|
|||
|
|
3. 重新载入 shader artifact 时恢复该字节码
|
|||
|
|
|
|||
|
|
要求:
|
|||
|
|
|
|||
|
|
1. 同一 variant 的 artifact 可以直接携带 D3D12 字节码
|
|||
|
|
2. 非 D3D12 后端不会被这套数据污染
|
|||
|
|
3. 缓存不存在时依然允许 fallback 到现场编译
|
|||
|
|
|
|||
|
|
## 5.3 方向三:D3D12 runtime 优先消费已编译字节码
|
|||
|
|
|
|||
|
|
这是运行时闭环的关键。
|
|||
|
|
|
|||
|
|
需要调整的不是 asset 层,而是 D3D12 runtime 创建 shader / pipeline 的入口:
|
|||
|
|
|
|||
|
|
1. `CreateShader(...)`
|
|||
|
|
2. `CreatePipelineState(...)`
|
|||
|
|
3. 任何内部 `CompileD3D12Shader(...)` 的调用链
|
|||
|
|
|
|||
|
|
优先级顺序应为:
|
|||
|
|
|
|||
|
|
1. 命中 `compiledBinary`,直接创建 shader bytecode
|
|||
|
|
2. 未命中时才走 `D3DCompile(...)`
|
|||
|
|
3. 首次现场编译成功后可回写缓存
|
|||
|
|
|
|||
|
|
验收标准:
|
|||
|
|
|
|||
|
|
1. 对同一项目二次启动时,`MainPS` 不再重新编译
|
|||
|
|
2. 日志里能明确区分 `cache_hit` / `cache_miss` / `runtime_compile`
|
|||
|
|
|
|||
|
|
## 5.4 方向四:把缓存失效规则正式化
|
|||
|
|
|
|||
|
|
没有失效规则,缓存就会变成隐患。
|
|||
|
|
|
|||
|
|
必须纳入失效判定的至少包括:
|
|||
|
|
|
|||
|
|
1. shader 源文件内容变化
|
|||
|
|
2. `#include` 展开结果变化
|
|||
|
|
3. entry point 变化
|
|||
|
|
4. profile 变化
|
|||
|
|
5. macro 变化
|
|||
|
|
6. backend 变化
|
|||
|
|
7. variant keyword 变化
|
|||
|
|
8. shader artifact schema version 变化
|
|||
|
|
|
|||
|
|
这一阶段必须输出一套明确规则,避免后面出现“改了 shader 却继续吃旧字节码”的错误。
|
|||
|
|
|
|||
|
|
## 5.5 方向五:引入可验证的日志与测试
|
|||
|
|
|
|||
|
|
这次 `NanoVDB` 能钉死问题,靠的是可计时日志,不是猜。
|
|||
|
|
|
|||
|
|
shader 预编译缓存正式化之后,也必须保留最小必要日志:
|
|||
|
|
|
|||
|
|
1. cache key
|
|||
|
|
2. cache hit/miss
|
|||
|
|
3. runtime compile elapsed
|
|||
|
|
4. binary load elapsed
|
|||
|
|
5. fallback reason
|
|||
|
|
|
|||
|
|
测试层至少要覆盖:
|
|||
|
|
|
|||
|
|
1. 首次冷启动:miss + compile + write
|
|||
|
|
2. 第二次热启动:hit + no compile
|
|||
|
|
3. 改 shader 源码后:缓存失效 + 重新编译
|
|||
|
|
4. 切换 profile / macro / keyword 后:缓存失效
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. 分阶段执行计划
|
|||
|
|
|
|||
|
|
## Phase 0:基线与接口盘点
|
|||
|
|
|
|||
|
|
任务:
|
|||
|
|
|
|||
|
|
1. 梳理 `ShaderStageVariant::compiledBinary` 的现状用途
|
|||
|
|
2. 盘点 D3D12 shader / pipeline 创建的全部编译入口
|
|||
|
|
3. 定义统一缓存键和版本策略
|
|||
|
|
4. 确认 artifact 与 runtime 的责任边界
|
|||
|
|
|
|||
|
|
交付:
|
|||
|
|
|
|||
|
|
1. 一份固定缓存键规则
|
|||
|
|
2. 一份 D3D12 编译调用链清单
|
|||
|
|
|
|||
|
|
## Phase 1:让 artifact 真正带上 D3D12 编译产物
|
|||
|
|
|
|||
|
|
任务:
|
|||
|
|
|
|||
|
|
1. 为 D3D12 variant 生成并保存编译字节码
|
|||
|
|
2. artifact 写入与读取完整覆盖 `compiledBinary`
|
|||
|
|
3. 为编译产物附加必要版本信息
|
|||
|
|
|
|||
|
|
交付:
|
|||
|
|
|
|||
|
|
1. D3D12 shader artifact 中存在真实二进制 payload
|
|||
|
|
2. 可验证 artifact 前后字节码一致
|
|||
|
|
|
|||
|
|
## Phase 2:让 D3D12 runtime 优先吃缓存
|
|||
|
|
|
|||
|
|
任务:
|
|||
|
|
|
|||
|
|
1. 调整 `CompileD3D12Shader(...)` 调用链
|
|||
|
|
2. 优先从 `compiledBinary` 构造 shader bytecode
|
|||
|
|
3. 未命中时 fallback 到 `D3DCompile(...)`
|
|||
|
|
4. 命中/失效/回写日志打通
|
|||
|
|
|
|||
|
|
交付:
|
|||
|
|
|
|||
|
|
1. 二次打开 editor 时不再重新编译体积云 `MainPS`
|
|||
|
|
2. 其他 D3D12 shader 也具备相同缓存能力
|
|||
|
|
|
|||
|
|
## Phase 3:引入自动验证与冷启动对比
|
|||
|
|
|
|||
|
|
任务:
|
|||
|
|
|
|||
|
|
1. 加入 shader cache 命中测试
|
|||
|
|
2. 加入变体失效测试
|
|||
|
|
3. 对 editor 冷启动做两轮连续对比
|
|||
|
|
|
|||
|
|
目标数字:
|
|||
|
|
|
|||
|
|
1. 二次启动 `MainPS compile total_ms` 应接近 `0`
|
|||
|
|
2. `SceneReady` 应继续从当前 `~8.5s` 下探
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. 当前建议的提交边界
|
|||
|
|
|
|||
|
|
在新计划开始实施前,建议把已经完成的修复与后续 shader cache 工作拆成两批提交:
|
|||
|
|
|
|||
|
|
### 提交一:NanoVDB 路径修复与编译热点降压
|
|||
|
|
|
|||
|
|
应包含:
|
|||
|
|
|
|||
|
|
1. volume artifact / payload 路径修复
|
|||
|
|
2. `ResizeUninitialized` 与大 payload 默认构造开销修复
|
|||
|
|
3. `volumetric.shader` 去除高风险 `[unroll]`
|
|||
|
|
4. HLSL profile 对齐到 `5_1`
|
|||
|
|
5. 当前保留的计时日志
|
|||
|
|
|
|||
|
|
### 提交二:Shader 预编译缓存正式化
|
|||
|
|
|
|||
|
|
暂不在本提交里混入,避免把“已验证修复”和“下一阶段系统改造”搅在一起。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. 验收标准
|
|||
|
|
|
|||
|
|
本计划完成时,至少满足:
|
|||
|
|
|
|||
|
|
1. D3D12 对同一 shader variant 的二次启动不再现场编译。
|
|||
|
|
2. 日志能明确证明缓存命中。
|
|||
|
|
3. 修改 shader 后缓存会正确失效。
|
|||
|
|
4. `NanoVDB` 体积云场景的二次启动 `SceneReady` 不再被 `MainPS` 编译拖慢。
|
|||
|
|
|