1. 项目缘起:为什么我要折腾 Madeira
第一次看到“Madeira”这个词,很多人会以为是葡萄牙那个产葡萄酒的岛屿。但在我们这行,尤其是最近这段时间,它指的是一套围绕FEX-Emu、Wine、DXMT构建的跨架构兼容方案,目标很明确:让 x86-64 的 Windows 应用和游戏,在 ARM 设备上跑起来,尤其是 iOS 设备。你没看错,就是 iPhone 和 iPad 上跑 PC 游戏。
这个项目的核心逻辑链条其实不复杂:FEX-Emu 负责把 x86-64 指令翻译成 ARM64 指令,Wine 负责把 Windows API 调用翻译成 POSIX 调用,DXMT 负责把 DirectX 调用翻译成 Metal 调用。三层翻译叠在一起,最终让一个原本为 Windows x86-64 编译的 exe 文件,在 iOS 的 ARM64 芯片上运行起来。听起来像套娃,但每一层都有它存在的必然性。
我之所以花时间研究这个,是因为最近社区里关于“iOS 游戏”“iOS 设备模拟”“麒麟 wine 助手”“统信 wine windows 兼容组件”的讨论突然多了起来。很多人手里有 ARM 设备,想跑一些只有 Windows 版本的老游戏或者专业软件,但又不愿意再买一台 x86 电脑。Madeira 这套方案就是冲着这个需求去的。它适合谁?适合那些愿意折腾、对命令行不陌生、能接受一定失败率的玩家和开发者。如果你只是想点一下图标就能玩,那这篇文章可能不适合你;但如果你想搞清楚背后的原理,并且愿意跟着步骤一步步来,那接下来的内容应该能帮你省下不少查资料的时间。
2. 核心架构拆解:三层翻译到底在干什么
2.1 FEX-Emu:x86-64 到 ARM64 的指令翻译层
FEX-Emu 是整个链条的地基。它的工作是把 x86-64 的机器码实时翻译成 ARM64 的机器码。你可以把它想象成一个同声传译员,只不过它翻译的不是自然语言,而是 CPU 指令。x86-64 和 ARM64 的指令集差异很大,比如 x86 有复杂的变长指令编码,ARM64 则是定长指令;x86 的寄存器数量少但有很多隐式用法,ARM64 的寄存器多且规整。FEX-Emu 要在运行时做指令解码、寄存器映射、内存模型适配,工作量相当大。
实际使用中,FEX-Emu 的性能损耗主要来自几个方面:一是翻译本身的开销,二是翻译后的代码缓存管理,三是内存访问的屏障和同步。我实测下来,对于计算密集型的应用,性能大概能到原生 x86 的 40% 到 70%,具体取决于应用的指令特征。如果应用大量使用 SSE/AVX 向量指令,FEX-Emu 的翻译效率会更高一些,因为它对这些指令有专门优化。但如果是大量分支跳转和系统调用的场景,损耗就会明显上升。
注意:FEX-Emu 的配置文件中有一个
TSOEnabled选项,控制是否启用 x86 的总存储顺序(Total Store Order)模拟。开启后兼容性更好,但性能会下降;关闭后性能提升,但某些依赖严格内存顺序的应用可能崩溃。建议先开启,确认稳定后再尝试关闭。
2.2 Wine:Windows API 到 POSIX 的转换层
Wine 的职责是让 Windows 程序以为自己还在 Windows 上。它提供了一套 Windows API 的实现,包括 kernel32、user32、gdi32、ntdll 等等。当 exe 调用CreateFileW时,Wine 把它转换成 POSIX 的open;当 exe 调用MessageBoxW时,Wine 通过自己的图形驱动把它画出来。在 Madeira 的场景里,Wine 跑在 FEX-Emu 之上,所以 Wine 本身也是被翻译的 x86-64 代码。
这里有个关键点:Wine 的版本选择很重要。社区里常说的“wine 乱码”“wine 栏是乱码”“wine gecko 官方正版下载”,其实都跟 Wine 的字体配置和 Gecko 引擎有关。Wine 默认使用系统字体来渲染 Windows 程序的界面,如果系统里没有对应的中文字体,或者 Wine 的字体替换规则没配好,就会出现方块或者乱码。Gecko 是 Wine 用来渲染 HTML 内容的引擎,很多 Windows 程序的安装界面和帮助文档都是 HTML 的,没有 Gecko 就会报错或者显示空白。
我一般会做两件事:第一,在 Wine 的注册表里把FontSubstitutes配好,把MS Shell Dlg、Tahoma、SimSun这些映射到系统里已有的中文字体;第二,下载对应版本的 Wine Gecko 包,手动放到 Wine 的share/wine/gecko目录下。这两步做完,大部分乱码问题都能解决。
2.3 DXMT:DirectX 到 Metal 的图形翻译层
DXMT 是最近才成熟起来的一个组件,它的前身是 DXVK 的 Metal 后端尝试。DXVK 是把 DirectX 9/10/11 翻译成 Vulkan,而 DXMT 是直接翻译成 Metal。在 iOS 上,Vulkan 的驱动支持很有限,Metal 才是原生图形 API,所以 DXMT 是更合理的选择。它把 D3D11 的绘制调用、着色器、资源管理映射到 Metal 的对应概念上。
DXMT 的着色器编译流程是这样的:先把 DXBC 字节码反编译成中间表示,再转换成 Metal Shading Language,最后交给 Metal 编译器生成 GPU 机器码。这个过程有缓存机制,第一次运行某个游戏时会比较慢,因为要编译大量着色器,之后就会快很多。我试过几个 D3D11 的游戏,第一次进游戏等了大概两三分钟,之后启动就只要十几秒了。
提示:DXMT 的着色器缓存默认放在
~/Library/Caches/dxmt下面,如果遇到奇怪的渲染错误,可以先删掉这个目录强制重新编译。另外,Metal 的验证层在调试时很有用,但会拖慢性能,正式玩的时候记得关掉。
3. 实操环境搭建:从零开始跑通第一个程序
3.1 设备与系统要求
先说清楚硬件门槛。Madeira 这套方案目前主要跑在 Apple Silicon 的 Mac 和越狱后的 iOS 设备上。Mac 这边要求 M1 及以上芯片,系统版本建议 macOS 13 以上,因为 Metal 3 的一些特性需要较新的系统。iOS 这边要求 A12 及以上芯片,系统版本 iOS 15 以上,而且需要越狱或者使用 TrollStore 这类工具来获得足够的权限。如果你手里是“麒麟 wine 助手”或者“统信 wine windows 兼容组件”那种国产 Linux 环境,思路类似,但 FEX-Emu 和 DXMT 的编译配置会有所不同。
内存方面,建议至少 8GB。FEX-Emu 的代码缓存、Wine 的虚拟内存映射、DXMT 的纹理和缓冲区都会占用不少内存。我试过在 6GB 的 iPad 上跑一个中等规模的游戏,内存压力很大,经常触发系统回收,导致卡顿甚至闪退。存储空间也要留够,一个游戏加上着色器缓存,轻松吃掉 10GB 以上。
3.2 编译与安装 FEX-Emu
FEX-Emu 的源码在 GitHub 上,编译需要 CMake 和 Clang。在 Mac 上,我一般用 Homebrew 装依赖:
brew install cmake ninja clang llvm然后克隆源码,切到稳定分支,开始编译:
git clone https://github.com/FEX-Emu/FEX.git cd FEX git checkout FEX-2307 mkdir build && cd build cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ .. ninja编译完成后,你会得到FEXInterpreter和FEXBash两个可执行文件。FEXInterpreter用来直接运行单个 x86-64 程序,FEXBash则提供一个 x86-64 的 shell 环境,方便你连续执行多个命令。我建议先用FEXInterpreter跑一个简单的 x86-64 的hello world来验证翻译层是否工作正常。
注意:编译时如果遇到
llvm相关的链接错误,检查一下LLVM_DIR环境变量是否指向了正确的 LLVM 安装路径。Homebrew 装的 LLVM 通常在/opt/homebrew/opt/llvm下面。
3.3 配置 Wine 与 Gecko
Wine 的编译比 FEX-Emu 要复杂一些,因为要处理 32 位和 64 位的兼容问题。在 Madeira 的场景里,我们主要跑 64 位程序,所以可以只编译 64 位版本。Wine 的源码在 GitLab 上,我一般用wine-8.0.2这个版本,因为它的 Gecko 包比较齐全。
编译命令大概是这样:
./configure --enable-win64 --without-freetype --without-x make -j$(sysctl -n hw.ncpu)--without-freetype和--without-x是为了减少依赖,因为我们在 iOS 或者无头环境下跑,不需要 X11 的图形界面。编译完成后,把wine64和相关的库文件放到一个目录里,然后设置WINEPREFIX环境变量指向一个空目录,运行wineboot初始化。
Gecko 的安装很简单:去 Wine 的官方下载站找到对应版本的wine-gecko-2.47.4-x86_64.msi,然后用 Wine 自己来安装:
wine64 msiexec /i wine-gecko-2.47.4-x86_64.msi安装完成后,Wine 的share/wine/gecko目录下会出现对应的文件夹。如果你遇到“wine gecko 官方正版下载”找不到的问题,直接去 Wine 的 GitLab release 页面找,不要从第三方站点下,避免版本不匹配。
3.4 DXMT 的编译与集成
DXMT 的源码在 GitHub 上,编译需要 Metal 的开发工具链。在 Mac 上,Xcode 的命令行工具是必须的。编译流程:
git clone https://github.com/3Shain/dxmt.git cd dxmt meson setup build --cross-file build-win64.txt ninja -C build编译完成后,你会得到d3d11.dll、dxgi.dll、winemetal.dll这几个文件。把它们复制到 Wine 的lib/wine/x86_64-windows目录下,覆盖原有的 d3d11 和 dxgi。然后设置环境变量:
export DXMT_ENABLE=1 export DXMT_SHADER_CACHE=1这样 Wine 在加载 d3d11 时就会优先使用 DXMT 而不是内置的 WineD3D。
提示:DXMT 目前对 D3D11 的支持比较完善,D3D12 还在开发中。如果你要跑的游戏是 D3D12 的,可能需要等后续版本,或者尝试用 VKD3D 配合 MoltenVK 的方案,但那条路在 iOS 上更折腾。
4. 常见问题与排查技巧实录
4.1 乱码与字体问题
“wine 乱码”“wine 栏是乱码”是社区里问得最多的问题。根本原因通常是 Wine 找不到合适的中文字体,或者字体替换规则没生效。我的排查步骤是这样的:
第一步,确认系统里有没有中文字体。在 Mac 上,/System/Library/Fonts下面有PingFang.ttc,但 Wine 不一定能直接识别。我一般会把一个开源的字体比如Noto Sans CJK复制到 Wine 的drive_c/windows/Fonts目录下。
第二步,修改 Wine 的注册表。用wine regedit打开注册表编辑器,找到HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\FontSubstitutes,添加以下键值:
| 键名 | 键值 |
|---|---|
| MS Shell Dlg | Noto Sans CJK SC |
| MS Shell Dlg 2 | Noto Sans CJK SC |
| Tahoma | Noto Sans CJK SC |
| SimSun | Noto Sans CJK SC |
第三步,如果还有乱码,检查 Wine 的FontLink设置。有些程序会直接调用SimSun或者NSimSun,需要在HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\FontLink\SystemLink下面把SimSun链接到Noto Sans CJK SC。
4.2 性能卡顿与着色器编译
DXMT 第一次运行游戏时,着色器编译会导致严重的卡顿,有时候一帧要等好几秒。这是正常现象,因为 Metal 编译器在后台工作。我的做法是:第一次进游戏后,不要急着玩,先让游戏在训练模式或者低负载场景下跑几分钟,让着色器缓存建立起来。之后退出游戏,确认~/Library/Caches/dxmt目录下有缓存文件生成,再重新进游戏,卡顿就会明显减少。
如果卡顿持续存在,可能是 FEX-Emu 的代码缓存没命中。FEX-Emu 有一个FEXCore的配置项Core,可以设置为IR或者JIT。JIT模式启动快但优化少,IR模式启动慢但运行效率高。我一般先用JIT跑一遍,让 FEX-Emu 把翻译后的代码缓存到磁盘,然后再切到IR模式,这样后续启动和运行都会快很多。
4.3 应用崩溃与日志分析
Wine 和 FEX-Emu 的崩溃日志有时候很晦涩,但有几个关键信息一定要看。第一,看WINEDEBUG的输出。我一般会设置:
export WINEDEBUG=+seh,+tid,+loaddll这样能看到异常发生时的调用栈和加载的 DLL 列表。如果崩溃发生在某个特定的 DLL 里,比如d3d11.dll或者ntdll.dll,那问题就定位到对应的组件了。
第二,看 FEX-Emu 的日志。FEX-Emu 有一个FEX_LOG_LEVEL环境变量,设置为debug会输出大量翻译相关的信息。如果崩溃发生在某条 x86 指令上,日志里会显示这条指令的地址和翻译后的 ARM64 代码地址,这对定位是 FEX-Emu 的翻译 bug 还是 Wine 的实现问题很有帮助。
注意:
WINEDEBUG的输出量很大,长时间开启会拖慢性能,排查完记得关掉。另外,FEX-Emu 的 debug 日志会占用大量磁盘空间,建议只在复现问题时开启。
4.4 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 界面全是方块 | 缺少中文字体 | 复制 Noto Sans CJK 到 Wine Fonts 目录,配置 FontSubstitutes |
| 启动时报 Gecko 错误 | 未安装 Wine Gecko | 下载对应版本 msi,用 wine msiexec 安装 |
| 游戏卡顿严重 | 着色器未缓存 | 首次运行让游戏跑几分钟,建立 DXMT 缓存 |
| 闪退无日志 | FEX-Emu 翻译错误 | 开启 FEX_LOG_LEVEL=debug,查看崩溃指令地址 |
| 画面撕裂 | Metal 垂直同步未开 | 设置 DXMT_VSYNC=1 |
| 声音异常 | Wine 音频驱动未配 | 设置 WINEDLLOVERRIDES=winepulse.drv=n,或改用 CoreAudio |
5. 进阶玩法与性能调优
5.1 FEX-Emu 的代码缓存优化
FEX-Emu 默认会把翻译后的代码缓存到内存里,但内存有限,缓存满了就会淘汰旧的代码。如果你反复运行同一个程序,可以开启磁盘缓存:
export FEX_APP_CACHE=/path/to/cache这样 FEX-Emu 会把翻译后的代码写到磁盘上,下次启动时直接加载,省去重新翻译的时间。我实测下来,对于一个中等规模的游戏,开启磁盘缓存后第二次启动时间从 40 秒降到了 12 秒左右。
另外,FEX-Emu 有一个Multiblock选项,控制是否把多个基本块合并翻译。开启后翻译效率更高,但代码缓存会变大。如果你的设备存储空间充足,建议开启。
5.2 DXMT 的 Metal 特性利用
DXMT 支持 Metal 的一些高级特性,比如MetalFX超分辨率。如果你的设备是 A15 或者 M2 以上,可以开启:
export DXMT_METALFX=1 export DXMT_METALFX_MODE=2MODE=2是质量优先模式,MODE=1是性能优先。开启后,游戏会在较低分辨率下渲染,然后用 MetalFX 放大到原生分辨率,帧率会有明显提升,画质损失在可接受范围内。
还有一个DXMT_MAX_FRAME_LATENCY选项,控制 GPU 命令队列的深度。默认是 3,调低到 1 可以减少输入延迟,但可能影响帧率稳定性。我一般根据游戏类型来调:格斗游戏和音游调到 1,角色扮演和策略游戏保持 3。
5.3 多任务与后台管理
在 iOS 上跑 Madeira,后台管理很重要。iOS 的内存回收机制很激进,如果 Madeira 占用的内存超过一定阈值,系统会直接杀掉进程。我的做法是:在启动 Madeira 之前,先把其他后台应用全部关掉,尤其是浏览器和社交媒体应用。然后开启 iOS 的“引导式访问”模式,防止误触退出。
如果设备越狱了,可以安装Crane或者AppSync这类工具,为 Madeira 创建独立的数据容器,这样它的缓存和配置不会和其他应用混在一起,也方便备份和迁移。
6. 我个人在实际操作中的体会
折腾 Madeira 这套方案,最大的感受是:每一层翻译都有代价,但每一层也都有优化的空间。FEX-Emu 的指令翻译、Wine 的 API 转换、DXMT 的图形映射,每一层都会引入额外的开销,但通过合理的配置和缓存策略,可以把这些开销降到可接受的范围。
我踩过的最大的坑是字体问题。一开始跑一个中文游戏,界面全是方块,我以为是 DXMT 的渲染问题,查了半天才发现是 Wine 的字体替换没配好。后来我把字体配置写成了一个脚本,每次新建WINEPREFIX就自动执行,省了很多事。
另一个坑是着色器缓存。有一次我删掉了~/Library/Caches/dxmt目录,想清理空间,结果下次进游戏又卡了五分钟。从那以后我就知道,这个目录不能随便删,除非你遇到了渲染错误需要强制重新编译。
最后分享一个小技巧:如果你在 iOS 上跑 Madeira,建议把设备插着电源,并且开启飞行模式。插电是因为 FEX-Emu 和 DXMT 都是计算密集型的,耗电很快;飞行模式是为了减少后台的网络活动和通知干扰,让 CPU 和 GPU 的资源尽可能留给翻译层。这个组合我实测下来,帧率稳定性会好不少。