
1. 项目缘起为什么我要折腾 Madeira 这套跨架构兼容方案第一次看到 Madeira 这个代号是在一个折腾跨平台兼容的讨论帖里。当时有人提到它跟 FEX-Emu、Wine、DXMT 这几个关键词绑在一起我脑子里第一反应是这又是一套把 x86-64 应用搬到别的架构上跑的组合拳。后来自己动手搭了一遍才真正理解这套东西的价值——它解决的是我手头只有一台 ARM 设备但有一堆只认 x86-64 的 Windows 程序想跑这个非常具体的痛点。Madeira 本质上不是一个单独的软件而是一套分层兼容栈的工程化组合。底层用 FEX-Emu 做 x86-64 到 ARM64 的指令翻译中间层用 Wine 提供 Windows API 的实现图形层再用 DXMT 把 Direct3D 调用翻译成 Metal。三层各管一段叠起来就能让原本为 Windows x86-64 编译的程序在一台 ARM 架构的设备上跑起来而且图形性能比纯软件渲染强得多。这套方案适合谁我总结下来是三类人一是手里有 ARM 开发板或者 ARM 笔记本想跑一些 Windows 独占工具的技术爱好者二是做兼容层测试、需要频繁验证 x86 程序在异构环境表现的开发者三是单纯对指令翻译和 API 转译感兴趣想拆开看看每一层到底怎么工作的人。如果你只是想安安静静用个软件那这套东西的折腾成本可能不太划算但如果你想搞清楚跨架构兼容这件事的底层逻辑Madeira 是个非常好的解剖样本。我前后搭了三套环境踩了不少坑也总结出一些文档里不会写的细节。下面按我实际的搭建顺序把整套东西拆开讲。2. 整体架构拆解三层兼容栈各自负责什么2.1 FEX-Emu把 x86-64 指令翻译成 ARM64FEX-Emu 是整个栈的地基。它的工作是把 x86-64 的机器指令动态翻译成 ARM64 指令让 ARM 处理器能听懂原本为 Intel/AMD 写的程序。这里的关键词是动态翻译——不是提前把整个程序编译一遍而是在程序运行时遇到一段 x86 指令就翻译一段翻译结果缓存起来下次遇到同样的代码块直接复用。为什么选动态翻译而不是静态重编译因为静态重编译需要拿到完整的程序二进制遇到自修改代码、加壳程序就歇菜了。动态翻译虽然有一点点运行时开销但兼容性好得多能处理绝大多数真实世界的程序。FEX-Emu 的翻译缓存机制做得很细热代码翻译一次之后基本就是原生速度冷代码才会反复翻译。实际配置里FEX-Emu 有几个参数值得关注。FEX_TSOEN控制内存序模拟x86 是强内存模型ARM 是弱内存模型开启 TSO 模拟能保证多线程程序的正确性但会损失一点性能。FEX_MULTIBLOCK开启多块编译能提升翻译吞吐。我实测下来对于单线程为主的工具类程序关掉 TSO 能快 10% 到 15%但多线程程序千万别关否则会出现各种诡异的竞态问题。2.2 Wine提供 Windows API 的兼容实现Wine 这一层大家相对熟悉它的作用是把 Windows 的系统调用翻译成宿主系统的调用。在 Madeira 这套组合里Wine 跑在 FEX-Emu 之上也就是说 Wine 本身也是被翻译执行的 x86-64 程序。这里有个容易混淆的点Wine 有两种用法一种是作为 x86-64 程序被 FEX 翻译另一种是直接用 ARM64 原生编译的 Wine。Madeira 走的是前者因为 x86-64 版的 Wine 对 Windows 程序的兼容性更成熟。Wine 的配置核心是prefix前缀每个 prefix 就是一个独立的 Windows 环境有自己的注册表、C 盘目录、DLL 集合。我建议给每个要跑的程序单独建 prefix别全塞一个里面否则 DLL 冲突能让你排查到怀疑人生。创建 prefix 的时候用WINEARCHwin64指定 64 位环境因为现在大部分 x86-64 程序都需要 64 位 prefix。Wine 的 DLL 覆盖设置是另一个重点。有些程序自带的 DLL 比 Wine 内置的实现更靠谱这时候就要在winecfg的 Libraries 标签页里把对应 DLL 设为 native。反过来如果程序自带的 DLL 有问题就设成 builtin 用 Wine 自己的。这个没有通用答案得针对具体程序试。2.3 DXMT把 Direct3D 翻译成 MetalDXMT 是这套栈里最年轻的一层它的任务是把 Windows 的 Direct3D 调用翻译成 Apple 的 Metal API。为什么需要它因为 Wine 自带的图形翻译层比如 wined3d 走 OpenGL在 Apple 平台上性能一般而 Metal 是 Apple 平台的原生图形 API直接翻译到 Metal 能拿到更好的性能和兼容性。DXMT 主要覆盖 Direct3D 11 和部分 Direct3D 12 的接口。它的工作方式和 DXVK把 D3D 翻译成 Vulkan类似但目标 API 换成了 Metal。实际使用中DXMT 对 D3D11 游戏的兼容性已经相当不错D3D12 还在完善中。配置上主要是把程序目录下的d3d11.dll、dxgi.dll替换成 DXMT 提供的版本然后在环境变量里指定一些调试选项。三层叠起来数据流向是这样的Windows 程序发出 x86-64 指令和 D3D 调用FEX-Emu 把指令翻译成 ARM64Wine 把 Windows API 调用转成宿主调用DXMT 把 D3D 调用转成 Metal。每一层都有开销但叠起来的总开销在可接受范围内尤其是图形密集型的场景DXMT 带来的性能提升远大于它引入的开销。3. 环境搭建实操从零到跑通第一个程序3.1 基础依赖安装与版本选择搭建之前先把基础依赖理清楚。FEX-Emu 需要从源码编译或者用预编译包我建议用预编译包省时间。Wine 建议用较新的稳定版太老的版本对 DXMT 支持不好。DXMT 直接从它的发布页拿对应版本的 DLL 包。版本搭配有个坑FEX-Emu 和 Wine 的版本要匹配。我试过用很新的 FEX 配很老的 Wine结果 Wine 启动就崩。后来固定用 FEX 的最新稳定版配 Wine 的最近两个稳定版之一就稳了。DXMT 的版本要和 Wine 的版本对应DXMT 发布说明里会写它适配哪个 Wine 版本区间照着选就行。安装顺序上先装 FEX-Emu确认它能正常翻译一个简单的 x86-64 程序比如uname -m这种再装 Wine最后配 DXMT。每装一层都验证一下别三层全装完再一起调出了问题根本不知道是哪层的锅。3.2 FEX-Emu 的配置与验证FEX-Emu 装好之后核心是配置它的 rootfs。FEX 需要一个 x86-64 的根文件系统来提供基础的库和工具这个 rootfs 可以用 debootstrap 生成也可以直接用现成的。我用的现成 rootfs省事。配置环境变量是关键一步。FEX_ROOTFS指向 rootfs 路径FEX_TSOEN按前面说的按需设置FEX_MULTIBLOCK建议开启。还有一个FEX_CACHE指向翻译缓存目录给足空间缓存越大热代码命中率越高。验证 FEX 是否工作最简单的办法是跑一个 x86-64 的静态编译程序看输出对不对。我一般用一个自己编译的 hello world静态链接不依赖任何动态库这样能排除 rootfs 的问题。如果 hello world 能跑说明 FEX 的指令翻译没问题再往上叠 Wine。3.3 Wine prefix 创建与 DLL 覆盖策略创建 prefix 的命令是WINEPREFIX/path/to/prefix WINEARCHwin64 wineboot。第一次运行会初始化 prefix生成 C 盘目录结构和注册表。这个过程在 FEX 翻译下会慢一些耐心等。prefix 建好之后先别急着装程序先跑winecfg确认基本环境正常。winecfg能打开说明 Wine 的图形层至少能工作。然后在winecfg里配置 DLL 覆盖把d3d11、dxgi、d3d10core这几个设成 native为后面上 DXMT 做准备。装程序的时候用wine setup.exe或者wine msiexec /i package.msi。安装过程中如果遇到乱码那是字体问题把 Windows 的字体文件比如 simsun.ttc拷到 prefix 的drive_c/windows/Fonts目录下再在注册表里配一下字体替换就能解决。这个乱码问题在热词里出现频率很高后面单独讲。3.4 DXMT 部署与图形层调优DXMT 的部署就是把它的 DLL 文件放到程序目录或者 prefix 的system32目录下。我建议放程序目录这样只影响这一个程序不会污染整个 prefix。需要替换的通常是d3d11.dll和dxgi.dll有些版本还需要d3d10core.dll。放好之后设置环境变量DXMT_LOG_LEVEL可以控制日志详细程度调试的时候设成info或debug正常用设成none减少开销。还有一个DXMT_FRAME_RATE_LIMIT可以限制帧率省电用。验证 DXMT 是否生效跑一个 D3D11 的程序看日志里有没有 DXMT 的初始化信息。如果程序能正常渲染说明 DXMT 工作正常。如果黑屏或者崩溃先看日志大概率是 DLL 版本不匹配或者某个 D3D 特性没实现。4. 常见问题排查那些让我熬夜的坑4.1 Wine 乱码问题的根因与修复Wine 乱码是出现频率最高的问题热词里wine 乱码、wine 栏是乱码都指向这个。根因是 Wine 默认用的字体不包含中文字形或者字体替换规则没配对。修复分两步。第一步把中文字体文件拷进 prefix。从 Windows 系统里拿simsun.ttc、msyh.ttc这些放到drive_c/windows/Fonts。第二步配注册表的字体替换。在winecfg里或者直接改注册表把System、MS Shell Dlg这些字体名映射到刚拷进去的中文字体。如果菜单栏还是乱码那可能是程序用了自绘的字体渲染这种情况得看程序具体用了什么字体然后在注册表里针对性替换。我遇到过一个程序死活乱码最后发现它硬编码了一个字体名在注册表里加了一条替换规则才解决。4.2 FEX 翻译下的性能异常排查FEX 翻译下的性能问题通常表现为程序启动慢、运行卡顿、CPU 占用高。排查思路是先确认是翻译开销还是程序本身的问题。用perf或者 FEX 自带的统计功能看翻译缓存命中率。命中率低说明热代码没被有效缓存可能是缓存目录空间不够或者程序本身代码局部性差。命中率高但性能还是差那可能是 TSO 模拟的开销试试关掉 TSO 看有没有改善前提是程序不是多线程密集型的。还有一个隐蔽的坑FEX 的翻译缓存如果放在慢速存储上每次读缓存都很慢。把缓存目录放到内存盘或者高速 SSD 上启动速度能快不少。4.3 DXMT 图形兼容性速查DXMT 的兼容性问题主要集中在特定 D3D 特性没实现、着色器编译失败、纹理格式不支持这几类。排查的时候先看 DXMT 日志日志里会写清楚哪个调用失败了。现象可能原因排查方向黑屏无渲染D3D 设备创建失败检查 DXMT DLL 版本与 Wine 是否匹配画面花屏纹理格式不支持看日志里的纹理格式确认 DXMT 是否支持着色器编译报错用了未实现的 Shader Model确认程序需要的 SM 版本DXMT 对 SM 5.0 支持较好帧率极低走了软件渲染回退确认 DXMT 是否真正生效看日志初始化信息程序崩溃D3D12 特性未实现尝试强制程序走 D3D11 模式这张表是我踩坑之后整理的基本覆盖了八成以上的图形问题。遇到表里没有的先看日志日志里一般都有线索。4.4 跨层调试的思路与工具跨层调试最麻烦的是不知道问题出在哪一层。我的方法是逐层隔离先用一个纯 x86-64 的程序验证 FEX再用一个纯 Windows 控制台程序验证 Wine最后用图形程序验证 DXMT。每层都过了再跑目标程序。工具方面FEX 有自己的日志和统计Wine 有WINEDEBUG环境变量控制日志级别DXMT 有DXMT_LOG_LEVEL。把这三层的日志都开起来对照时间戳看能定位到问题发生在哪一层。WINEDEBUGall会输出海量日志建议先设成err,warn缩小范围再细化。5. 实操心得与进阶技巧5.1 性能调优的几个关键参数性能调优的核心是减少翻译开销和图形开销。翻译侧FEX_MULTIBLOCK必开FEX_TSOEN按程序特性决定翻译缓存放高速存储。图形侧DXMT 的帧率限制按需设日志级别正常用设成none。还有一个容易被忽略的点Wine 的WINEDLLOVERRIDES环境变量可以批量设置 DLL 覆盖比在winecfg里一个个点快得多。格式是d3d11n;dxginn表示 nativeb表示 builtin。5.2 多程序共存的 prefix 管理多个程序共用一个 prefix 容易出 DLL 冲突我的做法是每个程序一个 prefix用脚本管理。脚本里设好WINEPREFIX、FEX_ROOTFS、DXMT相关环境变量然后启动程序。这样每个程序的环境互相隔离出问题也好排查。prefix 占空间不大一个干净的 win64 prefix 也就几百 MB多建几个无所谓。比起 DLL 冲突排查的时间成本这点空间完全值得。5.3 版本升级的注意事项FEX、Wine、DXMT 三层任意一层升级都可能影响其他层。升级前先备份 prefix 和翻译缓存升级后逐层验证。我一般先升 FEX验证通过再升 Wine最后升 DXMT。如果升级后出问题回滚对应层就行不用全部重来。DXMT 的升级要特别注意 DLL 版本和 Wine 版本的匹配DXMT 发布说明里会写适配的 Wine 版本区间跨区间升级大概率出问题。6. 这套方案还能怎么扩展Madeira 这套组合跑通之后我试过几个扩展方向。一是把 FEX 的翻译缓存做成共享的多个 prefix 共用一份缓存热代码命中率更高。二是给 DXMT 加自定义的着色器缓存减少重复编译。三是把整套环境打包成容器镜像换设备的时候直接拉镜像省去重新搭建的时间。还有一个方向是研究 FEX 的 JIT 优化看能不能针对特定程序做翻译策略的定制。FEX 的翻译器是可配置的理论上可以针对程序的指令分布调优但这需要比较深的底层知识我还在摸索阶段。这套东西的折腾成本不低但每解决一个问题对跨架构兼容的理解就深一层。如果你也在折腾类似的东西希望这些踩坑记录能帮你少走点弯路。