
1. 项目缘起为什么我要折腾 Madeira第一次看到“Madeira”这个词很多人会以为是葡萄牙那个产葡萄酒的岛屿。但在我们这行尤其是最近这波热搜词里Madeira 指向的是一个非常具体的技术方向在非 x86 平台上跑 x86-64 的 Windows 应用。热搜词里同时出现了 FEX-Emu、Wine、DXMT、iOS、x86-64这几个词凑在一起基本就把 Madeira 的轮廓勾出来了——它是一套围绕 FEX-Emu 做指令翻译、再叠 Wine 做 Windows API 兼容、最后用 DXMT 把 DirectX 调用翻译到 Metal 上的完整链路。我最早接触这个组合是因为手头有一台 ARM 架构的机器想跑一些只有 Windows 版的工具软件。纯 Wine 在 ARM 上跑 x86-64 的 PE 文件是跑不起来的因为指令集对不上。FEX-Emu 解决的就是这个问题它把 x86-64 指令动态翻译成 ARM64 指令。但光有 FEX-Emu 还不够Windows 程序依赖大量的系统 DLL 和图形接口这就需要 Wine 来提供 Windows API 层需要 DXMT 来把 D3D 调用转成 Metal。Madeira 本质上就是把这几个组件串起来的一套集成方案。这篇文章适合谁看如果你正在 ARM 设备上尝试运行 Windows 应用或者你对 FEX-Emu、Wine、DXMT 这套组合的协作方式感兴趣再或者你只是被热搜里那些“wine 乱码”“麒麟 wine 助手”之类的词勾起了好奇心那这篇内容应该能给你一些可以直接抄作业的东西。我会把整个链路的原理、配置、踩坑点都摊开讲不藏私。2. 核心组件拆解FEX-Emu、Wine、DXMT 各自在干什么2.1 FEX-Emux86-64 到 ARM64 的指令翻译层FEX-Emu 是整个链路的地基。它的工作方式是指令级动态翻译当 x86-64 程序执行到一段代码时FEX-Emu 把这段 x86-64 指令翻译成等价的 ARM64 指令然后让 CPU 去执行。这个过程是带缓存的翻译过的代码块会被存起来下次执行到同一段就直接用缓存不用重新翻译。为什么不用 QEMU 那种全系统模拟因为全系统模拟太重了它要模拟整个硬件环境包括 CPU、内存、外设性能损耗非常大。FEX-Emu 走的是用户态翻译路线它只翻译应用程序本身的指令系统调用直接透传给宿主系统。这样性能损耗小得多实测下来CPU 密集型任务的性能大概能到原生执行的 60% 到 80%具体取决于代码特征。FEX-Emu 有几个关键配置项需要关注。FEX_APP_CONFIG环境变量可以指定配置文件路径里面能调 JIT 缓存大小、TSO 内存模型模拟开关等。TSO 是 x86 的强内存模型ARM 是弱内存模型有些多线程程序依赖 TSO 才能正确运行但开启 TSO 模拟会带来性能下降。我的经验是单线程程序关掉 TSO多线程程序如果出现随机崩溃再打开。2.2 WineWindows API 的兼容层Wine 不是模拟器它是一套重新实现的 Windows API。当 Windows 程序调用CreateWindowEx的时候Wine 把这个调用翻译成宿主系统的窗口创建调用。在 Madeira 这套方案里Wine 跑在 FEX-Emu 之上也就是说 Wine 本身也是被翻译执行的 x86-64 代码。这里有个容易混淆的点Wine 有 32 位和 64 位之分FEX-Emu 也有 32 位和 64 位之分。如果你要跑 32 位的 Windows 程序需要 FEX-Emu 的 32 位版本加上 Wine 的 32 位版本。但热搜词里明确写了 x86-64所以我们主要讨论 64 位链路。64 位链路相对干净因为不需要处理 WoW64 那套东西。Wine 的配置核心是WINEPREFIX。每个 prefix 是一个独立的 Windows 环境里面有独立的注册表、独立的 C 盘目录。我建议每个应用单独一个 prefix避免 DLL 冲突。创建 prefix 的命令是WINEPREFIX~/.wine-myapp wineboot -u这个命令会初始化一个全新的 Windows 环境。2.3 DXMTDirect3D 到 Metal 的翻译层DXMT 是让 Windows 游戏和图形程序能在 Metal 上跑起来的关键。它的工作方式是拦截 D3D11 和 D3D12 的调用翻译成 Metal API 调用。为什么不用 DXVK因为 DXVK 是把 D3D 翻译成 Vulkan而 Vulkan 在 ARM 设备上的驱动支持参差不齐Metal 是更稳妥的选择。DXMT 目前对 D3D11 的支持比较成熟D3D12 还在完善中。如果你跑的是 D3D9 的老游戏可能需要配合 DXVK 或者 Wine 自带的 D3D9 实现。DXMT 的配置主要通过DXMT_CONFIG环境变量可以指定日志级别、着色器缓存路径等。着色器缓存特别重要第一次运行游戏时会编译大量着色器过程很慢但缓存之后第二次启动就快很多。3. 环境搭建实操从零把 Madeira 跑起来3.1 基础依赖安装假设你用的是一台 ARM64 的 Linux 设备比如某种国产化平台或者开发板。第一步是装编译工具链和基础库。以 Debian 系为例sudo apt update sudo apt install -y build-essential cmake ninja-build git python3 pkg-config sudo apt install -y libsdl2-dev libepoxy-dev libdrm-dev libgbm-dev sudo apt install -y libasound2-dev libpulse-dev libudev-dev这些依赖里SDL2 和 epoxy 是 FEX-Emu 和 Wine 都要用的drm 和 gbm 是图形输出相关的asound 和 pulse 是音频。少装一个都可能在编译或者运行时报错我踩过这个坑当时漏了 libgbm-dev结果 DXMT 初始化直接失败日志里只报了一个很模糊的“failed to create gbm device”。3.2 编译 FEX-EmuFEX-Emu 的编译比较吃资源建议至少 4 核 8G 内存。编译命令git clone https://github.com/FEX-Emu/FEX.git cd FEX git submodule update --init --recursive mkdir build cd build cmake -DCMAKE_BUILD_TYPERelease -DENABLE_ASSERTIONSOFF .. make -j$(nproc) sudo make installENABLE_ASSERTIONSOFF这个选项很重要打开的话性能会掉一大截因为每个翻译块都要做断言检查。Release 模式加上关断言是性能最优的组合。编译完成后FEXInterpreter这个可执行文件就是核心。3.3 配置 Wine 和 DXMTWine 建议用较新的版本至少 8.0 以上因为新版本对 ARM64 上的 FEX-Emu 支持更好。如果你用的发行版自带 Wine 版本太老可以考虑自己编译或者用第三方构建。DXMT 的安装相对简单下载预编译的二进制包把dxmt.dll、d3d11.dll、dxgi.dll放到 Wine prefix 的system32目录下然后在 Wine 注册表里做 DLL 覆盖WINEPREFIX~/.wine-myapp wine reg add HKCU\Software\Wine\DllOverrides /v d3d11 /t REG_SZ /d native /f WINEPREFIX~/.wine-myapp wine reg add HKCU\Software\Wine\DllOverrides /v dxgi /t REG_SZ /d native /f这两条命令的意思是让 Wine 优先加载 DXMT 提供的 d3d11.dll 和 dxgi.dll而不是 Wine 自带的实现。如果不做这个覆盖DXMT 根本不会被加载程序会走 Wine 自带的 D3D 实现在 ARM 上性能很差甚至直接崩溃。3.4 启动应用启动一个 Windows 程序完整命令是这样的FEX_APP_CONFIG~/.fex-config.json \ WINEPREFIX~/.wine-myapp \ DXMT_CONFIG~/.dxmt-config.json \ FEXInterpreter wine /path/to/app.exe这里FEXInterpreter负责翻译 wine 本身以及后续加载的 exeWine 负责提供 Windows APIDXMT 负责图形翻译。三层各司其职缺一不可。4. 常见问题与排查技巧实录4.1 Wine 乱码问题热搜里“wine 乱码”这个词出现频率很高我实际也遇到过。乱码通常出现在两个地方一是程序界面上的中文显示成方块或者问号二是终端里 Wine 输出的日志乱码。界面乱码的原因是 Wine prefix 里缺少中文字体。解决办法是把宿主系统的中文字体复制到 prefix 的字体目录cp /usr/share/fonts/truetype/wqy/wqy-microhei.ttc ~/.wine-myapp/drive_c/windows/Fonts/然后注册字体WINEPREFIX~/.wine-myapp wine reg add HKLM\Software\Microsoft\Windows NT\CurrentVersion\Fonts /v WenQuanYi Micro Hei /t REG_SZ /d wqy-microhei.ttc /f终端日志乱码一般是 locale 设置问题确保LANG和LC_ALL设置成zh_CN.UTF-8或者en_US.UTF-8。4.2 DXMT 初始化失败DXMT 初始化失败最常见的原因是 Metal 设备创建不了。在 Linux 上Metal 是通过某种兼容层提供的如果这个兼容层没装好或者版本不对DXMT 就会报错。排查方法是先单独跑一个 Metal 测试程序确认 Metal 可用再跑 DXMT。另一个原因是着色器缓存目录没有写权限。DXMT 默认把缓存放在~/.cache/dxmt如果这个目录不存在或者权限不对初始化会失败。手动创建并给写权限mkdir -p ~/.cache/dxmt chmod 755 ~/.cache/dxmt4.3 FEX-Emu 性能调优FEX-Emu 的性能调优有几个关键点。第一是 JIT 缓存大小默认可能偏小可以在配置文件里调大{ JITCacheSize: 268435456, TSOEnabled: false, SMCChecks: mtrack }JITCacheSize单位是字节256MB 是个比较稳妥的值。TSOEnabled前面说过单线程程序关掉。SMCChecks是自修改代码检查策略mtrack是性能比较好的选项。第二是 CPU 亲和性。FEX-Emu 是多线程的翻译线程和执行线程最好绑到不同的核心上避免争抢。可以用taskset来绑核taskset -c 0-3 FEXInterpreter wine app.exe4.4 常见问题速查表问题现象可能原因排查方法解决方案程序启动即崩溃FEX-Emu 未正确翻译查看 FEX 日志检查 FEX 版本和配置界面中文乱码prefix 缺中文字体检查 Fonts 目录复制字体并注册图形花屏DXMT 未加载检查 DLL 覆盖设置 d3d11/dxgi 为 native音频无声PulseAudio 未连接检查 PULSE_SERVER设置正确的音频后端性能极低TSO 模拟开启查看 FEX 配置关闭 TSOEnabled着色器编译卡顿缓存未生效检查缓存目录确保缓存目录可写5. 进阶玩法把 Madeira 用到实际场景里5.1 跑 Windows 工具软件最常见的场景是跑一些只有 Windows 版的工具软件比如某些嵌入式开发工具、老版本的办公软件。这类软件通常对图形要求不高DXMT 的压力不大主要考验 FEX-Emu 的翻译效率。我的经验是这类软件跑起来基本流畅偶尔有卡顿但可以接受。配置上这类软件建议单独一个 prefix并且把WINEDLLOVERRIDES设置得保守一些不要盲目覆盖所有 DLL。有些软件依赖 Wine 自带的 msvcrt如果你把它覆盖成 native 版本反而会崩溃。5.2 跑轻量级 Windows 游戏D3D11 的游戏在 DXMT 上跑效果取决于游戏对 GPU 的依赖程度。2D 游戏和轻量级 3D 游戏基本没问题重 3D 游戏帧率会明显下降。我实测过一个 D3D11 的独立游戏在 ARM 设备上能跑到 30 帧左右勉强可玩。游戏场景下着色器缓存特别重要。第一次运行会卡很久因为要编译着色器。建议第一次运行的时候耐心等让缓存完整生成。缓存文件在~/.cache/dxmt下可以备份换设备或者重装系统后直接恢复。5.3 与国产化平台的结合热搜里出现了“麒麟 wine 助手”“统信 wine windows 兼容组件下载”这些词说明国产化平台对 Windows 兼容的需求很旺盛。Madeira 这套方案在国产化 ARM 平台上同样适用而且因为 FEX-Emu 和 Wine 都是开源的可以深度定制。在国产化平台上部署需要注意的是系统自带的库版本可能比较老需要手动升级一些依赖。另外国产化平台通常有安全策略限制可能需要调整 SELinux 或者 AppArmor 策略才能让 FEX-Emu 正常执行 JIT 代码。6. 我踩过的坑和总结的经验第一个坑是 FEX-Emu 和 Wine 的版本匹配。FEX-Emu 更新很快有些新版本对 Wine 的某些调用翻译有问题导致 Wine 启动就崩溃。我的建议是锁定一个已知稳定的组合不要盲目追新。我目前用的是 FEX-Emu 某个稳定 tag 加上 Wine 8.0.2跑了几个月没出过大问题。第二个坑是 DXMT 的着色器缓存路径。默认路径在~/.cache下但有些系统的~/.cache是 tmpfs重启就没了。结果每次重启后第一次跑游戏都要重新编译着色器慢得让人抓狂。解决办法是把缓存路径改到持久化存储上在 DXMT 配置里指定ShaderCachePath。第三个坑是音频延迟。Wine 默认的音频后端在某些 ARM 设备上延迟很高玩游戏时音画不同步。解决办法是换成 PulseAudio 后端并且在 Wine 配置里调小缓冲区。具体是在winecfg的音频选项卡里把缓冲区大小从默认的 100ms 调到 50ms 甚至 30ms。第四个坑是输入法。在 Wine 里用中文输入法候选框经常不跟随光标或者干脆不显示。这个问题目前没有完美的解决办法我的做法是在需要大量中文输入的场景下先在宿主系统里输入好再粘贴到 Wine 程序里。虽然麻烦但比跟输入法较劲省心。这套 Madeira 方案本质上是一个“翻译套翻译”的架构x86-64 指令翻译成 ARM64Windows API 翻译成 POSIX APIDirect3D 翻译成 Metal。每一层翻译都有性能损耗但叠在一起之后仍然能让很多 Windows 程序在 ARM 设备上可用这本身就是一件很了不起的事情。如果你也在折腾类似的东西希望这篇内容能帮你少走点弯路。