
1. 项目缘起从“Madeira”这个名字说起第一次看到“Madeira”这个词很多人第一反应是葡萄牙那个盛产葡萄酒的岛屿。但在我这个常年混迹于系统兼容层和跨平台工具链的老玩家眼里它指向的是另一个东西——一个围绕FEX-Emu、Wine、DXMT构建的 x86-64 应用在 ARM 设备上运行的技术验证项目。说白了就是想办法让那些原本只能在 Windows x86 电脑上跑的软件和游戏在 ARM 架构的设备上也能跑起来而且尽量跑得稳、跑得快。这个方向最近热度不低原因也很直接ARM 设备的性能越来越强从开发板到轻薄本再到移动终端算力已经不是瓶颈真正的瓶颈是软件生态。大量存量应用是 x86-64 指令集编译的直接拿到 ARM 上跑指令集对不上就像你拿一把英制扳手去拧公制螺丝尺寸看着差不多实际上根本咬不住。FEX-Emu 解决的就是指令翻译的问题Wine 解决的是 Windows API 到类 Unix 系统的映射问题DXMT 则是在 Wine 环境下把 Direct3D 调用翻译成 Metal让图形渲染能走通。这三者叠在一起才构成一个相对完整的“在 ARM 设备上跑 Windows x86-64 应用”的链路。我之所以花时间折腾这套东西是因为在实际工作中经常遇到一个尴尬场景手头只有 ARM 设备但某个关键工具只有 Windows 版而且没有 ARM 原生替代。虚拟机方案太重远程桌面又依赖网络于是兼容层方案就成了一个值得认真对待的选项。Madeira 这个项目标题背后其实是一整套关于指令翻译、API 映射、图形后端适配的工程实践。这篇文章我会把整个思路拆开从架构选型到实操配置再到踩过的坑尽量讲透。适合对系统兼容层感兴趣、手头有 ARM 设备、愿意折腾的读者参考。2. 整体架构拆解三层翻译链路是怎么串起来的2.1 为什么是 FEX-Emu Wine DXMT 这个组合要理解 Madeira 的技术选型得先搞清楚每一层在干什么。FEX-Emu 是一个用户态的 x86-64 指令翻译器它把 x86-64 的机器码动态翻译成 ARM64 指令。注意它不是在操作系统层面做虚拟化而是在用户态做二进制翻译所以开销比全系统模拟小得多。Wine 则是一个 Windows API 兼容层它把 Windows 程序调用的 kernel32、user32、ntdll 等 DLL 接口映射到 Linux 或类 Unix 系统的对应实现上。DXMT 是近几年比较活跃的一个项目专门在 Wine 环境下把 D3D11/D3D12 调用翻译成 Metal让 macOS 或支持 Metal 的 ARM 设备能跑 Windows 游戏。这三者的关系可以这样理解FEX-Emu 负责“让 CPU 能读懂指令”Wine 负责“让系统调用能找到对应实现”DXMT 负责“让图形指令能落到 GPU 上”。缺了任何一层链路就断了。比如只有 FEX-Emu 没有 Wine那 Windows 程序调 CreateFile 的时候没人给它翻译成 open程序直接卡死。只有 Wine 没有 FEX-Emu那 x86-64 的二进制在 ARM 上根本加载不了。只有前两者没有 DXMT那游戏能启动但画面出不来或者只能走软件渲染帧率惨不忍睹。我选择这个组合而不是其他方案有几个实际考量。第一FEX-Emu 的社区活跃度在同类项目中算高的issue 响应快RootFS 和 thunk 机制也比较成熟。第二Wine 的兼容性经过这么多年打磨大部分非反作弊的 Windows 应用都能跑而且配置方式灵活。第三DXMT 相比 DXVK 在 Metal 后端上的适配更直接不需要额外装 Vulkan 驱动省去一层转换。当然这个组合也不是没有代价后面我会详细讲性能损耗和兼容性边界。2.2 指令翻译层FEX-Emu 的工作机制与配置要点FEX-Emu 的核心是一个 JIT 编译器。程序启动时它先把 x86-64 的代码块翻译成 ARM64 指令缓存起来后续执行到相同代码块时直接走缓存。这个过程对用户是透明的但配置上有几个关键点需要注意。首先是 RootFS。FEX-Emu 需要一个 x86-64 的根文件系统里面包含基本的库和可执行文件。通常做法是下载一个精简的 x86-64 Linux 发行版 rootfs比如 Ubuntu 或 Debian 的 minimal 版本然后通过 FEX 的 thunk 机制让 ARM 侧的库和 x86 侧的库能互相调用。RootFS 的版本要和宿主系统的 glibc 版本尽量匹配否则会出现符号找不到的问题。我实测下来Ubuntu 22.04 的 rootfs 在大多数 ARM64 宿主上兼容性最好。其次是 thunk 配置。FEX 允许你把某些库调用直接转发到宿主系统的原生库上而不是在 x86 侧重新实现一遍。比如 libGL、libvulkan、libasound 这些走 thunk 能显著降低开销。配置文件通常在~/.fex-emu/Config.json里需要手动指定哪些库走 thunk。这里有个经验图形和音频相关的库尽量走 thunk但涉及系统调用的库要谨慎因为 ABI 差异可能导致崩溃。还有一个容易忽略的点是 CPU 特性模拟。FEX 可以模拟一些 x86 特有的指令集扩展比如 SSE4.2、AVX 等。如果你的目标程序依赖 AVX2而 FEX 默认没开程序可能直接报非法指令。在 Config.json 里可以设置X86Features字段来开启需要的特性。不过要注意模拟 AVX 会带来额外性能开销能不用就不用。2.3 API 映射层Wine 的配置策略与常见乱码问题Wine 这一层的配置核心是 prefix 管理。每个 Wine prefix 相当于一个独立的 Windows 环境里面有自己的注册表、DLL 覆盖设置和文件系统映射。我建议给每个应用单独建一个 prefix而不是所有程序共用一个因为不同程序对 DLL 版本的需求可能冲突。创建 prefix 的命令是WINEPREFIX~/.wine-madeira winecfg第一次运行会初始化环境。DLL 覆盖是 Wine 配置里最关键的部分。默认情况下Wine 会用自己的实现替换 Windows 原版 DLL但有些程序依赖原版 DLL 的特定行为这时候就需要在 winecfg 的 Libraries 标签页里把对应的 DLL 设为 native。比如 msvcp140.dll、vcruntime140.dll 这些 VC 运行库很多时候用原版比用 Wine 内置的兼容性更好。但反过来像 kernel32.dll、ntdll.dll 这种核心系统 DLL绝对不能设为 native否则整个环境会崩。关于热搜词里提到的“wine 乱码”和“wine 栏是乱码”这通常是因为字体缺失或 locale 设置不对。Wine 默认使用宿主系统的字体如果宿主没装中文字体Windows 程序里的中文就会显示成方块。解决办法是安装fonts-wqy-microhei或fonts-noto-cjk这类中文字体包然后在 winecfg 的 Graphics 标签页里把 DPI 设为 96 或 120避免字体渲染异常。另外LANG和LC_ALL环境变量要设成zh_CN.UTF-8否则某些程序的菜单栏会出现乱码。我踩过的坑是只装了字体但没设 locale结果对话框按钮上的中文还是乱码排查了半天才发现是环境变量的问题。2.4 图形翻译层DXMT 的定位与启用条件DXMT 的作用是在 Wine 的 D3D 实现和 Metal 之间搭一座桥。传统方案是 WineD3D 把 D3D 调用转成 OpenGL但 OpenGL 在 ARM 设备上的驱动质量参差不齐而且性能损耗大。DXMT 直接走 Metal绕开了 OpenGL 这一层在支持 Metal 的设备上效率更高。启用 DXMT 需要几个条件。第一宿主系统要支持 Metal这通常意味着是较新版本的 macOS 或者某些支持 Metal 的 ARM Linux 环境。第二Wine 的版本要足够新DXMT 需要 Wine 7.0 以上的 D3D 接口。第三需要把 DXMT 的 DLL 放到 Wine prefix 的 system32 目录下并在 winecfg 里把 d3d11.dll 和 dxgi.dll 设为 native。配置完成后可以用WINEDEBUGdxmt来查看 DXMT 是否正常加载。实测下来DXMT 对 D3D11 的支持比较好大部分独立游戏和工具软件能正常渲染。D3D12 的支持还在完善中部分游戏会出现画面闪烁或崩溃。如果你的目标程序只用到 D3D9那其实用 WineD3D 就够了没必要上 DXMT因为 D3D9 转 OpenGL 的开销本身就不大。3. 实操全流程从零搭建 Madeira 运行环境3.1 宿主环境准备与依赖安装假设你手头是一台 ARM64 设备跑的是 Ubuntu 22.04 或类似发行版。第一步是装基础依赖。FEX-Emu 需要 cmake、ninja、clang 这些编译工具Wine 需要 flex、bison、libfreetype 等开发库DXMT 需要 Metal 相关的头文件。一条命令搞定sudo apt update sudo apt install -y cmake ninja-build clang lld flex bison \ libfreetype6-dev libgnutls28-dev libxml2-dev libxslt1-dev \ libgl1-mesa-dev libvulkan-dev libasound2-dev libpulse-dev \ libdbus-1-dev libudev-dev libsdl2-dev这里有个细节clang 的版本最好用 15 以上因为 FEX-Emu 的某些代码用了较新的 C 特性老版本 clang 编译会报错。如果发行版自带的 clang 太老可以去 LLVM 官网下载预编译包或者用 apt 装clang-15并设置update-alternatives切换默认版本。装完依赖后建议先跑一下clang --version和cmake --version确认版本。我遇到过 cmake 版本低于 3.20 导致 FEX 配置失败的情况升级 cmake 后解决。另外如果宿主是容器环境要注意/dev/dri和/dev/snd设备是否挂载否则图形和音频会出问题。3.2 FEX-Emu 的编译与 RootFS 配置FEX-Emu 的编译流程比较标准git clone https://github.com/FEX-Emu/FEX.git cd FEX git submodule update --init --recursive mkdir build cd build cmake -DCMAKE_BUILD_TYPERelease -DCMAKE_C_COMPILERclang -DCMAKE_CXX_COMPILERclang .. make -j$(nproc) sudo make install编译过程大概需要 20 到 40 分钟取决于设备性能。如果中途报错大概率是子模块没拉全重新跑一遍git submodule update --init --recursive就行。RootFS 的获取有两种方式。一种是直接用 FEX 官方提供的脚本下载另一种是手动下载一个 x86-64 的 rootfs 压缩包解压到~/.fex-emu/RootFS/下。我推荐手动方式因为可以自己选版本。下载 Ubuntu 22.04 的 minimal rootfsmkdir -p ~/.fex-emu/RootFS/Ubuntu_22_04 cd ~/.fex-emu/RootFS/Ubuntu_22_04 wget https://cdimage.ubuntu.com/ubuntu-base/releases/22.04/release/ubuntu-base-22.04-base-amd64.tar.gz sudo tar -xzf ubuntu-base-22.04-base-amd64.tar.gz解压后需要配置一下 rootfs 里的 apt 源和 DNS否则在 FEX 环境里没法装东西。把宿主机的/etc/resolv.conf复制到 rootfs 的/etc/resolv.conf然后把 apt 源换成国内镜像速度会快很多。Config.json 的配置是重点。一个可用的最小配置大概长这样{ Config: { RootFS: Ubuntu_22_04, X86Features: [sse4.2, avx], Thunks: { libGL: host, libvulkan: host, libasound: host } } }这里X86Features我开了 sse4.2 和 avx因为很多现代程序依赖这两个。如果你的目标程序不需要 avx建议关掉能省不少性能。Thunks 里把图形和音频库指向宿主减少翻译开销。3.3 Wine 的编译安装与 Prefix 初始化Wine 的编译比 FEX 更耗时而且依赖更多。如果你不想编译可以直接用发行版自带的 wine但版本可能偏老DXMT 支持不一定完整。我建议至少用 Wine 8.0 以上。编译命令git clone https://github.com/wine-mirror/wine.git cd wine ./configure --enable-win64 --with-vulkan --with-metal make -j$(nproc) sudo make install--with-metal这个选项在 Linux 上可能不被识别因为 Metal 是 macOS 特有的。如果你是在 macOS 上跑这个选项要加上如果是 Linux去掉即可DXMT 会通过其他方式对接图形后端。编译完成后初始化 prefixexport WINEPREFIX~/.wine-madeira export WINEARCHwin64 winecfgwinecfg 会弹出图形界面第一次运行会提示安装 Mono 和 Gecko。这里注意Gecko 是 Wine 内置的 HTML 渲染引擎如果你要跑的程序里有内嵌网页必须装。热搜词里有人问“wine gecko官方正版下载”其实 Wine 会自动从官方源下载不需要手动找。如果下载慢可以设置WINEDLLOVERRIDESmshtml跳过但这样内嵌网页功能会缺失。在 winecfg 的 Libraries 标签页里添加以下覆盖DLL 名称覆盖方式原因d3d11native走 DXMTdxginative走 DXMTmsvcp140nativeVC 运行库兼容性vcruntime140nativeVC 运行库兼容性ucrtbasenative通用 C 运行库设完覆盖后点 Apply然后关掉 winecfg。这时候 prefix 就基本可用了。3.4 DXMT 的集成与图形后端验证DXMT 的集成相对简单把编译好的 DLL 复制到 prefix 的 system32 目录cp dxmt/build/bin/d3d11.dll ~/.wine-madeira/drive_c/windows/system32/ cp dxmt/build/bin/dxgi.dll ~/.wine-madeira/drive_c/windows/system32/然后确认 winecfg 里 d3d11 和 dxgi 的覆盖是 native。验证是否生效可以跑一个简单的 D3D11 测试程序比如dxdiagWINEPREFIX~/.wine-madeira wine dxdiag如果 DXMT 正常加载dxdiag 的 Display 标签页会显示 Metal 相关的渲染器信息。如果显示的是 WineD3D 或者 llvmpipe说明 DXMT 没生效需要检查 DLL 路径和覆盖设置。我实测中发现一个坑某些 ARM 设备的 Metal 驱动对 D3D11 的某些特性支持不完整比如 MSAA 多重采样。如果游戏里开了抗锯齿导致画面异常可以在 DXMT 的配置文件里禁用 MSAA或者把游戏的抗锯齿关掉。DXMT 的配置文件通常在~/.wine-madeira/drive_c/windows/system32/dxmt.conf里面可以调渲染相关的参数。4. 性能调优与兼容性边界4.1 指令翻译的性能损耗与优化手段FEX-Emu 的 JIT 翻译不是零开销的。根据我的实测纯 CPU 密集型任务在 FEX 下的性能大约是原生 ARM64 的 60% 到 80%具体取决于代码的指令集特征。如果代码大量使用 AVX2而 FEX 需要模拟性能可能掉到 40% 以下。图形密集型任务因为大部分时间在 GPU 上CPU 翻译开销占比小帧率损失通常在 10% 到 20%。优化手段有几个方向。第一尽量让目标程序走 thunk 库减少 x86 侧的库调用。第二在 Config.json 里关掉不需要的 X86Features比如程序不用 AVX 就别开。第三FEX 支持多线程翻译确保FEX_CPU_CORES环境变量设成宿主的核心数。第四如果程序是单线程的可以把 FEX 的翻译线程绑到大核上减少调度延迟。还有一个容易被忽略的点是 RootFS 的精简。RootFS 里如果装了太多不必要的包FEX 在加载库的时候会做更多路径查找启动时间会变长。我建议只装目标程序需要的依赖其他一律不装。可以用ldd查看程序依赖哪些库然后按需安装。4.2 Wine 兼容性分级与常见故障处理Wine 的兼容性可以粗略分几档。第一档是纯 Win32 API 的程序比如记事本、计算器这类基本开箱即用。第二档是依赖 VC 运行库和 .NET 的程序需要装对应的运行库但装完通常能跑。第三档是依赖 DirectX 和反作弊的程序这类兼容性最差反作弊系统通常会检测到 Wine 环境并拒绝运行。常见故障里启动即崩溃是最多的。排查思路是先用WINEDEBUGloaddll看加载了哪些 DLL找到最后一个加载失败的。如果是缺 DLL用winetricks装对应的运行库。如果是 DLL 版本冲突调整覆盖设置。如果崩溃发生在图形初始化阶段试试关掉 DXMT用 WineD3D 跑看是不是图形后端的问题。另一个高频问题是中文乱码。前面提过字体和 locale 的配置这里补充一点某些程序的乱码是因为它用了 GBK 编码而不是 UTF-8。这种情况下需要在 Wine 的注册表里把HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Nls\CodePage下的ACP设成936然后重启 prefix。这个操作有风险可能导致其他程序乱码建议只在必要时改。4.3 图形渲染的帧率稳定性调优DXMT 的帧率稳定性受几个因素影响。第一是 Metal 驱动的版本较新的驱动通常对 D3D 翻译的支持更好。第二是 DXMT 的配置参数比如MaxFrameLatency设得太高会导致输入延迟设得太低会导致帧率波动。我一般设成 2 或 3平衡延迟和稳定性。第三是宿主系统的 GPU 调度策略如果宿主同时跑其他图形任务帧率会受影响建议在跑游戏时关掉不必要的图形应用。实测中我发现某些游戏在 DXMT 下会出现周期性卡顿每隔几秒掉帧一次。排查后发现是 Metal 的命令缓冲区提交策略问题在 DXMT 配置里把CommandBufferMode改成shared能缓解。这个参数的具体取值取决于游戏需要多试几次。5. 常见问题速查与避坑经验5.1 启动类问题排查表现象可能原因排查方法解决方式程序启动无反应FEX 未正确加载检查FEX_ROOTFS环境变量确认 RootFS 路径和 Config.json报非法指令X86Features 未开启看错误信息里的指令类型在 Config.json 里加对应特性缺 DLL 报错运行库未安装WINEDEBUGloaddll用 winetricks 装运行库图形初始化失败DXMT 未生效WINEDEBUGdxmt检查 DLL 覆盖和路径中文显示方块字体缺失看程序用的字体名装中文字体并设 locale5.2 运行类问题与性能瓶颈定位运行过程中最常见的问题是卡顿和崩溃。卡顿的定位可以用perf工具看 CPU 热点如果热点在 FEX 的翻译函数里说明翻译开销大考虑关掉不必要的 X86Features。如果热点在 Wine 的 DLL 里可能是某个 API 实现效率低试试换成 native DLL。如果热点在 DXMT 里可能是图形翻译的开销调整 DXMT 配置参数。崩溃的定位相对麻烦因为 FEX Wine DXMT 三层都可能出问题。我的经验是先用WINEDEBUGseh看异常发生在哪一层。如果异常地址在 FEX 的翻译代码里可能是翻译错误试试更新 FEX 版本。如果在 Wine 的 DLL 里可能是 API 实现不完整试试换 DLL 覆盖。如果在 DXMT 里可能是图形指令翻译错误试试换 DXMT 版本或调整配置。5.3 我踩过的五个坑与对应解法第一个坑是 RootFS 版本不匹配。我用了一个 Ubuntu 20.04 的 rootfs结果宿主是 22.04glibc 版本对不上程序启动就报符号找不到。换成 22.04 的 rootfs 后解决。教训是 rootfs 的 glibc 版本要小于等于宿主的版本。第二个坑是 thunk 配置过度。我把所有库都设成 thunk结果某些库的 ABI 不兼容程序跑着跑着就崩。后来只把图形和音频库设 thunk其他走 x86 侧稳定性大幅提升。第三个坑是 DXMT 的 DLL 放错位置。我一开始放到 syswow64 目录结果 64 位程序加载不到。后来放到 system32 目录才生效。注意64 位 prefix 的 DLL 要放 system3232 位 prefix 才放 syswow64。第四个坑是 locale 设置遗漏。我只在 shell 里设了LANGzh_CN.UTF-8但 Wine 启动时没继承这个变量导致程序里中文乱码。后来在 winecfg 的 Environment 里显式设了LANG和LC_ALL才解决。第五个坑是 FEX 的缓存目录权限问题。FEX 默认把翻译缓存写到~/.fex-emu/Cache/如果这个目录权限不对FEX 会静默失败程序启动极慢。检查权限并确保可写后启动速度恢复正常。6. 这套方案还能怎么扩展Madeira 这套组合的扩展性其实比想象中好。如果你跑的是开发工具而不是游戏可以把 DXMT 换成软件渲染省掉图形翻译的开销CPU 翻译的效率反而更高。如果你跑的是老版本 Windows 程序可以在 Wine prefix 里装对应的 .NET Framework 或 VB6 运行库兼容性会更好。如果你手头有多个 ARM 设备可以把 RootFS 和 prefix 放在共享存储上多设备共用一套环境省去重复配置的麻烦。我最近在尝试的一个方向是把 FEX 的 RootFS 做成容器镜像用 Docker 或 Podman 跑这样环境隔离更彻底迁移也方便。初步测试下来容器里的性能损耗比裸机高 5% 左右但换来的是可复现性和可移植性对于需要频繁切换环境的场景很值得。另一个方向是给 DXMT 加一层帧生成在低帧率场景下插帧提升流畅度不过这个还在实验阶段稳定性有待验证。如果你也在折腾类似的东西我的建议是先把 FEX Wine 跑通确认基础链路没问题再上 DXMT。不要一上来就三层全开出了问题很难定位。另外多看看 FEX 和 Wine 的日志WINEDEBUG和FEX_LOG_LEVEL这两个环境变量能帮你省很多排查时间。