Files
XCEngine/docs/used/Shader预编译缓存与D3D12字节码正式化计划_完成归档_2026-04-10.md

288 lines
8.5 KiB
Markdown
Raw Normal View History

2026-04-11 00:27:23 +08:00
# 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` 编译拖慢。