如何快速验证 D3D12 转换管线:Madeira 的 DXIL 金丝雀着色器完整指南
【免费下载链接】MadeiraRun x86-64 Windows PC games on jailed iOS via FEX-Emu + Wine + DXMT项目地址: https://gitcode.com/GitHub_Trending/mad/Madeira
Madeira是一个让 x86-64 Windows 游戏运行在越狱 iOS 上的项目,核心思路是 FEX-Emu + Wine + DXMT。其中最难的一环是把 Windows 的D3D12画到 Apple GPU 上——iOS 没有 D3D12,每个着色器必须先把DXIL中间格式编译成 Metal 库(Metal Shader Converter)。这条转换管线一旦出错,游戏就是黑屏。
Madeira 的答案很"金丝雀":不直接拿游戏开刀,而是先写一对极小、极刻意的 DXIL 金丝雀着色器,用 26~27 项自动化检查把"DXIL → 转换器 → Metal 库 → 渲染管线 → 像素读回"整条链路逐层验证,全部通过后才允许在其上搭建真正的 D3D12 实现。
为什么需要"预编译金丝雀":先证明管线,再谈游戏
在 iOS 上跑 D3D12 游戏的完整链路是:
游戏 DXBC/DXIL 着色器 → Metal Shader Converter 转换 → metallib 加载 → MTLRenderPipelineState → GPU 出图 → 像素读回验证
链路上任何一环失败都会静默地表现为"颜色不对"或"启动黑屏"。Madeira 的设计原则是:在写任何 COM 对象代码之前,先有一个门禁(gate)证明"转换出来的着色器能正确读回我们喂进去的颜色"。这就是 M1 里程碑的 msc_canary.mm 金丝雀测试,它的地位相当于矿井里的金丝雀——管线坏了,它先死,而且死法能精确指出坏在哪一环。
两个金丝雀着色器:故意最小的"探针"
金丝雀的 HLSL 源码只有几十行,刻意选成"能证明整条绑定路径的最小着色器":
- 单常量缓冲金丝雀canary.hlsl:顶点着色器不读任何顶点缓冲,用
SV_VertexID生成一个全屏三角形;像素着色器把b0根常量缓冲的颜色原样返回。像素值完全可预测,与光栅化规则无关。 - 布局金丝雀canary_layout.hlsl:故意在根签名里放"4 个 32 位根常量 + 1 个根 CBV"两参布局——红、绿通道来自根常量,蓝通道来自 CBV。这样任何一侧的偏移错误都会让像素颜色出错,而不是错出一个"看起来合理"的颜色。
两份着色器编译成的 DXIL 固件 直接放在仓库里作为测试输入,绕开了"顶点取数"和"转换器"两个变量同时被测试的问题(M4 里程碑的立方体再单独证明真实顶点缓冲路径)。
🔑 金丝雀设计精髓:错误的绑定偏移/绑定点会产出"错误的颜色",而不是"可接受的颜色"——结果二元化,检查才能自动判定。
26 项检查:金丝雀如何逐层拆解转换管线
msc_canary.mm 的检查按"从加载到像素"的顺序分层推进,每一层独立判定:
| 层级 | 检查内容 | 证明什么 |
|---|---|---|
| 1️⃣ 转换器加载 | 通过dlopen/dlsym解析 24 个入口符号 | iOS dylib 能在进程内加载,缺符号会点名报告而非 dyld 崩溃 |
| 2️⃣ 根签名驱动布局 | 显式构造根签名(不复用编译器嵌入的 RTS0 块) | 真实运行时收到的根签名来自应用,参数缓冲布局必须随之正确生成 |
| 3️⃣ 转换产物 | DXIL 转出的 metallib 在目标设备加载并建成渲染管线 | 转换器输出对设备"有效",而不只是字节合法 |
| 4️⃣ 像素读回 | 两组常量状态必须精确读出(255,0,0,255)和(200,100,255,255) | 参数缓冲的偏移、绑定全部正确——验证而非假设 |
| 5️⃣ 错误边界 | 无效 DXIL、截断字节流、不存在的入口名 | 全部以有界、有名字的错误失败,而不是崩溃 |
其中第 4 层最妙:布局金丝雀把根常量放在字节 0..15、根 CBV 的 64 位 GPU 地址放在字节 16,红/绿来自常量、蓝来自 CBV,"两侧任何一侧的布局错误都会显形且无歧义"。
缓存安全:预编译产物什么时候可以复用
金丝雀还顺带验证了着色器缓存的前提:
- 相同输入重编译,产出字节一致的 metallib——这是把转换产物落盘复用的基础(但 README 特别强调:字节一致既非必要也非充分条件,关键是把所有相关输入都纳入缓存键)。
- 缓存键敏感性:改最低部署版本、改根签名,输出必须变化或拒绝编译。
- GPU 族级(GPU family)是最深的坑:同一着色器在 macOS 上 Apple9 与 Metal3 产出字节相同,换到 iOS 设备上却产出不同字节——"保留族级在缓存键里"这个看似多余的决定在另一个平台上救了正确性。
生产环境的缓存实现是 madeira_dxil_cache.h:缓存键覆盖字节码、入口名、根签名、目标 OS/GPU 族/最低版本等 DXIL 分支真正读取的全部输入,并额外存一个独立的 64 位校验哈希防止文件名碰撞误命中;它的行为由纯 C 主机测试 dxil_cache_test.c 在无设备、无 SDK 的情况下先行验证。
真机成绩单:从 macOS 到 iPhone 13 Pro
同一套金丝雀在不同环境全部通过,结果对照如下(来自 README.md):
| 环境 | 结果 | 首次转换耗时 | 缓存重编译 |
|---|---|---|---|
| macOS M4 Max | ✅ 26/26 | 1.0 ms | 0.4 ms |
| iOS 越狱 vphone VM | ✅ 26/26 | 2.2 ms | 0.3 ms |
| 真机 iPhone 13 Pro(A15 GPU) | ✅ 27/27 | 16.8 ms | 0.6 ms |
跨目标测试更给出决定性结论:iOS 上编译、macOS 上加载的 metallib 哈希与 macOS 本地编译逐字节相同,"iOS 设备自己转换产物 → 远程 Mac 渲染"的回退方案因此不需要。
金丝雀还抓到一个真实缺陷:转换器不校验入口点名——用NoSuchEntryPoint编译能成功,唯一线索是反射返回空函数名,若不拦下,故障会延后到"缺少 MTLFunction"才爆炸。运行时自行加边界,正是金丝雀的价值。
金丝雀门禁之后:从 26/26 到 90/90
通过门禁后,项目才逐层往上盖楼,每一步都保持"检查数全绿":
- M1 in-app:金丝雀编译进
libdxmt,在 Madeira 自己的签名和沙箱里dlopen运行,27/27 通过——顺带验证了"dylib 能用应用的 team 签名被 dyld 接受"; - M2:独立
madeira_d3d12.dll(ARM64EC)实现 D3D12 COM ABI,41/41; - M3:真实 GPU 缓冲拷贝与读回、无屏渲染,77/77;
- 运行时 DXIL 转换 + M5 纹理:fixture 模式彻底移除,应用着色器在管线创建时当场转换,90/90 通过。
新手 takeaway:这套方法的 3 个可复用技巧
- 把"管线对不对"和"游戏对不对"分开验证:先造一个输出完全可预测的最小探针(全屏三角形 + 原样返回颜色),失败即定位到具体环节;
- 错误必须"显形":探针的输出设计成"错了就是错色,而不是错出一个合理值",才能让自动化检查二元判定;
- 在便宜的环境先跑:金丝雀先在 M4 Max 跑、再上 iOS VM、最后上真机 A15——三种故障(转换器本身 / 打包签名 / 启动路径)被刻意分开,"值得各自独立的失败"。
相关源码与文档:
- 金丝雀着色器:canary.hlsl、canary_layout.hlsl
- 金丝雀测试:msc_canary.mm
- DXIL 转换缓存:madeira_dxil_cache.h
- 模块里程碑与实测数据:README.md
- 转换 SDK 许可说明:metal-shader-converter/README.md
【免费下载链接】MadeiraRun x86-64 Windows PC games on jailed iOS via FEX-Emu + Wine + DXMT项目地址: https://gitcode.com/GitHub_Trending/mad/Madeira
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考