ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

iOS上通过FEX-Emu与Wine实现x86-64模拟运行Windows游戏

iOS上通过FEX-Emu与Wine实现x86-64模拟运行Windows游戏 1. 项目缘起为什么要在 iOS 上折腾 x86-64 模拟Madeira 这个项目标题乍一看像是个地名但在我们这行里它指向的是一套相当硬核的技术组合在 iOS 设备上通过 FEX-Emu 把 x86-64 指令翻译成 ARM64 指令再叠加 Wine 和 DXMT 把 Windows 游戏跑起来。这套链路听起来像是天方夜谭但确实是近几年移动端兼容层领域最值得关注的方向之一。先说清楚这个项目到底解决什么问题。iOS 设备的芯片是 ARM 架构而大量 PC 游戏和 Windows 应用是 x86-64 架构编译的。两者指令集完全不同就像一个人只会说中文另一个人只会说葡萄牙语中间必须有个翻译。FEX-Emu 就是这个翻译官它做的是动态二进制翻译——把 x86-64 的机器指令实时转换成 ARM64 能执行的指令。Wine 则负责把 Windows 的 API 调用翻译成 POSIX 调用DXMT 再把 DirectX 调用翻译成 Metal。三层翻译叠在一起才能让一个 Windows 游戏在 iPhone 或 iPad 上跑起来。这套方案适合谁说实话不适合纯小白。你需要对 iOS 开发流程有基本了解知道怎么签名、怎么侧载、怎么看系统日志。但如果你是个喜欢折腾的开发者或者想在移动端做 Windows 兼容层相关的研究这个项目的思路和踩坑经验对你会有直接帮助。我前后在这套链路上花了大概三个月从完全跑不起来到能稳定运行部分游戏中间踩的坑足够写一篇长文。注意本文讨论的所有操作均基于公开的开发工具和技术文档仅用于技术研究和学习目的。涉及的系统操作请确保在合法合规的前提下进行。2. 整体架构拆解三层翻译到底怎么串起来2.1 FEX-Emu 的角色与工作原理FEX-Emu 是整个链路的地基。它的核心是一个JIT 编译器运行时会扫描 x86-64 的代码块把它们翻译成 ARM64 指令然后缓存起来。第一次执行某段代码时会慢因为要翻译后续再执行同一段代码就直接走缓存速度会快很多。这里有个关键概念叫block linking。FEX-Emu 会把经常连续执行的代码块链接在一起减少查找和跳转的开销。我实测下来一个典型的游戏循环第一次跑可能只有 10 帧等 JIT 缓存热起来之后能到 30 帧以上。所以如果你刚启动游戏觉得卡得没法玩先别急着下结论让它跑个几分钟再说。FEX-Emu 在 iOS 上的移植有几个特殊之处。iOS 对JIT 权限管得很严普通应用根本没有执行动态生成代码的权限。这就意味着你不能直接把 FEX-Emu 编译成一个普通的 iOS App 就跑起来。常见的做法是借助开发者的调试权限或者利用某些允许 JIT 的运行环境。具体怎么获取这个权限不同 iOS 版本差异很大后面会细说。另一个坑是内存管理。x86-64 和 ARM64 的内存模型有差异特别是涉及到原子操作和内存屏障的时候。FEX-Emu 内部有一套自己的内存管理机制但在 iOS 的沙盒环境下某些系统调用会被限制导致翻译后的代码行为异常。我遇到过最诡异的一个 bug 是游戏里的物理引擎偶尔会抽风角色穿墙查了半天发现是某个原子操作在翻译后失去了原有的语义。2.2 Wine 的适配层从 Windows API 到 POSIXWine 在这套链路里的位置是中间层。FEX-Emu 负责指令翻译但 Windows 程序不光是机器指令它还调用大量的 Windows API——文件操作、窗口管理、图形渲染、音频输出等等。Wine 的工作就是把这些 API 调用拦截下来转换成底层系统能理解的调用。在 iOS 上跑 Wine 有个天然优势iOS 底层是 Darwin和 macOS 同源而 Wine 在 macOS 上已经有很多成熟的适配。但劣势也很明显iOS 的沙盒限制比 macOS 严格得多很多 Wine 依赖的系统功能在 iOS 上要么不存在要么需要特殊权限。我在这部分遇到的最大问题是文件系统映射。Wine 默认会把 Windows 的 C 盘映射到某个目录但 iOS 的应用沙盒里你能访问的路径非常有限。解决方案是在 Wine 的注册表里手动配置路径映射把游戏需要的目录挂载到沙盒内可访问的位置。具体操作是在user.reg里添加 drive 映射项这个后面实操部分会给出具体配置。还有一个经常被忽略的点是字符编码。热词里有人提到wine 乱码这通常是因为 Wine 默认的代码页和游戏期望的不一致。Windows 游戏很多是 GBK 或者 Shift-JIS 编码而 Wine 在非中文 locale 下默认用 UTF-8导致界面文字全变成方块或者问号。解决办法是在 Wine 环境里设置正确的 locale或者用winecfg调整代码页。2.3 DXMT把 DirectX 调用翻译成 MetalDXMT 是这套链路里最年轻的一环它的前身是 DXVK把 DirectX 翻译成 Vulkan但 iOS 上没有 Vulkan只有 Metal所以 DXMT 做的是 DirectX 到 Metal 的翻译。这个翻译层的复杂度在于图形管线的差异。DirectX 和 Metal 虽然都是图形 API但资源管理、着色器编译、渲染状态设置的模型都不一样。DXMT 需要维护一套内部的状态机把 DirectX 的调用序列转换成 Metal 能理解的命令缓冲区。我实测下来DXMT 对 DirectX 9 和 DirectX 11 的支持相对成熟DirectX 12 还在早期阶段。如果你要跑的游戏是 DX9 时代的比如很多经典老游戏成功率会高很多。DX11 的游戏大部分能进界面但复杂场景下可能会有渲染错误或者性能骤降。DX12 的游戏目前基本不用想能启动就算运气好。提示DXMT 的着色器编译是异步的游戏刚启动时可能会卡顿几秒到几十秒这是正常现象。等着色器缓存建立起来之后就会流畅很多。3. iOS 环境准备从开发者模式到 JIT 权限3.1 开发者模式的开启与注意事项在 iOS 上做任何非 App Store 的部署第一步都是开启开发者模式。这个选项在设置里默认是隐藏的需要先通过 Xcode 或者第三方工具把设备标记为开发设备然后才能在设置里看到。具体路径是设置 → 隐私与安全性 → 开发者模式。打开之后设备会重启重启后会弹窗确认。这里有个坑开发者模式开启后设备的安全性会降低某些银行类 App 会检测到这个状态并拒绝运行。如果你主力机要日常使用建议用备用机来做这些实验。另外不同 iOS 版本对开发者模式的处理不一样。比较新的版本比如 iOS 16 以后对开发者模式的管控更严格开启后会有持续的系统提示。而且如果你长时间不连接开发工具开发者模式可能会自动关闭需要重新开启。3.2 JIT 权限的获取思路这是整个项目最核心也最困难的部分。iOS 默认不允许应用执行动态生成的代码这是W^X 策略Write XOR Execute——内存页要么可写要么可执行不能同时可写可执行。FEX-Emu 的 JIT 需要不断生成新代码并执行所以必须绕过这个限制。常见的思路有几种。一种是利用调试器附加的权限当应用被调试器附加时系统会放宽某些限制。另一种是利用特定的 entitlement某些企业级或者开发者的签名可以包含允许 JIT 的权限。还有一种是在越狱环境下直接修改系统策略但这个门槛更高而且不同 iOS 版本的越狱工具差异很大。我个人的建议是如果你只是想做技术验证优先考虑调试器附加的方案。这个方案不需要越狱只需要一台 Mac 和 Xcode。具体操作是在 Xcode 里配置好调试环境然后把 FEX-Emu 的宿主应用以 debug 模式启动。这样应用就获得了被调试的权限JIT 也能正常工作。但这个方法有个明显的缺点应用必须保持调试连接。一旦断开调试器JIT 权限就没了应用会崩溃。所以它只适合开发和测试阶段不适合日常使用。3.3 签名与侧载的实操细节把编译好的应用装到 iOS 设备上需要经过签名。苹果的签名机制要求每个应用都必须有有效的证书和描述文件。对于非 App Store 的应用常见的签名方式有个人开发者证书、企业证书、以及各种自签工具。个人开发者证书是最正规的方式但免费账号签名的应用只有 7 天有效期过期后需要重新签名。付费开发者账号是 1 年有效期。企业证书理论上可以签很多设备但苹果对滥用企业证书的打击力度很大证书随时可能被吊销。我在这部分踩过的坑主要是描述文件不匹配。有时候证书没问题但描述文件里的设备 UDID 没包含你的设备或者 entitlement 配置不对都会导致安装失败。排查方法是看 Xcode 的 Devices and Simulators 窗口里的详细日志里面会明确告诉你哪一步出了问题。还有一个细节是Bundle ID 的冲突。如果你之前装过同一个 Bundle ID 的应用新安装的可能会覆盖旧的导致数据丢失。建议在开发阶段用不同的 Bundle ID 后缀来区分不同版本。4. 核心组件编译与配置从源码到可执行文件4.1 FEX-Emu 的交叉编译FEX-Emu 官方主要支持 Linux 和 macOSiOS 的支持需要自己动手。交叉编译的过程大致是在 macOS 上配置好 iOS 的编译工具链然后修改 FEX-Emu 的构建脚本把目标平台改成 iOS。这里最大的难点是依赖库的适配。FEX-Emu 依赖一些底层的系统库这些库在 iOS 上要么不存在要么接口不一样。比如它用到的某些线程和内存管理函数在 iOS 上需要用 Darwin 的对应实现来替换。我建议的做法是先用一个简单的测试程序验证工具链是否配置正确然后再编译 FEX-Emu 本身。测试程序可以就是一个打印 Hello World 的 iOS 应用确保它能正常编译、签名、安装、运行。这一步看起来简单但能帮你排除掉很多环境问题。编译 FEX-Emu 时的关键配置项包括关闭不需要的架构支持只保留 x86-64 到 ARM64 的翻译、启用 JIT 相关的选项、配置好内存分配器的参数。具体的 CMake 参数我会在下面的代码块里给出。# FEX-Emu iOS 交叉编译的关键 CMake 参数示例 cmake .. \ -DCMAKE_SYSTEM_NAMEiOS \ -DCMAKE_OSX_ARCHITECTURESarm64 \ -DCMAKE_OSX_DEPLOYMENT_TARGET15.0 \ -DENABLE_JITON \ -DENABLE_X86_64ON \ -DENABLE_ARM64ON \ -DDISABLE_ASSERTIONSON \ -DCMAKE_BUILD_TYPERelease注意编译过程中如果遇到链接错误大概率是某个系统库的符号找不到。这时候需要检查 FEX-Emu 的源码里对应平台的实现看看是不是需要添加 iOS 特有的桩函数。4.2 Wine 的裁剪与定制完整的 Wine 体积很大包含大量 iOS 上用不到的功能。为了减小体积和提高启动速度需要做裁剪。裁剪的原则是只保留运行目标游戏所需的最小功能集。具体来说可以去掉 DirectX 12 的支持如果只跑 DX9 游戏、去掉打印支持、去掉某些不常用的字体和国际化资源。Wine 的构建系统支持通过配置选项来禁用这些模块。另一个重要的定制是注册表的预配置。Wine 第一次运行时会初始化注册表这个过程在 iOS 上可能很慢而且某些默认配置不适合移动端。我通常会在构建阶段就把一份配置好的注册表打包进去这样首次启动就能直接用。注册表里需要重点配置的项包括显示驱动设为 DXMT、音频驱动设为 CoreAudio、以及前面提到的代码页设置。代码页这块如果游戏是中文的建议把HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Nls\CodePage下的ACP设为936GBKOEMCP也设为936。4.3 DXMT 的 Metal 着色器编译DXMT 的编译相对直接但需要注意Metal 着色器库的生成。DXMT 内部会把 DirectX 的着色器字节码转换成 Metal 的着色器语言然后编译成 Metal 库。这个过程在 iOS 上必须在应用运行时完成因为 Metal 库的编译依赖具体的 GPU 架构。我遇到的一个性能问题是着色器编译的卡顿。游戏第一次遇到某个特效时DXMT 需要现场编译对应的 Metal 着色器这会导致明显的掉帧。解决办法是启用异步编译让着色器在后台线程编译主线程继续渲染。DXMT 有相关的配置项但需要手动开启。还有一个坑是Metal 库的缓存。iOS 会把编译好的 Metal 库缓存起来但缓存有大小限制。如果游戏着色器特别多缓存可能会被清理导致下次启动又要重新编译。我通常会在应用启动时预加载一部分常用着色器减少运行时的卡顿。5. 实操全流程从零到跑起第一个游戏5.1 环境搭建的完整步骤先把整个流程串一遍。你需要一台 Mac用于编译和签名、一台 iOS 设备建议 iPad屏幕大散热好、以及稳定的网络连接用于下载依赖。第一步在 Mac 上安装 Xcode 和命令行工具。Xcode 的版本要和 iOS 设备的系统版本匹配太新的 Xcode 可能不支持旧的 iOS太旧的 Xcode 又可能不支持新的 iOS。我一般用比设备系统晚一到两个大版本的 Xcode兼容性最好。第二步配置 iOS 的编译工具链。如果你用 Homebrew可以安装ios-toolchain相关的包。但更可靠的方式是直接用 Xcode 自带的工具链通过xcrun命令来调用。第三步按顺序编译三个组件先编译 FEX-Emu再编译 Wine最后编译 DXMT。顺序不能乱因为 Wine 依赖 FEX-Emu 的某些头文件DXMT 又依赖 Wine 的配置。第四步把编译好的组件打包成一个 iOS 应用。这个应用的结构大致是主程序负责启动 FEX-Emu 的翻译器然后加载 Wine 的 loaderWine 再加载游戏的可执行文件。第五步签名并安装到设备上。用 Xcode 的ios-deploy或者第三方的侧载工具都可以。5.2 关键配置文件的编写配置文件是这套链路的灵魂。我整理了一份最小可用的配置模板你可以根据自己的需求调整。Wine 的注册表配置user.reg里驱动相关的部分是这样[Software\\Wine\\Drivers] Graphicsdxmt Audiocoreaudio [System\\CurrentControlSet\\Control\\Nls\\CodePage] ACP936 OEMCP936FEX-Emu 的配置文件fex.ini里和性能相关的关键项[Emulation] JITCacheSize268435456 BlockLinkingtrue Multiblocktrue SMCChecksmtrack [Memory] Mmap32BittrueDXMT 的配置dxmt.conf里和渲染相关的[Render] AsyncShaderCompiletrue MaxFrameLatency1 ShaderCachePath/tmp/dxmt_cache这些配置不是一成不变的需要根据具体游戏来调。比如有些游戏对帧延迟敏感MaxFrameLatency就要设小一点有些游戏着色器特别多ShaderCachePath要指向一个空间足够大的目录。5.3 第一个游戏的启动与调试选第一个测试游戏很重要。建议选一个DirectX 9 时代的老游戏最好是 2D 或者简单 3D 的对性能要求不高。比如一些经典的独立游戏或者老 RPG。启动流程是先启动宿主应用等 FEX-Emu 初始化完成然后通过 Wine 的命令行加载游戏的可执行文件。如果一切正常你应该能看到游戏的窗口出现。但大概率不会一次成功。我记录一下我第一次尝试时的排查过程游戏启动后黑屏没有任何输出。查日志发现是 DXMT 初始化失败原因是 Metal 设备创建的时候权限不足。解决办法是在宿主应用的Info.plist里添加UIRequiredDeviceCapabilities的metal项确保系统分配 Metal 设备。第二次尝试游戏能进界面了但文字全是乱码。这就是前面提到的代码页问题改了注册表之后解决。第三次尝试游戏能玩了但帧率只有个位数。查下来是 JIT 缓存太小翻译后的代码频繁被淘汰。把JITCacheSize从 64MB 调到 256MB 之后帧率提升到 20 帧左右。5.4 性能调优的实操记录性能调优是个持续的过程。我总结下来影响性能的主要因素有三个JIT 缓存大小、着色器编译策略、内存分配效率。JIT 缓存前面说过了越大越好但受限于设备内存。iPhone 的内存比较紧张建议不超过 256MBiPad 可以放宽到 512MB。着色器编译策略方面异步编译能显著减少卡顿但会增加 CPU 占用。如果设备发热严重可以改成同步编译用帧率换温度。内存分配效率这块FEX-Emu 默认的内存分配器在 iOS 上表现一般。我试过替换成系统自带的内存分配器性能有提升但稳定性下降。最后还是用回了默认的通过调整参数来优化。实测数据同一个游戏优化前平均 12 帧优化后平均 28 帧。提升主要来自 JIT 缓存扩大和异步着色器编译。6. 常见问题与排查技巧实录6.1 启动失败类问题速查现象可能原因排查方法解决方案应用闪退签名无效或 entitlement 缺失查看设备日志中的 crash report重新签名确保包含 JIT 权限黑屏无输出DXMT 初始化失败检查 Metal 设备创建日志在 Info.plist 添加 metal 能力声明卡在启动画面Wine 注册表配置错误查看 Wine 的 debug 输出检查驱动配置和代码页设置提示缺少 DLLWine 的 DLL 路径未配置检查 WINEPREFIX 环境变量正确设置 WINEPREFIX 和 DLL 路径6.2 运行时的典型故障处理文字乱码是最常见的问题。除了代码页设置还要注意字体的安装。Wine 默认不带中文字体需要把系统的字体文件复制到 Wine 的字体目录或者在注册表里配置字体替换。我通常会把几个常用的中文字体比如思源黑体打包进去然后在注册表里设置FontSubstitutes。音频爆音或者无声通常是 CoreAudio 的缓冲区设置问题。Wine 的音频驱动默认缓冲区可能太小导致 underrun。可以在注册表里把音频缓冲区调大代价是延迟增加。对于游戏来说延迟稍微大一点问题不大爆音更影响体验。游戏手柄不识别iOS 对游戏手柄的支持是通过 GameController 框架Wine 需要把这个框架的输入事件转换成 Windows 的 XInput 或者 DirectInput 事件。这部分 DXMT 和 Wine 都有相关代码但可能需要手动配置。我试过几个主流手柄Xbox 系列的手柄兼容性最好。性能突然下降如果游戏之前跑得好好的突然变卡大概率是热降频。iOS 设备散热能力有限长时间高负载运行会触发降频。解决办法是加散热背夹或者降低游戏的画质设置。我实测下来加一个半导体散热背夹帧率能稳定提升 30% 左右。6.3 独家避坑经验分享第一个坑不要用最新的 iOS 版本。苹果每次更新 iOS 都会收紧一些安全策略导致原本能用的方法失效。我建议用比最新版晚一到两个大版本的 iOS兼容性最好。比如现在最新是 iOS 18那用 iOS 16 或者 17 会比较稳。第二个坑备份你的配置。这套链路的配置项很多调好一个能跑的环境不容易。建议把整个 WINEPREFIX 目录和相关的配置文件都备份下来换设备或者重装的时候直接恢复能省很多时间。第三个坑日志是你的朋友。iOS 的日志系统很完善但默认不显示调试信息。你需要用idevicesyslog或者 Xcode 的 Devices 窗口来查看实时日志。FEX-Emu 和 Wine 都有详细的日志输出遇到问题先看日志能解决 80% 的问题。第四个坑不要贪多。一开始不要想着把所有游戏都跑起来先专注一个游戏把它调通。调通一个之后你会发现很多经验可以复用到其他游戏上。第五个坑注意法律风险。这套技术本身是中性的但用它来运行未经授权的软件可能涉及侵权。请确保你运行的游戏是你合法拥有的并且遵守相关的许可协议。7. 后续扩展方向与个人体会这套链路目前还在快速演进中。FEX-Emu 的 ARM64 后端还在优化DXMT 对 DirectX 12 的支持也在推进。我关注到的一些新方向包括利用 Apple Silicon 的神经引擎加速着色器编译、通过 MetalFX 做超分辨率提升帧率、以及多线程 JIT 翻译减少启动时间。从个人经验来说这套方案目前的状态是能跑但离好用还有距离。如果你是想在移动端玩 Windows 游戏现阶段的体验还比不上原生的移动游戏。但如果你是对兼容层技术感兴趣想研究指令翻译、API 转换、图形管线映射这些底层技术这套链路是个非常好的实验平台。我在这个项目里最大的收获是对跨架构兼容这件事有了更具体的认识。以前觉得翻译就是个软件层的事实际做下来才发现从指令集到内存模型到图形管线每一层都有大量的细节需要处理。每一个能跑起来的游戏背后都是无数个 bug 的修复和参数的调整。最后分享一个小技巧如果你在调试过程中遇到难以定位的问题可以尝试最小化复现。把游戏换成最简单的测试程序把配置精简到最少然后逐步添加功能直到问题复现。这样能快速定位到是哪个组件、哪个配置项导致的问题。这个方法帮我解决了好几个查了很久都没头绪的 bug。
返回列表