Madeira AOT预翻译全解析:如何让JIT编译成本"只付一次"
【免费下载链接】MadeiraRun x86-64 Windows PC games on jailed iOS via FEX-Emu + Wine + DXMT项目地址: https://gitcode.com/GitHub_Trending/mad/Madeira
Madeira是一个让 iPhone 免越狱运行 Windows PC 游戏的开源项目:它通过 FEX-Emu 动态翻译 x86/x86-64 指令、Wine 提供 Windows 运行环境、DXMT 用 Metal 绘制图形。动态翻译(JIT)听起来很酷,但"边跑边编译"会在启动时吃掉大量 CPU 时间。这篇文章带你搞懂Madeira 的 AOT 预翻译思路:哪些翻译成本被提前完成、哪些被缓存摊薄,最终让第二次启动快得像原生应用。
先搞懂痛点:JIT 的"启动税"有多贵
传统上运行 x86 游戏在 ARM 上有两条路:
- JIT(即时编译):程序跑一条指令,翻译器才现场把它译成 ARM64 代码。优点是灵活,缺点是启动阶段最疼——游戏加载时成百上千个函数第一次被执行,CPU 忙于翻译而非跑游戏;
- AOT(预翻译/提前编译):在运行前把二进制翻译成目标架构代码。启动快,但需要预先处理整个程序。
Madeira 走的是第三条务实路线:能 AOT 的尽量 AOT,必须 JIT 的部分把成本摊到"只付一次"。它自己也在 README 里承认:这是活跃的研究项目,兼容性因游戏而异(见 README.md)。
Madeira 的翻译栈:四层各司其职
| 层级 | 组件 | 编译时机 |
|---|---|---|
| 指令翻译 | FEX-Emu | 翻译器本体提前编译好,游戏指令 JIT |
| Windows 层 | Wine 11.4(ARM64EC) | 全部提前编译,仅游戏代码被翻译 |
| D3D 9/10/11 | DXMT | 运行时 + 磁盘缓存 |
| D3D12 | madeira-d3d12 | DXIL→Metal 转换 + 内容缓存 |
关键设计在 docs/BUILDING.md 里说得很清楚:FEX 被预先构建为xtajit64.dll(64 位)与xtajit.dll(32 位 WoW64),Wine 本身跑在 ARM64EC 上原生执行。也就是说,翻译引擎本身从不参与 JIT——只有游戏自己的代码需要动态翻译,这是第一层成本削减。
预分配 JIT 内存池:把"翻译的地盘"提前备好
即便走 JIT,也有一笔隐藏开销:每翻译一段代码都要申请可执行内存。iOS 上这更麻烦——JIT 需要调试器协作才能开启。
Madeira 的做法是应用启动时一次性划好"地盘":
- 双映射区域(RW 视图写代码、RX 视图执行代码),由 JITAllocator.h 中的
jit_region_create建立,翻译时写代码、自动做缓存失效; - 通过 StikJITHelper.swift 借助 StikDebug 在启动早期预留整块 JIT 内存池,并让调试器随后分离——之后翻译过程不再需要反复申请内存;
JITAllocator.h还专门维护了一个"早期窗口"(madeira_early_window_base,0x140000000 附近),在应用自身内存分配把低地址空间打碎之前就把这块稀缺地址段占住,避免翻译后期找不到连续可执行区。
对用户的直观感受就是:设置里 JIT 和 Memory+ 都显示绿勾后(README 称之为Ready to play),游戏运行期的翻译成本被压缩到纯粹的指令翻译本身。
最大头:着色器翻译的"30 秒黑屏"与磁盘缓存
真正让启动慢的不是指令,而是着色器。一个 D3D12 游戏启动时要转换约 160 个 DXIL 着色器,早期每次都现转,黑屏超过 30 秒——这个数字被直接写进了源码注释:
"Measured: a D3D12 title's startup was CPU-bound converting ~160 DXIL shaders on every launch, with a black screen for over 30 seconds." —— madeira-d3d12/src/unix/madeira_dxil_cache.h#L3-L6
解法就是典型的 AOT 化:内容寻址的磁盘缓存。madeira-d3d12/src/unix/madeira_dxil_cache.h 实现的缓存以"着色器字节码 + 入口点 + root signature + 目标 GPU 家族"为键(.mdxc文件),并额外存一个独立的 64 位哈希防止键碰撞取错着色器。首次启动付一次全价转换,之后的每次启动全部命中缓存,转换成本归零。DXBC 路径更早(ml1020 起)就有同款磁盘缓存,DXIL 路径这次补齐后两条路都做到了"翻译成本只付一次"。
这套纯 C 实现的缓存甚至不依赖 Apple SDK,宿主端就能跑测试:tests/native/dxil_cache_test.c。
安装期摊薄:Steam 库与清单"每安装只解析一次"
指令和着色器之外,Madeira 还在安装/入库环节做缓存:
- 从 Steam 下载的游戏(docs/STEAM_LIBRARY.md)入库后,其可执行程序、位数(32/64 位 pill)、所需 VC 运行时、安装大小这些信息被读取一次并挂在库条目上——文档原话是"per install folder, build and picked program",即每个安装文件夹 + 构建 + 选定程序只解析一遍,换构建或换程序才重读;
- 下载本身支持后台续传(SteamDownloadBackground.swift),已完成分块有日志(journal),失败暂停后不重复下载已完成的 chunk。
这些都属于"安装期干活,启动期受益":你等下载的那段时间,等于免费完成了后续启动需要的分析工作。
新手速查:这些设计对你意味着什么
- 首次启动慢是正常的:着色器首转、清单解析都发生在第一次;
- 二次启动会明显变快:DXIL/DXBC 缓存命中后,30 秒黑屏基本消失;
- 换 GPU 机型/系统大版本可能改变缓存键(目标 OS 与 GPU 家族参与哈希),需要重新转换一轮;
- 卸载游戏会清掉库条目(docs/STEAM_LIBRARY.md),但缓存机制与库条目独立管理,重装同一构建可直接命中;
- 想看完整构建链路(各组件如何被预编译、哪些输入不在仓库内),参考 docs/BUILDING.md。
小结
Madeira 并没有发明新的翻译器,而是把"编译成本"做了一次精明的调度:翻译引擎本身 AOT 预编译进xtajit*.dll、JIT 内存池启动时一次备齐、着色器转换靠内容寻址磁盘缓存摊到"只付一次"、Steam 库元数据在入库时解析完就长期复用。对新手来说记住一句话就够了:它让昂贵的翻译工作尽量发生在你不盯着屏幕的时候。作为 GPL-3.0 许可的活跃研究项目(LICENSE、LICENSE-EXCEPTION.md),这套"预翻译 + 缓存"的思路对任何跨平台运行时都是值得借鉴的通用解法。
【免费下载链接】MadeiraRun x86-64 Windows PC games on jailed iOS via FEX-Emu + Wine + DXMT项目地址: https://gitcode.com/GitHub_Trending/mad/Madeira
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考