1. 项目缘起:为什么我要折腾 Madeira 这套跨平台兼容方案
第一次看到 "Madeira" 这个词,很多人会以为是那个葡萄牙的旅游海岛,但在我们这行里,它指的是一套围绕FEX-Emu、Wine、DXMT构建的跨平台应用兼容与运行方案,核心目标只有一个:让原本为 Windows 或 x86 环境编写的程序,在 ARM 架构的设备上尽可能顺畅地跑起来,尤其是 iOS 设备与国产 Linux 发行版这两类场景。我最初接触它,是因为手上有一批老旧的 Windows 工具链和几个自研的小工具,需要在移动端和国产系统上做演示,重写成本太高,于是就走上了这条"兼容层"的路子。
这套方案能解决的问题很具体:你不需要拿到源代码,也不需要重新编译,只要把可执行文件和依赖库准备好,通过兼容层做指令翻译和系统调用映射,就能把程序跑起来。它适合谁?一类是做跨端演示的开发者,一类是在国产化环境里需要迁移历史软件的技术人员,还有一类是喜欢在移动设备上折腾桌面级应用的玩家。但我要先把丑话说在前面:这条路不是"一键安装包"式的体验,坑非常多,尤其是字体乱码、依赖缺失、图形接口不匹配这几类问题,几乎每个人都会遇到。
我写这篇东西,不是要给你一份官方文档的翻译,而是把我自己从零搭建、反复踩坑、最后跑通的全过程拆开讲。里面涉及的FEX-Emu负责 x86 到 ARM 的指令翻译,Wine负责 Windows API 到 POSIX 的映射,DXMT负责把 Direct3D 调用转成 Metal,这三者叠在一起才构成完整的链路。任何一个环节配置错了,表现出来的症状都可能是"程序闪退"或者"界面乱码",排查起来非常折磨人。所以下面我会按"整体设计思路 → 核心细节 → 实操流程 → 问题排查"的顺序,把每个环节讲透。
2. 整体架构设计与方案选型思路
2.1 三层兼容链路到底各自干什么
很多人一上来就把 FEX-Emu、Wine、DXMT 混在一起装,结果出了问题根本不知道是哪一层的事。我建议你先在脑子里把这三层分清楚,它们的分工是完全不同的。
FEX-Emu是底层,它做的是 CPU 指令集的翻译。ARM 设备听不懂 x86 的机器码,FEX-Emu 就把 x86 指令动态翻译成 ARM64 指令。这一层不关心你跑的是 Windows 程序还是 Linux 程序,它只管指令。Wine是中间层,它实现的是 Windows 的系统调用和 API,比如文件操作、注册表、窗口管理这些,让 Windows 程序以为自己在一个 Windows 系统里。DXMT是图形层,专门处理 Direct3D 到 Metal 的转换,因为 iOS 和 macOS 上只有 Metal 没有 DirectX。
这三层的关系可以这样理解:FEX-Emu 是"翻译官",把 x86 的话翻译成 ARM 能听懂的话;Wine 是"场景搭建师",把 Windows 的舞台搭出来;DXMT 是"道具师",把 DirectX 的道具换成 Metal 的道具。缺了翻译官,程序根本跑不起来;缺了场景搭建师,程序找不到自己需要的系统资源;缺了道具师,画面出不来或者直接黑屏。
2.2 为什么选这套组合而不是别的方案
市面上做兼容的方案不止这一套,比如有纯模拟器路线、有容器路线、有远程串流路线。我最终选 FEX-Emu + Wine + DXMT,主要基于三个考量。
第一是性能与兼容性的平衡。纯模拟器(比如 QEMU 全系统模拟)兼容性最好,但性能损耗极大,跑个简单程序都卡。FEX-Emu 做的是用户态的动态二进制翻译,配合缓存机制,实际性能比全系统模拟高一个数量级。第二是图形栈的适配。在 iOS 和 macOS 上,DirectX 是绝对跑不了的,必须转 Metal,DXMT 是目前相对活跃且对 D3D11 支持较完整的方案。第三是生态成熟度。Wine 有几十年的积累,遇到问题基本都能搜到前人的经验,而 FEX-Emu 在 ARM 上的社区也在持续更新。
当然这套组合也有明显短板。它对 D3D12 和 Vulkan 的支持还不完善,对反作弊、内核级驱动的程序基本无解,对 .NET 某些版本的兼容也时好时坏。所以你在动手之前,先确认你要跑的程序属于哪一类,如果是重度依赖显卡新特性的游戏,这套方案大概率会让你失望。
2.3 目标场景的差异决定了配置重点
同样是 Madeira 这套方案,跑在 iOS 上和跑在国产 Linux 上,配置重点完全不同。iOS 端的核心矛盾是沙盒限制和图形接口,你没法随意读写系统目录,所有文件都得放在应用沙盒内,图形必须走 Metal,所以 DXMT 的配置是重中之重。而国产 Linux(比如统信 UOS、麒麟)端的核心矛盾是依赖库和字体,系统里缺一堆 Windows 程序需要的运行库,字体映射也经常出问题,所以 Wine 的依赖补全和字体配置是重点。
我在两个平台上都跑过,iOS 上最头疼的是"程序能启动但画面出不来",Linux 上最头疼的是"程序能启动但全是乱码方块"。这两个症状背后的原因完全不同,后面我会分别展开。你先记住一个原则:先让程序能启动,再解决画面,最后解决字体和细节,不要一上来就追求完美。
3. 核心细节解析与关键配置要点
3.1 FEX-Emu 的根文件系统与指令缓存
FEX-Emu 要跑起来,首先需要一个RootFS(根文件系统),里面包含 x86_64 的基础库和运行时。这个 RootFS 不是随便找个 Linux 发行版的镜像就行,它需要和你的宿主环境做路径映射。我一般用 debootstrap 或者现成的精简 RootFS 来搭,体积控制在几百 MB 到 1GB 之间。
配置里最关键的是RootFS 的挂载路径和FEX 的配置项。FEX 有一个配置文件(通常在~/.fex-emu/Config.json),里面要指定 RootFS 路径、CPU 核心数、是否开启多线程翻译等。我实测下来,Multiblock和TSOEnabled这两个选项对性能影响最大。Multiblock开启后会把多个基本块合并翻译,减少翻译开销;TSOEnabled是内存序模拟,某些程序不开会崩溃,开了会慢一些,需要按程序试。
指令缓存(JIT Cache)也是重点。FEX 会把翻译过的代码缓存到磁盘,下次启动就快很多。缓存目录默认在~/.fex-emu/下面,你要确保这个目录有足够的写入权限和空间。我在 iOS 上就遇到过缓存目录不可写导致每次启动都重新翻译、慢得离谱的情况,后来把缓存路径改到应用沙盒内的可写目录才解决。
提示:RootFS 的架构必须是 x86_64,不要用 i386 的,否则很多现代程序跑不了。另外 RootFS 里的 glibc 版本要和 FEX 的预期匹配,版本差太多会出现符号找不到的错误。
3.2 Wine 的前缀管理与依赖补全
Wine 的核心概念是Prefix(前缀),你可以把它理解成一个"虚拟的 Windows 安装目录"。每个 Prefix 里有一套独立的 C 盘、注册表和配置。我强烈建议一个程序一个 Prefix,不要把所有程序塞进同一个 Prefix,因为不同程序对 Windows 版本、依赖库的要求经常冲突。
创建 Prefix 的命令是WINEPREFIX=/path/to/prefix wineboot,这一步会初始化目录结构。初始化完成后,你要根据程序需求设置 Windows 版本,比如winetricks win7或者winetricks win10。很多老程序在 win10 模式下会出问题,切到 win7 就正常了,这个要试。
依赖补全是 Wine 最烦人的部分。Windows 程序经常依赖 VC++ 运行库、.NET Framework、DirectX 运行时这些东西,Wine 自带的实现不完整,需要用winetricks补。常用的有winetricks vcrun2019、winetricks dotnet48、winetricks corefonts。注意corefonts这个一定要装,它解决的就是最常见的字体乱码问题,装完之后很多方块字就正常了。
注意:winetricks 装 .NET 的时候经常卡住或者失败,这是因为 .NET 安装程序本身对环境要求高。我的经验是先把 Prefix 设成 win7,装完 .NET 再切回 win10,成功率会高很多。
3.3 DXMT 的 Metal 转换与图形参数
DXMT 是把 Direct3D 调用翻译成 Metal 的组件,它的配置直接决定画面能不能出来。核心配置项包括D3D 版本映射、纹理格式支持、同步模式这几类。DXMT 通过环境变量或者配置文件来指定行为,比如DXMT_D3D11开启 D3D11 支持,DXMT_METAL_DEVICE指定用哪个 GPU。
在 iOS 上,Metal 的设备和队列管理比较严格,DXMT 需要正确获取到 Metal 设备句柄。如果程序启动后黑屏但没崩溃,八成是 DXMT 没拿到设备或者纹理格式不支持。我遇到过一个典型情况:程序用的是 D3D11 的某个压缩纹理格式,DXMT 默认不支持,结果画面全黑,后来在配置里显式开启对应格式的支持才解决。
同步模式也很关键。Metal 的呈现和 DirectX 的呈现机制不同,如果同步没做好,会出现画面撕裂或者卡顿。DXMT 提供了几种同步策略,我一般先用默认的,如果画面有问题再逐个试。另外要注意,iOS 上的后台渲染限制很严,程序切到后台再切回来,Metal 上下文可能失效,需要程序自己处理重建,这个兼容层帮不了你。
3.4 字体与编码:乱码问题的根源
"wine 乱码"和"wine 栏是乱码"是搜索里出现频率极高的问题,根源基本就两个:字体缺失和编码不匹配。字体缺失好理解,Windows 程序默认用宋体、微软雅黑这些字体,Linux 和 iOS 上没有,Wine 找不到就画方块。解决办法是装corefonts,或者把 Windows 的字体文件复制到 Prefix 的drive_c/windows/Fonts目录下。
编码不匹配更隐蔽。有些程序内部用 GBK 编码处理中文,而 Wine 默认的 locale 是 UTF-8,两边对不上就乱码。这种情况要在启动程序时设置LANG=zh_CN.GBK或者用winecfg里的区域设置来调整。我踩过的坑是:同一个程序,菜单栏乱码但内容正常,查了半天发现是菜单用的字体和内容用的字体不是同一个,只补了一种字体。
提示:判断是字体问题还是编码问题有个简单方法——如果乱码显示的是方块或者问号,多半是字体缺失;如果显示的是奇怪的字母组合(比如"ÄãºÃ"这种),那就是编码问题。两种问题的解法完全不同,别搞混了。
4. 完整实操流程与关键环节实现
4.1 环境准备与基础依赖安装
不管你是在 iOS 还是 Linux 上做,第一步都是把基础环境搭好。Linux 端相对简单,我用统信 UOS 举例,先更新系统源,然后装编译工具链和基础库:
sudo apt update sudo apt install -y build-essential cmake git python3 pkg-config sudo apt install -y libsdl2-dev libgl1-mesa-dev libvulkan-dev这些是编译 FEX-Emu 和 DXMT 需要的基础依赖。SDL2 用于窗口和输入,Mesa 提供 OpenGL 支持,Vulkan 是某些图形路径需要的。如果你不打算自己编译,直接用预编译包也行,但预编译包经常缺依赖,还是得手动补。
iOS 端就麻烦多了,你需要一个能编译和部署的环境,Xcode 是必须的,还要配置好签名和开发者模式。关于"ios 开发者模式"和"ios 26.3.1 怎么开发者模式"这类问题,核心就是在设备的设置里找到开发者选项并开启,然后在 Xcode 里信任你的开发者证书。这一步不做,后面所有部署都会失败。
RootFS 的准备我用 debootstrap 在 Linux 上生成,命令大致是:
sudo debootstrap --arch=amd64 --variant=minbase bullseye ./rootfs http://deb.debian.org/debian生成后要 chroot 进去装一些基础库,比如 libc6、libstdc++6、libgl1 这些。RootFS 不用太大,够跑目标程序就行,太大了反而拖慢启动。
4.2 FEX-Emu 的编译与配置落地
FEX-Emu 的编译我建议用 CMake,流程是标准的:
git clone https://github.com/FEX-Emu/FEX.git cd FEX git submodule update --init --recursive mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local .. make -j$(nproc) sudo make install编译过程中最容易出问题的是子模块拉取失败和依赖缺失。子模块拉取失败一般是网络问题,多试几次或者换源。依赖缺失看报错信息补对应的 dev 包就行。
编译完成后,配置~/.fex-emu/Config.json,关键字段我列一下:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| RootFS | /path/to/rootfs | 根文件系统路径 |
| EmulatedCPU | 0 | 0 表示自动检测 |
| Multiblock | true | 开启多块翻译,提升性能 |
| TSOEnabled | true | 内存序模拟,兼容性优先 |
| SMCChecks | mtrack | 自修改代码检测策略 |
| X87ReducedPrecision | true | x87 精度降低,提升性能 |
SMCChecks这个选项值得说一下,有些程序会动态修改自己的代码(比如加壳的程序),FEX 需要检测这种修改并重新翻译。mtrack是性能较好的策略,但某些程序可能需要换成full才能正常运行。
4.3 Wine Prefix 的创建与程序部署
Prefix 创建我前面说了,一个程序一个。创建完先装基础依赖:
export WINEPREFIX=/path/to/prefix export WINEARCH=win64 wineboot -u winetricks -q corefonts vcrun2019WINEARCH=win64指定 64 位架构,现在大部分程序都是 64 位的。wineboot -u是更新初始化。winetricks -q是静默安装,省得一路点确认。
装完依赖,把程序文件复制到 Prefix 的drive_c下面,比如drive_c/Program Files/YourApp/,然后用wine YourApp.exe启动。第一次启动会慢,因为要初始化各种东西,耐心等。
如果程序需要特定的 Windows 版本,用winecfg打开配置界面,在"Windows 版本"里选。我一般先用 win10 试,不行再降。有些程序还需要设置 DLL 覆盖,比如把某个 DLL 设成"原生"或"内建",这个在winecfg的"函数库"标签里配。
4.4 DXMT 的集成与图形调试
DXMT 的集成方式取决于你的部署形态。如果是 Linux 端,DXMT 通常作为 Wine 的一个 DLL 替换进去;如果是 iOS 端,DXMT 需要编译成对应的库并链接到宿主应用里。
配置 DXMT 主要通过环境变量:
export DXMT_D3D11=1 export DXMT_METAL_DEVICE=0 export DXMT_LOG_LEVEL=infoDXMT_LOG_LEVEL=info在调试阶段很有用,它会输出图形调用的日志,你能看到哪些调用成功了、哪些失败了。画面出不来的时候,先看日志里有没有"unsupported format"或者"failed to create texture"这类信息,有的话就是格式或资源问题。
图形调试我一般分三步走:第一步确认程序能启动到主界面(哪怕黑屏),第二步看日志确认 D3D 设备创建成功,第三步看具体是哪个绘制调用失败。很多时候问题出在某个特定的着色器或者纹理格式上,DXMT 的日志会告诉你具体是哪个。
注意:iOS 上 Metal 的调试可以用 Xcode 的 GPU 调试工具,能看到每一帧的绘制调用。这个工具对定位图形问题帮助极大,建议学会用。
4.5 启动脚本与参数固化
程序跑通之后,把所有配置固化成启动脚本,省得每次手动设环境变量。我一般写一个 shell 脚本:
#!/bin/bash export WINEPREFIX=/path/to/prefix export FEX_ROOTFS=/path/to/rootfs export DXMT_D3D11=1 export LANG=zh_CN.UTF-8 cd "$WINEPREFIX/drive_c/Program Files/YourApp" wine YourApp.exe "$@"脚本里把 Prefix、RootFS、DXMT、编码这些关键变量都设好,启动时直接跑脚本。如果程序需要传参,用"$@"透传。这样既方便,也避免了每次手动配置出错。
5. 常见问题与排查技巧实录
5.1 启动类问题:闪退、卡死、报错
启动类问题是最常见的,我整理了一个速查表:
| 症状 | 可能原因 | 排查方向 |
|---|---|---|
| 双击无反应 | 依赖库缺失 | 用wine命令行启动看报错 |
| 启动即闪退 | RootFS 不匹配 | 检查 glibc 版本和架构 |
| 卡在启动画面 | 图形初始化失败 | 看 DXMT 日志,检查 Metal 设备 |
| 报"找不到 xxx.dll" | 运行库缺失 | winetricks 补对应运行库 |
| 报"无法初始化" | Windows 版本不对 | winecfg 切换 Windows 版本 |
闪退问题我遇到最多的是 RootFS 里的库版本和程序要求的不一致。比如程序要求 glibc 2.31,你的 RootFS 里是 2.28,就会报符号找不到。解决办法是换一个更新的 RootFS,或者手动把需要的库复制进去。
卡死问题经常和图形有关。程序在等一个永远不来的图形事件,就卡住了。这时候看 DXMT 日志,如果日志停在某个调用上不动了,那就是那个调用出了问题。我遇到过一次是 Metal 的某个纹理格式不支持,DXMT 一直在重试,改成支持的格式就好了。
5.2 显示类问题:黑屏、花屏、乱码
显示类问题分三种:黑屏、花屏、乱码。黑屏是画面完全出不来,花屏是画面出来了但颜色或内容不对,乱码是文字显示不对。
黑屏的排查顺序是:先确认程序有没有在渲染(看日志有没有绘制调用),再确认渲染目标有没有绑定(看日志有没有 render target 相关),最后确认呈现有没有成功(看日志有没有 present 相关)。这三步能定位到大部分黑屏问题。
花屏一般是纹理格式或者颜色空间的问题。DXMT 支持的纹理格式有限,程序用了不支持的格式,转换过程中就可能出错。解决办法是在 DXMT 配置里开启更多格式支持,或者让程序用兼容的格式。
乱码问题我前面讲过,字体和编码两个方向。字体问题装 corefonts 或者复制字体文件,编码问题调 locale。这里补充一个技巧:如果只有部分文字乱码,用fc-list看看系统里有哪些字体,然后对比程序用的字体,缺哪个补哪个。
5.3 性能类问题:卡顿、掉帧、启动慢
性能问题在兼容层里很常见,因为多了一层翻译,性能损耗是必然的。但损耗多少是可以优化的。
启动慢一般是 JIT 缓存没生效。检查缓存目录是否可写,缓存是否在正常累积。第一次启动慢是正常的,第二次还慢就是缓存有问题。
运行卡顿要看是 CPU 瓶颈还是 GPU 瓶颈。用系统监控工具看 CPU 和 GPU 占用,CPU 满了就是翻译开销大,可以试试调 FEX 的多线程选项;GPU 满了就是图形转换开销大,可以试试降低分辨率或者关掉一些特效。
掉帧问题有时候和同步模式有关。DXMT 的同步策略如果设得太严格,会等 Metal 完成才返回,导致帧率上不去。可以试试放宽同步策略,但可能会引入画面撕裂,需要权衡。
提示:性能优化不要一次改太多参数,一次改一个,改完测一次,这样才能知道哪个参数起了作用。我见过有人一次改十几个参数,结果性能反而更差了,还不知道是哪个参数的问题。
5.4 独家避坑经验汇总
最后分享几个我踩过的坑,都是文档里不会写的。
第一个坑:不要在 Prefix 里装太多东西。Prefix 越干净,问题越少。装了一堆用不上的运行库,反而可能引入冲突。按需装,装完测试,没问题再装下一个。
第二个坑:RootFS 和 Prefix 的路径不要有中文和空格。Wine 和 FEX 对路径的处理有时候会有问题,中文路径可能导致找不到文件。全用英文和数字,省心。
第三个坑:备份可用的配置。调通一个程序后,把 Prefix 和配置文件备份一份。下次遇到类似程序,直接复制过来改,比从头配快得多。
第四个坑:日志是你的朋友。Wine 有WINEDEBUG环境变量,DXMT 有日志级别,FEX 也有日志。出问题先开日志,日志里的报错信息比任何猜测都准。
第五个坑:不要迷信最新版本。兼容层这东西,新版本不一定比旧版本好,有时候新版本引入了新 bug。我一般会保留一个已知稳定的版本,新版本出问题就回退。
这套 Madeira 方案我前后折腾了小半年,从完全跑不起来到能稳定运行几个常用程序,中间的经历可以说是"痛并快乐着"。如果你也在做类似的事情,我的建议是先从最简单的程序开始,跑通一个再挑战下一个,不要一上来就啃硬骨头。兼容层的问题往往是连锁的,一个环节没通,后面全是问题,循序渐进才是正道。