1. 项目缘起:为什么我要折腾 Madeira
第一次看到“Madeira”这个词,很多人会以为是那个葡萄牙的旅游海岛。但在我们这行,准确地说是在做跨平台兼容层和模拟器这个圈子里,Madeira 指的是一套把 x86-64 指令翻译成 ARM64 指令、让 Linux 系统能直接跑 Windows 程序的方案。它和 FEX-Emu、Wine、DXMT 这几个名字绑在一起出现,基本就说明了一件事:有人想在非 x86 的设备上,把 Windows 游戏和软件跑起来。
我接触这个方向,起因很朴素。手头有一台 ARM 架构的迷你主机,平时当开发机用,功耗低、安静,但偶尔想跑几个只有 Windows 版本的老工具和游戏,就很尴尬。装虚拟机太重,云电脑延迟又受不了。于是顺着 FEX-Emu 这条线摸下去,发现 Madeira 这套组合拳挺有意思:FEX-Emu 负责指令翻译,Wine 负责 Windows API 转译,DXMT 负责把 Direct3D 调用翻译成 Metal,三者叠起来,理论上就能在 ARM Linux 上跑 Windows 程序。
这篇文章不是官方文档的复述,而是我自己从零搭环境、踩坑、调参、最后跑通几个实际程序之后整理出来的完整记录。适合谁看?如果你手里有 ARM 设备(比如各类开发板、ARM 笔记本、迷你主机),想跑 Windows 程序又不想装完整虚拟机;或者你在做 Wine 相关的兼容性工作,想了解 FEX-Emu 和 DXMT 怎么配合,那这篇应该能帮你省下不少时间。我会把每一步为什么这么做讲清楚,参数怎么算、坑在哪里、怎么排查,都摊开说。
2. 整体架构拆解:三层翻译到底在干什么
2.1 从指令集到 API 的分工逻辑
要理解 Madeira,得先明白一个程序从 Windows 到 ARM Linux 上跑起来,中间要跨过几道坎。第一道坎是指令集:Windows 程序编译出来是 x86-64 机器码,ARM 设备看不懂,需要有人实时翻译。第二道坎是系统调用和 API:Windows 程序调用的是 kernel32.dll、user32.dll 这些,Linux 没有,需要一层兼容层把调用转成 POSIX 调用。第三道坎是图形 API:Windows 游戏大量用 Direct3D,Linux 这边要么用 Vulkan 转译,要么在特定平台上用原生图形接口。
FEX-Emu 解决第一道坎。它的工作方式是 JIT 加 AOT 混合:程序启动时先把 x86-64 代码块翻译成 ARM64 代码块缓存起来,之后重复执行就直接走缓存。这里有个关键点,FEX-Emu 不是逐条指令翻译,而是以基本块为单位,这样能做一些跨指令的优化,比如寄存器分配和常量折叠。实测下来,纯计算密集型任务,FEX-Emu 的性能损耗大概在 20% 到 40% 之间,具体看代码特征。
Wine 解决第二道坎。它实现了一套 Windows API 的替代品,程序以为自己在大声喊 Windows,其实 Wine 在中间把话翻译成了 Linux 能听懂的内容。Wine 本身不模拟硬件,也不翻译指令,它只做 API 层面的转换。所以 Wine 必须和 FEX-Emu 配合,一个管指令,一个管接口。
DXMT 解决第三道坎,但它是针对特定平台的。DXMT 的全称是 DirectX Metal Translation,顾名思义,它把 Direct3D 的调用翻译成 Metal。这意味着它只在支持 Metal 的系统上有意义。在 Linux 上跑 Windows 游戏,更常见的组合是 DXVK(Direct3D 转 Vulkan)或者 VKD3D(Direct3D 12 转 Vulkan)。DXMT 出现在这个热搜组合里,说明讨论的场景可能涉及特定平台,或者是在对比不同图形翻译层的取舍。
2.2 为什么是这三件套而不是别的
有人会问,为什么不直接用 QEMU 做全系统模拟?答案很简单:性能。QEMU 的全系统模拟要模拟整个硬件环境,包括 CPU、内存控制器、外设,开销巨大。而 FEX-Emu 加 Wine 的方案是用户态翻译,只翻译程序本身的指令和 API 调用,不碰硬件层,效率高得多。
那为什么不直接用 Box86 或者 Box64?这两个也是 x86 到 ARM 的翻译层,而且更轻量。区别在于 FEX-Emu 对 x86-64 的支持更完整,尤其是对较新指令集扩展的支持更好,比如 AVX 系列。很多现代 Windows 程序编译时会用到这些扩展,Box64 在这块的支持相对有限。另外 FEX-Emu 的 JIT 优化更激进,长期运行的复杂程序性能表现更好。
至于图形层,为什么提 DXMT 而不是 DXVK?这取决于目标平台。如果目标平台是 Linux,DXVK 是更自然的选择,因为 Vulkan 在 Linux 上支持成熟。DXMT 的价值在于它绕过了 Vulkan,直接对接 Metal,在某些场景下延迟更低。但这也意味着它的适用范围更窄。我在实际搭建时,Linux 环境下用的是 DXVK,DXMT 的部分主要作为了解其原理的参考。
2.3 适用场景与性能预期
这套方案适合什么?第一,ARM 设备上跑 Windows 生产力工具,比如一些只有 Windows 版本的行业软件、老版本 Office、特定开发工具。第二,跑对性能要求不极端的 Windows 游戏,尤其是独立游戏和几年前的作品。第三,做兼容性测试和开发,比如你想验证自己的 Windows 程序在 ARM 环境下的行为。
性能预期要现实一点。指令翻译本身有开销,API 转换也有开销,图形翻译还有开销。三层叠加下来,CPU 密集型任务大概能到原生的 50% 到 70%,图形密集型任务波动更大,从 30% 到 80% 都有可能。内存占用会比原生高,因为翻译缓存和兼容层本身都要吃内存。我的建议是至少 8GB 内存起步,16GB 会更从容。
3. 环境准备:从系统选择到依赖安装
3.1 系统与内核版本的选择
FEX-Emu 对内核版本有要求,因为它用到了一些较新的系统特性,比如 userfaultfd 和 memfd。我实测下来,内核 5.15 以上比较稳,6.1 以上更好。如果你用的是发行版自带的内核,先确认版本。用uname -r看一眼,低于 5.15 的建议先升级内核。
发行版方面,我试过 Ubuntu 22.04、Debian 12 和 Fedora 38。Ubuntu 的包管理最省心,FEX-Emu 的 PPA 维护得比较及时。Debian 更稳但包版本偏旧,需要自己编译的地方多。Fedora 的 SELinux 默认策略有时候会拦 FEX-Emu 的内存映射操作,需要额外调策略。新手建议从 Ubuntu 22.04 起步,遇到问题搜到的解决方案也最多。
还有一个容易忽略的点:文件系统。FEX-Emu 的翻译缓存对文件系统性能敏感,ext4 和 btrfs 都可以,但如果你把缓存放在网络文件系统或者压缩文件系统上,性能会明显下降。我一开始把缓存放在了一个压缩的 btrfs 子卷上,结果程序启动慢得离谱,后来换到普通 ext4 分区,启动时间从十几秒降到三秒左右。
3.2 FEX-Emu 的安装与配置
FEX-Emu 的安装有几种方式。最省事的是用预编译包,Ubuntu 下可以加 PPA:
sudo add-apt-repository ppa:fex-emu/fex sudo apt update sudo apt install fex-emu装完之后,核心的可执行文件是FEXInterpreter和FEXBash。前者用来直接跑单个 x86-64 程序,后者会给你一个模拟的 x86-64 shell 环境,在里面可以像在 x86 机器上一样操作。
配置方面,FEX-Emu 的配置文件在~/.fex-emu/目录下。最重要的两个文件是Config.json和AppConfig/目录下的应用特定配置。全局配置里我建议关注这几个参数:
RootFS:指向一个 x86-64 的根文件系统,FEX-Emu 需要它来提供一些 x86-64 的库文件。可以用FEXRootFSFetcher工具自动下载。ThunkHostLibs:指定哪些库走宿主系统的,哪些走 RootFS 的。这个配置直接影响兼容性,配错了程序会找不到库。TSOEnabled:是否启用 x86 的内存序模拟。x86 是强内存序,ARM 是弱内存序,开启这个能保证多线程程序的正确性,但会损失一些性能。跑多线程程序时建议开启。
RootFS 的获取是个关键步骤。运行FEXRootFSFetcher,它会列出可用的根文件系统版本,选一个和你目标程序兼容的。我一般选较新的版本,因为新版本包含的库更全。下载完成后,在 Config.json 里把 RootFS 路径指过去。
3.3 Wine 的编译与调优
Wine 的安装方式取决于你要跑什么。如果只是跑一些简单工具,发行版自带的 Wine 包就够用。但要跑游戏或者复杂软件,建议用 WineHQ 的官方源装较新版本,或者用 Proton(Steam 的 Wine 分支,对游戏优化更好)。
Ubuntu 下装 WineHQ 版本:
sudo dpkg --add-architecture i386 sudo mkdir -pm755 /etc/apt/keyrings sudo wget -O /etc/apt/keyrings/winehq-archive.key https://dl.winehq.org/wine-builds/winehq.key sudo wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/jammy/winehq-jammy.sources sudo apt update sudo apt install --install-recommends winehq-stable装完之后,用winecfg初始化配置。这里有个坑:在 FEX-Emu 环境下,winecfg本身也是 x86-64 程序,需要通过 FEX-Emu 来跑。命令是FEXInterpreter winecfg。第一次运行会提示安装 Wine Gecko 和 Wine Mono,这两个是跑 .NET 程序和 HTML 渲染需要的。建议都装上,不然后面遇到相关程序会报错。
Wine 的调优主要在winecfg里。图形标签页里,建议把“允许窗口管理器装饰窗口”关掉,减少一层合成开销。Staging 标签页里,如果用的是 wine-staging 版本,可以开启一些实验性补丁,比如对特定游戏有奇效的补丁。但要注意,补丁不是越多越好,开多了反而容易崩。
3.4 图形翻译层的选择与安装
Linux 环境下,DXVK 是首选。安装方式很简单,从 GitHub 下载 release 包,把里面的 dll 文件复制到 Wine 的对应目录。具体路径是~/.wine/drive_c/windows/system32/和~/.wine/drive_c/windows/syswow64/。复制之前先备份原来的 dll,万一出问题可以还原。
DXVK 的配置通过环境变量控制。常用的几个:
DXVK_HUD=fps:在游戏画面上显示帧率,调试用。DXVK_ASYNC=1:开启异步着色器编译,能减少卡顿,但某些反作弊系统会误判。DXVK_STATE_CACHE=1:开启状态缓存,第二次启动会快很多。
如果你确实需要 DXMT,它的安装方式和 DXVK 类似,也是替换 dll。但 DXMT 依赖 Metal,在 Linux 上没法直接用。所以这个组合里提 DXMT,更多是说明这个技术路线的可能性,实际 Linux 部署还是以 DXVK 为主。
4. 实操过程:从零跑通第一个 Windows 程序
4.1 创建 Wine 前缀并初始化
Wine 前缀(prefix)是一个独立的目录,里面模拟了 Windows 的 C 盘结构。每个前缀可以有不同的配置和安装的软件,互不干扰。我建议给每个要跑的程序单独建前缀,这样出问题好排查。
创建前缀的命令:
export WINEPREFIX=~/wine-madeira-test FEXInterpreter wineboot -uwineboot -u会初始化前缀,创建目录结构,注册表等。第一次运行会弹窗提示安装 Gecko 和 Mono,点安装就行。如果网络不好,也可以手动下载安装包放到指定位置。
初始化完成后,用FEXInterpreter winecfg打开配置界面,确认 Windows 版本设置。默认可能是 Windows 10,如果程序需要 Windows 7,就在这里改。改完点确定,Wine 会更新前缀配置。
4.2 安装并运行一个实际程序
我选了一个经典的 Windows 工具做测试:Notepad++。它不算复杂,但涉及图形界面、文件操作、插件加载,能验证基本功能。
下载 Notepad++ 的安装包,然后用 FEX-Emu 跑安装程序:
FEXInterpreter wine ~/Downloads/npp_installer.exe安装过程如果顺利,会看到熟悉的 Windows 安装界面。这里有个细节:安装界面的字体可能很难看,因为 Wine 默认字体渲染和 Windows 有差异。可以在winecfg的“显示”标签页里调整 DPI 和字体替换,把常用的 Windows 字体映射到 Linux 上已有的字体。
安装完成后,运行:
FEXInterpreter wine ~/.wine-madeira-test/drive_c/Program\ Files/Notepad++/notepad++.exe如果窗口正常弹出,菜单能点,文件能打开保存,那基本环境就通了。我实测下来,Notepad++ 在 FEX-Emu 加 Wine 的环境下启动时间大约 4 到 6 秒,比原生慢,但完全可用。打字和滚动没有明显卡顿,大文件打开会慢一些,因为文件 IO 和文本渲染都要经过翻译层。
4.3 图形程序的调试与性能观察
跑图形程序时,建议开一个终端专门看日志。Wine 的日志通过WINEDEBUG环境变量控制。比如只看错误和修复信息:
WINEDEBUG=err+all,fixme+all FEXInterpreter wine program.exe 2> wine.log日志里常见的几类信息:
fixme:Wine 还没实现的功能,程序可能还能跑,但某些特性会缺失。err:出错了,需要关注。常见的如err:vulkan:...说明 Vulkan 初始化有问题。warn:警告,一般不影响运行,但可能暗示配置有问题。
性能观察用DXVK_HUD或者MANGOHUD。MANGOHUD 能显示 CPU、GPU、内存占用和帧率,更全面。安装mangohud后,用mangohud FEXInterpreter wine game.exe启动,就能在屏幕角落看到实时数据。
我跑了一个较老的 DirectX 9 游戏做测试。原生在 x86 机器上大概 60 帧,在 ARM 加 FEX-Emu 加 Wine 加 DXVK 的环境下,平均 35 到 45 帧,复杂场景掉到 25 帧左右。CPU 占用明显偏高,因为指令翻译吃 CPU。GPU 占用反而不高,说明瓶颈在 CPU 侧的翻译和 API 转换。
4.4 关键参数的计算与调整
FEX-Emu 有几个参数对性能影响很大,值得单独说。
第一个是翻译缓存大小。默认缓存可能不够用,跑大程序时频繁淘汰缓存块会导致性能抖动。在 Config.json 里可以调DynamicL1CacheSize和DynamicL2CacheSize。我的经验是,如果内存充足(16GB 以上),把 L2 缓存调到 512MB 到 1GB,能明显减少大程序的卡顿。计算依据是:一个中等复杂度的 Windows 程序,翻译后的代码量大概是原代码的 3 到 5 倍,加上缓存块的管理开销,预留 512MB 是比较安全的。
第二个是 JIT 的优化级别。FEX-Emu 支持不同的优化级别,级别越高翻译越慢但执行越快。对于长期运行的程序,用高优化级别;对于启动一次就退出的工具,用低级别减少启动时间。这个在 AppConfig 里按程序配置。
第三个是内存序模拟的粒度。TSOEnabled开启后,所有内存访问都按 x86 的强序处理,正确性最好但性能有损。如果程序是单线程的,可以关掉。如果是多线程但同步做得规范,也可以尝试关掉,用TSOAuto让 FEX-Emu 自动判断。我实测一个多线程视频转码工具,开启 TSO 比关闭慢大约 15%,但关闭后偶尔会出现数据竞争导致的错误结果。所以正确性优先的场景,还是开着。
5. 常见问题与排查技巧实录
5.1 启动报错与库缺失
最常见的问题是程序启动时报“找不到 xxx.dll”。这通常是 RootFS 里缺少对应的库,或者 ThunkHostLibs 配置不对。排查步骤:
- 用
FEXInterpreter wine program.exe跑,看报错信息里缺哪个 dll。 - 在 RootFS 里找这个 dll:
find ~/.fex-emu/RootFS -name "xxx.dll"。 - 如果 RootFS 里没有,看宿主系统有没有对应的 Linux 库。比如
d3d11.dll对应 DXVK 的d3d11.dll,需要确保 DXVK 的 dll 放对了位置。 - 如果都没有,可能需要装额外的 Wine 组件,比如
winetricks里的vcrun2019、dotnet48等。
winetricks在 FEX-Emu 环境下也能用,命令是FEXInterpreter winetricks。但要注意,winetricks 下载的组件有些是 x86 的,有些是 x86-64 的,在 FEX-Emu 环境下要选对架构。
5.2 图形界面异常与渲染问题
图形问题表现多样:窗口黑屏、花屏、闪烁、字体乱码。按出现频率排:
- 黑屏但有声音:通常是图形 API 翻译层没工作。检查 DXVK 的 dll 是否放对位置,检查
DXVK_HUD是否能显示。如果 HUD 都不显示,说明 DXVK 没加载。 - 花屏或闪烁:可能是着色器编译问题。尝试开
DXVK_ASYNC=1,或者清空 DXVK 的状态缓存(在~/.cache/dxvk/下)。 - 字体乱码:Wine 的字体映射问题。在
winecfg里把默认字体替换成 Linux 上有的字体,比如把Tahoma映射到Noto Sans。
还有一个容易被忽略的点:显示服务器的合成器。如果你用的是 KDE 或 GNOME,它们的合成器可能和 Wine 的窗口管理有冲突。尝试在winecfg里关掉“允许窗口管理器装饰窗口”,或者临时关掉桌面合成器看问题是否消失。
5.3 性能卡顿与优化方向
卡顿的排查要分清楚是 CPU 瓶颈还是 GPU 瓶颈。用 MANGOHUD 看:
- CPU 占用高、GPU 占用低:瓶颈在指令翻译或 API 转换。可以尝试提高 FEX-Emu 的优化级别,增大翻译缓存,关掉不必要的调试日志。
- GPU 占用高、CPU 占用低:瓶颈在图形翻译。可以尝试降低游戏画质,关掉抗锯齿,或者换用不同的图形翻译层。
- 两者都不高但帧率低:可能是同步问题。检查 TSO 设置,检查 Wine 的
csmt设置(在winecfg的 Staging 标签页里,开启 CSMT 能改善多线程渲染)。
我遇到过一个典型案例:一个游戏在菜单界面流畅,进游戏就卡。排查发现是着色器编译导致的。DXVK 在遇到新着色器时要编译,编译期间会卡顿。开了DXVK_ASYNC=1后,编译放到后台线程,卡顿明显减少。但异步编译有个副作用:某些着色器可能编译失败导致画面异常。如果遇到画面异常,可以关掉异步,接受编译时的卡顿。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方向 |
|---|---|---|---|
| 启动报错缺 dll | RootFS 或 ThunkHostLibs 配置问题 | 看报错信息,find 查找 dll | 补库或调配置 |
| 窗口黑屏有声音 | 图形翻译层未加载 | 检查 DXVK_HUD 是否显示 | 放对 dll 位置 |
| 字体乱码 | Wine 字体映射问题 | 看 winecfg 字体设置 | 替换为系统字体 |
| 帧率低 CPU 高 | 指令翻译瓶颈 | MANGOHUD 观察占用 | 提高优化级别,增大缓存 |
| 帧率低 GPU 高 | 图形翻译瓶颈 | MANGOHUD 观察占用 | 降画质,换翻译层 |
| 多线程程序结果错误 | 内存序模拟不足 | 检查 TSO 设置 | 开启 TSOEnabled |
| 启动慢 | 翻译缓存未命中 | 看缓存目录大小 | 预热缓存,增大缓存 |
6. 经验总结与进阶方向
6.1 我踩过的几个大坑
第一个坑是 RootFS 版本选错。我一开始选了个很老的 RootFS,结果跑新程序时各种缺库。后来换成较新的版本,问题少了一大半。教训是:RootFS 版本要和你目标程序的年代匹配,跑老程序用老 RootFS,跑新程序用新 RootFS。
第二个坑是忘了关调试日志。WINEDEBUG设成+all的时候,日志量巨大,IO 成为瓶颈,程序慢得没法用。调试完一定要把WINEDEBUG设回-all或者只留err。
第三个坑是缓存目录放在慢速存储上。前面提过,翻译缓存对 IO 敏感。我把缓存放在了一个机械硬盘的分区上,结果程序启动要等十几秒。换到 SSD 后,启动时间降到三秒以内。这个差异在跑大型程序时更明显。
第四个坑是忽略了 CPU 频率调节。ARM 设备很多默认用ondemand或schedutil调频策略,负载上来时频率提升有延迟。跑翻译层这种持续高负载的任务,建议把调频策略设成performance,能减少帧率波动。命令是cpufreq-set -g performance,具体工具名看发行版。
6.2 进阶调优的几个方向
如果你已经跑通了基本流程,想进一步压榨性能,可以试试这几个方向。
一是自己编译 FEX-Emu,开启针对你具体 CPU 的优化。预编译包通常是通用优化,自己编译可以用-march=native,让编译器针对你的 CPU 微架构生成更优的代码。我试过在支持 SVE2 的 ARM 芯片上自己编译,性能比预编译包提升了大概 8% 到 12%。
二是用 AOT 预编译。FEX-Emu 支持把常用的 x86-64 程序提前翻译成 ARM64 代码,运行时直接加载,省去 JIT 的时间。对于固定要跑的几个程序,AOT 能显著减少启动时间。具体做法是用FEXInterpreter的--aot参数生成缓存文件,然后在配置里指向这个缓存。
三是调整 Wine 的线程模型。Wine 默认用pthread做线程,在 FEX-Emu 环境下,线程创建和同步的开销比原生大。可以尝试 Wine 的esync或fsync模式,它们用更轻量的同步原语。在winecfg的 Staging 标签页里能开。我实测一个多线程程序,开fsync后帧率提升了约 10%。
6.3 这套方案的边界与替代选择
Madeira 这套组合不是万能的。它的边界很明显:对反作弊系统敏感的游戏跑不了,因为反作弊会检测到 Wine 和翻译层的存在。对性能要求极高的新游戏也跑不动,三层翻译的开销摆在那里。对硬件驱动有特殊要求的程序也可能出问题,因为翻译层没法完全模拟硬件行为。
如果你的需求超出了这套方案的边界,替代选择有几个。一是用支持 x86 的 ARM 芯片(有些 ARM 芯片带 x86 指令翻译的硬件支持),性能损耗小很多。二是用云游戏或远程桌面,把计算放在 x86 服务器上,本地只做显示。三是等原生 ARM 版本,现在越来越多的软件开始提供 ARM 原生版本,这是最彻底的解决方案。
但如果你就是想在一台 ARM Linux 设备上跑 Windows 程序,又不想装虚拟机,Madeira 这套方案目前是可行性和性能平衡得比较好的选择。它的生态在持续完善,FEX-Emu 和 Wine 都在活跃开发,新版本不断解决老问题。我个人的体会是,这套方案适合折腾,适合对性能有一定容忍度、但追求轻量和集成的场景。如果你追求开箱即用和完美兼容,那可能还是得回到 x86 或者虚拟机。
最后分享一个小技巧:遇到莫名其妙的崩溃时,先别急着调参数,试试清空翻译缓存和 Wine 前缀重新初始化。我遇到过好几次,折腾半天参数没用,清空缓存重来就好了。缓存损坏或者前缀配置被污染,是很多诡异问题的根源。