1. 项目缘起:为什么要在 iOS 上折腾 Wine 这件事
“Madeira”这个项目标题,乍一看像是个地名,但在我们这批折腾跨平台兼容层的人眼里,它指向的是一件事:在 iOS 设备上跑 Windows 应用。热搜词里那一串 Wine、FEX-Emu、DXMT、x86-64、iOS 的组合,已经把技术路线图摊在桌面上了——用 Wine 做 Windows API 转译,用 FEX-Emu 做 x86-64 到 ARM64 的指令翻译,用 DXMT 把 Direct3D 调用翻译成 Metal,最终落在 iOS 这个封闭但性能强悍的平台上。
我先说清楚这个项目适合谁看。如果你只是想在 iPhone 上玩个 Windows 老游戏,那这篇内容会让你少走很多弯路;如果你是做移动端兼容层开发、想理解用户态模拟器怎么在 ARM 设备上跑 x86 程序,那这里面的架构拆解和踩坑记录对你更有价值。我自己是从 Wine 在桌面 Linux 上的乱码问题一路踩过来的,后来转到移动端兼容层,前后折腾了差不多两年,Madeira 这个方向是我认为目前最有意思也最容易被低估的一条路。
核心要解决的问题其实很朴素:iOS 生态里没有官方的 Windows 应用运行环境,而大量行业软件、老游戏、专业工具只有 Windows 版本。传统做法是远程桌面或者云电脑,但这两条路都依赖网络,延迟和隐私都是问题。Madeira 的思路是本地转译——把 x86-64 指令翻译成 ARM64,把 Windows 系统调用翻译成 POSIX 调用,把图形 API 翻译成 Metal。三层翻译叠在一起,听起来很疯狂,但实测下来在 A 系列芯片上跑一些轻量级 Windows 程序是可行的。
注意:本文讨论的所有技术方案均基于公开的兼容层技术原理,不涉及任何规避平台政策的内容。实际部署时请遵守你所在地区的法律法规和平台服务条款。
2. 技术栈拆解:Wine、FEX-Emu、DXMT 各自在干什么
2.1 Wine 的角色:Windows API 的翻译官
Wine 不是模拟器,它是一套兼容层。它把 Windows 的 PE 可执行文件加载进来,然后把里面调用的kernel32.dll、user32.dll、gdi32.dll这些接口,翻译成宿主系统能理解的调用。在 Linux 上它翻译成 glibc 和 X11/Wayland,在 macOS 上翻译成 Mach-O 和 Cocoa,在 iOS 上则要翻译成 Darwin 内核暴露出来的那套 POSIX 接口和 Metal。
这里有个关键点很多人搞混:Wine 本身不负责 CPU 指令翻译。如果你的 Windows 程序是 x86-64 编译的,而你的 iOS 设备是 ARM64,那 Wine 加载完 PE 文件后,第一条 x86 指令就执行不了。所以必须有一个指令翻译层垫在 Wine 和硬件之间,这就是 FEX-Emu 的位置。
Wine 在 iOS 上的另一个难点是文件系统布局。Windows 程序习惯C:\盘符和反斜杠路径,Wine 需要把这些映射到 iOS 沙盒里的目录。我实测下来,把C:映射到应用沙盒的Documents/drive_c是最稳的,因为 iOS 对Documents目录的读写权限最宽松,也方便通过文件 App 往里拷东西。
2.2 FEX-Emu:x86-64 到 ARM64 的实时翻译
FEX-Emu 是一个用户态的 x86-64 模拟器,专门为 ARM64 宿主设计。它的工作方式是动态二进制翻译:程序运行时,FEX 把 x86-64 的基本块翻译成 ARM64 指令,翻译结果缓存起来,下次执行到同一块就直接用缓存。这比逐条解释快得多,实测在 A15 上跑一些老游戏能到可玩帧率。
FEX 的核心优势是它不需要内核模块,完全在用户态运行。这对 iOS 至关重要,因为 iOS 不允许加载内核扩展。FEX 通过syscall拦截和信号处理来模拟 x86 的系统调用行为,虽然有一层额外开销,但换来的是可以在非越狱设备上运行。
参数调优方面,FEX 有几个关键配置项值得关注。FEX_TSOENABLED控制是否启用 x86 的强内存序模拟,开启后兼容性更好但性能下降约 15% 到 20%。FEX_MULTIBLOCK控制多块编译,开启后能提升 10% 左右的吞吐。我的建议是先用默认配置跑通,再根据具体程序的崩溃情况微调。
2.3 DXMT:把 Direct3D 翻译成 Metal
DXMT 是 Direct3D 到 Metal 的翻译层,可以理解为 DXVK 的 Metal 版本。DXVK 在 Linux 上把 D3D9/10/11 翻译成 Vulkan,DXMT 则直接翻译成 Metal。为什么不用 MoltenVK 再转一道?因为多一层转换就多一层开销和 bug,DXMT 直接对接 Metal 能省掉中间环节。
DXMT 目前对 D3D11 的支持比较成熟,D3D12 还在开发中。对于大多数老游戏和轻量级 Windows 应用,D3D9 和 D3D11 已经够用了。实测下来,用 DXMT 跑一些 2010 年代前后的 Windows 游戏,在 iPhone 15 Pro 上能到 30 到 60 帧,具体取决于游戏复杂度和分辨率设置。
提示:DXMT 的着色器编译缓存建议开启,第一次运行游戏时会卡顿几秒到几十秒,之后就会流畅很多。缓存文件放在沙盒的
Library/Caches目录下,注意不要被系统清理掉。
3. 实操环境搭建:从零到跑起第一个 Windows 程序
3.1 基础环境准备与依赖梳理
在 iOS 上搭建这套环境,第一步是准备一个能编译和运行原生代码的开发环境。你需要一台 Mac 作为构建机,Xcode 版本建议 15 以上,因为要用到较新的 Metal 着色器编译工具链。iOS 设备方面,A12 芯片以上的设备比较稳妥,A14 及以上体验会好很多,因为 FEX 的翻译开销对 CPU 单核性能很敏感。
依赖库的获取顺序很重要,我踩过的坑是先把 Wine 编译好了,结果发现 FEX 的静态库版本对不上,又得重新编。正确的顺序是:
- 先编译 FEX-Emu 的 ARM64 版本,产出
libFEXCore.a和相关的头文件。 - 再编译 DXMT,它依赖 Metal 框架和 FEX 提供的一些工具链。
- 最后编译 Wine,在 configure 阶段把 FEX 和 DXMT 的路径指进去。
Wine 的编译配置里,--enable-archs=aarch64,x86_64这个参数要加上,因为我们要同时支持 ARM64 宿主和 x86-64 客户机。--with-fex指向 FEX 的安装路径,--with-dxmt指向 DXMT 的路径。如果编译过程中报找不到libMetal之类的错误,检查 Xcode 的 command line tools 是否指向了正确的 SDK。
3.2 Wine 前缀初始化与乱码问题根治
Wine 前缀(prefix)就是那个模拟的C:\盘。初始化命令是wineboot -u,但在 iOS 上直接跑会报一堆错,因为很多 Windows 服务在 iOS 沙盒里没法启动。我的做法是先用一个精简的wine.inf替换默认的,跳过那些不必要的服务初始化。
乱码问题是 Wine 在中文环境下的老毛病了。热搜词里“wine 乱码”和“wine 栏是乱码”说明很多人被这个问题卡住。根本原因是 Wine 默认的字体替换表里,中文字体映射到了不存在的字体上。解决办法是在注册表里手动指定字体替换:
# 在 Wine 前缀的注册表中添加字体替换项 wine reg add "HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\FontSubstitutes" /v "MS Shell Dlg" /t REG_SZ /d "Noto Sans CJK SC" /f wine reg add "HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\FontSubstitutes" /v "MS Shell Dlg 2" /t REG_SZ /d "Noto Sans CJK SC" /f然后把Noto Sans CJK SC的 TTF 文件拷到前缀的drive_c/windows/Fonts目录下。实测这一步做完,大部分中文程序的界面乱码就消失了。如果还有个别程序乱码,那多半是程序自己带了字体文件但没正确加载,可以试试用winetricks装cjkfonts包。
注意:iOS 沙盒对字体文件的加载路径有要求,必须放在
drive_c/windows/Fonts下,放在其他目录 Wine 找不到。
3.3 FEX-Emu 的配置与性能调优
FEX 的配置文件放在~/.fex-emu/Config.json,在 iOS 上对应沙盒的Documents/.fex-emu/Config.json。关键配置项如下:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
TSOEnabled | true | 开启强内存序,兼容性好,性能降 15% 左右 |
Multiblock | true | 多块编译,提升吞吐约 10% |
SMCChecks | mtrack | 自修改代码检测,用 mtrack 模式平衡性能和兼容性 |
X87ReducedPrecision | false | 保持 x87 全精度,避免某些老程序计算错误 |
RootFS | ./rootfs | 根文件系统路径,指向沙盒内的目录 |
性能调优的核心思路是减少翻译开销。FEX 有一个-c参数可以指定只翻译特定区域的代码,对于已知热点函数的程序,可以用这个参数把翻译范围缩小。另外,FEX_APP_CONFIG环境变量可以针对单个程序覆盖全局配置,比如某个游戏对 TSO 不敏感,就可以单独关掉 TSO 换性能。
我实测下来,在 iPhone 15 Pro 上跑一个典型的 D3D11 游戏,FEX 的翻译开销大约占 CPU 时间的 30% 到 40%。也就是说,如果原生 ARM64 能跑 60 帧,经过 FEX 翻译后大概能跑 36 到 42 帧。这个损耗是用户态翻译的物理极限,除非用硬件虚拟化,但 iOS 不开放这个能力。
4. 图形与输入:DXMT 渲染管线与 iOS 触控映射
4.1 DXMT 渲染管线的搭建与调试
DXMT 的工作流程是:Wine 里的 D3D 调用被 DXMT 拦截,转换成 Metal 的MTLCommandBuffer和MTLRenderCommandEncoder,然后提交给 GPU。这个转换过程中,着色器编译是最耗时的环节。DXMT 会把 D3D 的 HLSL 着色器编译成 Metal 的 AIR 格式,再进一步编译成 GPU 机器码。
调试 DXMT 的时候,DXMT_LOG_LEVEL环境变量很有用。设成debug会输出详细的着色器编译日志和管线状态信息,设成warn只输出警告和错误。我一般先用debug跑一遍,看看有没有着色器编译失败或者管线状态不匹配的问题,然后再切回warn正常使用。
一个常见的坑是纹理格式不匹配。D3D 的DXGI_FORMAT_B8G8R8A8_UNORM在 Metal 里没有直接对应的格式,DXMT 需要做一次 swizzle。如果某个游戏的纹理显示颜色不对,多半是 swizzle 出了问题。可以在 DXMT 的配置里强制指定纹理格式转换规则,或者等 DXMT 更新修复。
4.2 iOS 触控到 Windows 鼠标键盘的映射
Windows 程序期望的是鼠标和键盘输入,而 iOS 只有触摸屏。Madeira 需要做一层输入映射:单指触摸模拟鼠标移动,轻点模拟左键单击,长按模拟右键,双指捏合模拟滚轮。键盘输入则通过 iOS 的软键盘或者外接蓝牙键盘来传递。
映射的难点在于精度和延迟。触摸屏的坐标精度比鼠标低,而且手指会遮挡屏幕。我的做法是加一个虚拟光标,手指在屏幕上滑动时,光标按相对位移移动,而不是直接跳到手指位置。这样虽然不如直接触摸直观,但精度高很多,适合需要精细操作的程序。
外接键盘的映射相对简单,iOS 支持蓝牙键盘,Wine 能直接读取键盘事件。但要注意键位映射,Windows 的VK_*虚拟键码和 iOS 的UIKey不是一一对应的,需要在 Wine 的驱动层做转换。我整理了一份常用键位的映射表,放在项目的docs/keymap.md里,有需要的可以自取。
提示:如果程序需要鼠标滚轮,双指上下滑动模拟滚轮在大多数情况下够用,但有些程序对滚轮事件的分辨率有要求,可能需要在映射层做平滑处理。
5. 常见问题与排查实录
5.1 程序启动崩溃的排查思路
程序启动就崩溃是最常见的问题,原因可能出在 FEX、Wine、DXMT 任何一层。我的排查顺序是:
- 先看 FEX 日志。设
FEX_LOG_LEVEL=debug,看有没有非法指令或者内存访问错误。如果 FEX 报SIGILL,说明遇到了不支持的 x86 指令,需要更新 FEX 版本或者给 FEX 提 issue。 - 再看 Wine 日志。设
WINEDEBUG=+loaddll,+module,看 PE 文件加载是否正常。如果某个 DLL 加载失败,检查是不是缺了依赖库。 - 最后看 DXMT 日志。如果程序能启动但黑屏或者花屏,多半是 DXMT 的问题。设
DXMT_LOG_LEVEL=debug,看着色器编译和管线创建有没有报错。
我遇到过最诡异的一个崩溃是程序在WinMain之前就挂了,最后发现是 FEX 的 TSO 模拟和程序的某个原子操作不兼容。关掉 TSO 就好了,但性能降了不少。这种问题没有通用解法,只能具体程序具体分析。
5.2 性能瓶颈的定位与优化
性能问题通常表现为帧率低或者操作延迟高。定位瓶颈可以用Instruments工具,Time Profiler 看 CPU 热点,Metal System Trace 看 GPU 利用率。如果 CPU 占用高但 GPU 空闲,瓶颈在 FEX 翻译或者 Wine 的 API 转换;如果 GPU 占用高,瓶颈在 DXMT 的渲染管线。
优化手段按性价比排序:
- 降低分辨率:最直接有效,渲染分辨率减半,帧率大概能翻倍。
- 关闭 TSO:如果程序不依赖强内存序,关掉能提升 15% 到 20%。
- 启用多块编译:FEX 的
Multiblock开启,提升约 10%。 - 减少着色器编译:DXMT 的着色器缓存要保留,避免每次启动都重新编译。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 程序启动即崩溃 | FEX 不支持某条指令 | 看 FEX 日志有无 SIGILL | 更新 FEX 或提 issue |
| 界面中文乱码 | 字体替换未配置 | 检查注册表 FontSubstitutes | 添加字体替换项并拷贝字体 |
| 黑屏但音频正常 | DXMT 渲染管线失败 | 看 DXMT 日志着色器编译 | 更新 DXMT 或换 D3D 版本 |
| 帧率极低 | TSO 开启或分辨率过高 | Instruments 看 CPU 热点 | 关 TSO 或降分辨率 |
| 触摸操作不准 | 输入映射精度不足 | 检查虚拟光标配置 | 调整光标移动灵敏度 |
| 程序找不到文件 | 路径映射错误 | 检查 drive_c 目录结构 | 修正路径映射规则 |
6. 一些实操心得与后续可扩展的方向
折腾这套东西两年多,我最大的体会是:兼容层的问题,90% 出在细节上,而不是架构上。架构层面 Wine + FEX + DXMT 这套组合已经被验证可行了,但每个程序都有自己的一套怪癖,需要逐个去磨。我建议新手不要一上来就挑战大型商业软件,先从notepad.exe和winmine.exe这种自带的小程序开始,跑通了再逐步加复杂度。
另一个心得是日志是你的朋友。FEX、Wine、DXMT 三层都有详细的日志输出,遇到问题先把日志打开,逐层排查。我见过很多人一遇到崩溃就到处问,其实日志里已经把原因写得很清楚了。
后续可以扩展的方向有几个。一是D3D12 支持,DXMT 的 D3D12 后端还在开发中,等成熟了能跑更多现代游戏。二是ARM64 原生 Windows 程序支持,如果 Windows 程序本身是 ARM64 编译的,就不需要 FEX 翻译了,性能会好很多。三是输入映射的智能化,比如根据程序类型自动切换触摸映射方案,游戏用虚拟摇杆,办公软件用虚拟光标。
最后分享一个小技巧:如果你在 iOS 上跑 Windows 程序时遇到音频延迟,可以试试把 Wine 的音频驱动从winealsa换成winecoreaudio,在 iOS 上 coreaudio 的延迟比 ALSA 模拟低不少。这个配置在winecfg的音频选项卡里改,改完重启程序生效。