版本地狱实录Wine、FEX-Emu、DXMT 三方版本不匹配就是一场白折腾【免费下载链接】MadeiraRun x86-64 Windows PC games on jailed iOS via FEX-Emu Wine DXMT项目地址: https://gitcode.com/GitHub_Trending/mad/Madeira在 Apple Silicon 上跑 Windows 游戏早已不是装个模拟器那么简单。Madeira 走的是另一条更激进的路用 FEX-Emu 把 x86-64 指令实时翻译成 ARM64用 Wine 提供完整的 Windows 环境再用 DXMT 把 Direct3D 9/10/11 翻译成 Metal最后用自家 madeira-d3d12 补上 D3D12 这块拼图。四层翻译栈叠在一起任何一个组件版本不对整条链就断在某个谁也说不清的位置——游戏启动黑屏、Direct3D 设备创建失败、32 位程序直接拒绝运行而你排查了半天最后发现只是版本没配对。这篇文章基于 Madeira 仓库的源码与构建记录把三方组件之间的版本契约、ABI 错配的真实现场以及一套可以照抄的锁定策略拆开来讲。三方版本兼容矩阵与 ABI 错配的现场先看官方 README 给出的标准组合这是仓库里唯一一张明确的版本表层组件版本/形态指令翻译FEX-Emu把游戏自身的 x86/x86-64 代码翻译为 ARM64fork 分支ios-port-2607API 层Wine11.4按 ARM64EC 构建Wine 自身原生运行只翻译游戏代码32 位走 WoW64图形翻译DXMTD3D9/10/11 → Metalfork 分支ios-port补充层madeira-d3d12自家 D3D12 运行时用 Apple Metal Shader Converter 运行时转换 DXIL但真正决定能不能跑起来的是这四层之间那些看不见的 ABI 契约。.gitmodules里四个子模块全部指向willfaust的 fork分支名分别是ios-port-2607、madeira-lgpl、ios-port和main。README 说得非常直白FEX、wine、dxmt和madeira-dock是子模块指向 Madeira 自己的 fork上游 checkout 在这里根本编译不过。这不是谦虚是事实陈述每一层都携带了 Madeira 的私有改动这些改动恰好与其余几层咬合。比如 Wine 的 PE→unix 边界上DXMT 的 winemetal 通过WINE_UNIX_CALL表调用原生侧在build/dxmt-ios/build.sh里Makeira 把这条边界彻底重写为dxmt_madeira_nativeD3D9 前端被编译成原生 iOS-arm64 代码编入同一个静态库wineunixlib.h把WINE_UNIX_CALL变成一个表间接调用。这条边界的函数表名在winemetal_unix.c:5084被重命名构建脚本只能靠命令行宏-D__wine_unix_call_funcsdxmt_winemetal_unix_call_funcs指过去——表名、宏、符号三者必须同时对上任何一层换了版本这行编译参数就是错的。32 位支持的 ABI 契约更严苛。docs/WOW64.md记录了完整的设计i386 Wine farmapp/Madeira/i386-windows/ FEX 的 WOW64 模块xtajit.dll即libwow64fex.dll aarch64 的wow64.dll/wow64win.dll三者缺一不可。而tests/host/check-audio-wow64.py里有一条硬性断言wow64 表必须与 64 位表保持同样的 37 个槽、同样的顺序凡是共享条目必须是忽略参数或不携带指针的那一类。32 位调用方从 32 位参数块里解出 guest 指针再加B基址这个偏移逻辑如果跟 Wine 侧的addr_32to64实现不是同一代指针就全错位——轻则崩溃重则静默越界。为什么升级一个组件会连环炸版本不匹配的炸往往不在你最怀疑的地方。仓库里埋着几个教科书级的现场。现场一Metal 着色器语言版本差 0.1整个图形栈罢工。在build/dxmt-ios/build.sh的注释里开发者记录了这样一次事故DXMT 的命令库dxmt_command.metal若不加-std参数Xcode 的 metal 编译器会默认输出它 SDK 知道的最新语言版本一个在 macOS 27 工具链上构建的版本其 metallib 声称使用 Metal 语言 4.1而 iOS 26.1 的设备拒绝加载——报错信息是This library is using language version 4.1 which is not supported on this OS。后果链条写得很清楚metallib 不存在 → 无法创建 Direct3D 设备 → 所有需要它的游戏启动失败。于是脚本把DXMT_METAL_STD默认 pin 死在metal3.1AIR 目标也跟着 pin——着色器语言版本成了构建系统里被锁死的常量。现场二FEX 的 arena 契约是等 FEX 先表态。ContentView.swift里的 ml757 注释记录了一次差点自伤的优化让 Wine 预先保留 FEX 的 host arena听起来能避免地址空间竞争但当时 FEX 还不消费WINE_IOS_FEX_ARENA_BASE/SIZE它仍会自己选一块 band——于是预保留反而挤占了 FEX 自己选中的区域等于饿死 x64。结论是这个开关只有在对应版本的 FEX 真正读取那两个环境变量之后才能打开。一个开关的语义绑定在另一个组件的某个具体版本上这比任何编译错误都难排查。现场三着色器缓存也有身份指纹换版本即失效。同一个 build 脚本里converter_id_inputs()把转换服务源码、IR ABI 头、本构建脚本、DXMT 的 airconv、LLVM 配置、Metal Shader Converter 的头文件和 iOS 库全部做 SHA-256拼出 16 位CONVERTER_ID写进madeira_ir_unix.mm的MAD_SC_BUILD。它决定设备上已有的 shader cache 是否还能复用。任何一项的哈希变了所有已转换的 DXIL 缓存全部作废——不是报错而是下次启动每个着色器都要重新转换一遍游戏启动时间凭空翻几倍你还以为是自己机器变慢了。现场四升级顺序本身就是契约。docs/WOW64.md第 10 节给出了一份极其苛刻的合入顺序rpmalloc#1 → wine#6–#12 → FEX#2–#4FEX#3 还 pin 着 rpmalloc#1→ Madeira#40–#44 → 启动 PRpin FEX#4→ winemetal PRpin DXMT→ D3D9 PRpin dxmt#2。每一步 pin 都依赖上一步的 merge commit。这条顺序链说明这三个项目不是三个独立升级的软件而是同一份代码的不同切面谁先动了版本整条链的编译产物就全变了。除此之外构建还依赖一批外部工具链它们同样是版本雷区docs/BUILDING.md列出的清单包括llvm-mingw-20260421-ucrt-macos-universalFEX 两个 Windows 模块的交叉编译器用错版本编出来的 DLL 直接带错 ABI、LLVM 上游 commit8dfdcc7b7airconv 依赖它的 iOS 构建光编译就要数小时、Metal Shader Converter 4.0 beta 2SHA-256 被 pin 在build/madeira-d3d12/deps.sh、FFmpeg 7.1.1、GnuTLS 3.8.9、GMP 6.3.0、Nettle 3.10.1连 Wine Mono 11.0.0 的 x86 包和 mscorlib 补丁都要按 SHA-256 对账build/wine-mono/pin.sh。其中任何一项不是文档指定的版本构建过程就会在某个没人预料的环节断裂——这正是版本地狱最真实的形态。一份可照抄的版本组合与锁定策略好消息是Madeira 的工程实践本身就是一份完整的锁定策略教科书。逐条拆出来可以直接复用1. 用子模块 pin fork 分支锁定源码版本。不要把上游当依赖而是 fork 出专属分支ios-port-2607、madeira-lgpl、ios-port把私有改动沉淀在 fork 里子模块永远指向 fork 的特定 commit。这让版本从一个模糊的 tag 变成一个可复现的 SHA。副作用是上游新特性无法直接同步——但换来的是整条链的可重复构建。2. 把不可入仓的输入也 pin 死。docs/BUILDING.md专门维护了一张不在仓库里的输入表每个外部工具链写明获取 URL、精确版本、SHA-256以及是否从干净 checkout 验证过。工具链 tarball 的哈希、Apple 安装包的哈希、Wine Mono 的哈希全部记录在案。无法放进仓库的依赖就用哈希把它钉在某个坐标上。3. 构建选项写入脚本CMakeCache 作为真相。每个build/*/build.sh都把自己首次 configure 的参数完整写进脚本并注明选项镜像开发机上的 CMakeCache从 CMakeCache 反推的配置还专门标注 UNVERIFIED诚实地区分跑过和没跑过。DXMT 的 Metal 语言版本、FEX 的-DENABLE_CLANG_THUNKS这类影响 ABI 的选项绝不交给编译器默认值。4. 用哈希链管理缓存有效性。shader cache 的CONVERTER_ID机制是通用思路凡是影响产物的输入源码、脚本、头文件、依赖版本都纳入哈希产物缓存绑定这个哈希。换版本时宁可全部失效重来也不要用可能被旧缓存污染的中间结果。5. 把跨层契约写成测试。tests/host/下数十个check-*.py就是活的契约文档wow64 表与 64 位表同序同槽、JIT 映射别名、audio 表结构……这些断言把版本必须配对从口头约定变成 CI 里会失败的硬检查。每修一层就写一条锁住边界的测试。6. 运行时开关也要锁定。即使二进制全对FEX 的翻译行为仍受环境变量影响。docs/madeira.cfg.example给出了生产配置基线env.FEX_TSOENABLED 1、env.FEX_EXTENDEDVOLATILEMETADATA为特定 DLL 保留易失寄存器元数据、swap-mb、pool、totalphys等内存参数。版本锁定不止管构建期运行期配置同样是组合的一部分——这也是为什么升级一个组件后什么都没改但游戏变卡了往往要回头查 cfg。7. 升级必须按顺序且一次只动一层。docs/WOW64.md的 merge order 是这条策略的终极形态先依赖、再 Wine、再 FEX、再 DXMT每层合入后先验证再 pin 下一层。FEX 的 ARM64EC 模块被要求与上游构建逐字节相同每次 FEX PR 都要反汇编比对——用字节级一致性守住这层没动的底线。说到底版本地狱的根源不是项目维护得差而是三层翻译栈之间的 ABI 契约太密集、太隐晦PE→unix 调用表、WoW64 指针偏移、Metal 语言版本、shader 转换器身份、工具链 ABI……任何一个变化都会越过编译器的检查直接变成运行时的黑屏和崩溃。Madeira 的答案很朴素不追求最新只追求锁定——把每一层的版本、哈希、构建选项、运行时开关和跨层断言全部变成仓库里可审计、可复现的记录。下次你想顺手升个 Wine 试试之前先想想那句This library is using language version 4.1——那可能就是你今晚唯一的收获。【免费下载链接】MadeiraRun x86-64 Windows PC games on jailed iOS via FEX-Emu Wine DXMT项目地址: https://gitcode.com/GitHub_Trending/mad/Madeira创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考