
1. 项目缘起为什么要在移动端折腾 x86-64 兼容层第一次看到 Madeira 这个代号是在一个折腾 FEX-Emu 的小圈子里。当时群里有人丢了一句Madeira 跑起来了Wine 那边终于不炸了配了一张 iOS 设备上开着 Windows 程序的截图。说实话我第一反应是这又是哪个营销号在编因为 x86-64 指令集翻译到 ARM64 再叠加 Wine 的 Windows API 转换这条链路在桌面端都够呛更别说移动端那点功耗和内存带宽。但后来我自己上手把 FEX-Emu、Wine、DXMT 这一套在 ARM 设备上串起来之后才明白 Madeira 这类项目真正解决的是什么问题。它不是要让你在手机上打 3A 大作而是把x86-64 的 Windows 程序能在 ARM 移动设备上跑起来这件事从理论上可行推进到日常能用。这背后涉及指令翻译、系统调用转发、图形 API 转换、以及移动端特有的生命周期管理每一层都有坑。这篇文章适合三类人看一是想在 ARM 设备上跑 Windows 老软件、老游戏的折腾党二是做跨平台兼容层、模拟器相关开发的工程师三是对 FEX-Emu、Wine、DXMT 这套技术栈好奇、想搞清楚它们各自负责哪一段的读者。我会把整个链路的原理、实操步骤、参数选择、以及我踩过的坑都摊开讲尽量让你看完能自己复现一套。先说清楚一个前提Madeira 在这里我更倾向于把它理解为一个整合层或者发行版式的项目代号它本身不发明新技术而是把 FEX-Emux86-64 到 ARM64 的指令翻译、WineWindows API 到 POSIX 的转换、DXMTDirect3D 到 Metal 的转换这几块拼在一起再针对移动端的限制做裁剪和调优。所以你要理解 Madeira就得先理解它拼的这几块各自是什么、边界在哪。2. 技术栈拆解FEX-Emu、Wine、DXMT 各自负责哪一段2.1 FEX-Emu把 x86-64 指令翻译成 ARM64FEX-Emu 的核心工作是指令级翻译。x86-64 和 ARM64 是两套完全不同的指令集寄存器数量、寻址模式、内存模型都不一样。FEX-Emu 采用的是动态二进制翻译DBT加 JIT 的路线程序运行到哪段代码就把那段 x86-64 指令翻译成 ARM64 指令翻译结果缓存起来下次直接跑缓存。这里有个关键设计叫块翻译block translation。FEX-Emu 不会一条一条翻译而是以基本块为单位一个基本块是一段顺序执行、末尾有跳转的指令序列。这样做的好处是翻译开销被摊薄坏处是遇到自修改代码或者间接跳转多的程序缓存命中率会掉得厉害。我在实测中发现FEX-Emu 对 SSE、AVX 这些 SIMD 指令的支持程度直接决定了程序能不能跑。很多 Windows 程序编译时默认开了 SSE2如果 FEX-Emu 这块翻译有问题程序启动就崩。所以选 FEX-Emu 版本时一定要看它的 changelog 里 SIMD 相关的修复记录别随便拿个老版本就上。另一个容易被忽略的点是内存模型。x86-64 是强内存模型TSOARM64 是弱内存模型。FEX-Emu 需要在翻译时插入内存屏障来保证语义正确这会带来性能损失。Madeira 这类项目如果针对移动端优化通常会在屏障插入策略上做取舍比如对单线程程序放宽屏障对多线程程序严格插入。这个取舍直接影响到多线程程序的稳定性后面讲排查的时候会再提。2.2 WineWindows API 到 POSIX 的翻译层Wine 不是模拟器它不翻译指令它翻译的是 API 调用。Windows 程序调用CreateFileWWine 把它翻译成 Linux 的open调用MessageBoxWine 用 X11 或者 Wayland 画一个出来。所以 Wine 必须和 FEX-Emu 配合FEX-Emu 负责让 x86-64 指令能在 ARM64 上跑Wine 负责让 Windows API 调用能在 Linux 上跑。Wine 的坑主要集中在三个方面。第一是 DLL 加载顺序Windows 和 Linux 的动态链接机制不一样Wine 需要自己实现一套 PE 加载器遇到依赖复杂的程序容易加载失败。第二是注册表很多程序把配置写在注册表里Wine 的注册表实现虽然完整但默认的初始内容和真实 Windows 有差异程序可能因为读不到某个键而行为异常。第三是字体和编码这也是热词里wine 乱码的来源。关于乱码我后面会单独开一节讲因为这个问题在中文环境下太常见了而且排查思路很固定值得展开。2.3 DXMTDirect3D 到 Metal 的桥梁DXMT 是 Direct3D 到 Metal 的转换层主要针对 macOS 和 iOS。它的工作是把 D3D11、D3D12 的调用翻译成 Metal 的调用。为什么不用 DXVK因为 DXVK 是 D3D 到 Vulkan而 iOS 上 Vulkan 支持很弱Metal 才是原生图形 API。所以在 iOS 设备上跑 Windows 游戏DXMT 是绕不开的一环。DXMT 的难点在于 D3D 和 Metal 的模型差异。D3D 有资源状态、有命令列表、有描述符堆Metal 的模型更接近底层。DXMT 需要在两者之间做映射映射不好的地方就会出现渲染错误、性能骤降、甚至崩溃。我在测试中遇到过一个典型问题D3D11 的Map/Unmap在 Metal 上没有直接对应DXMT 需要用 staging buffer 中转如果程序频繁 Map 小 buffer性能会掉得很惨。2.4 三者的协作关系把这三块串起来看一个 Windows 程序的执行路径是这样的程序入口是 x86-64 指令FEX-Emu 把它翻译成 ARM64 执行程序调用 Windows APIWine 拦截并翻译成 POSIX 调用程序调用 D3D 画图DXMT 拦截并翻译成 Metal 调用。三层各管一段任何一层出问题程序都跑不起来。这也解释了为什么 Madeira 这类整合项目的价值单独装 FEX-Emu、单独装 Wine、单独装 DXMT配置起来极其繁琐而且版本兼容性很难保证。整合层把这些依赖固定下来提供一个开箱即用的环境这才是它真正的贡献。3. 实操环境搭建从零把链路跑通3.1 环境准备与依赖检查先说硬件前提。ARM64 设备内存建议 8GB 起步因为 FEX-Emu 的翻译缓存、Wine 的 PE 加载器、DXMT 的资源映射都要吃内存。存储建议留 20GB 以上Wine prefix 加上程序本身很容易就吃掉几个 G。系统层面你需要一个能跑 Linux 用户态的环境。如果是桌面 ARM 设备直接装发行版就行如果是移动设备情况会复杂一些涉及系统权限和运行环境的问题这里不展开只讨论用户态可操作的部分。依赖检查我习惯用一张表来过避免漏装依赖项作用检查命令glibc基础 C 库ldd --versionlibGL / libEGL图形上下文ls /usr/lib/aarch64-linux-gnu/libGL*fontconfig字体配置fc-listfreetype字体渲染pkg-config --modversion freetype2libpulse / pipewire音频pactl infoVulkan loader部分场景需要vulkaninfo装依赖的时候有个经验不要一次性把所有包都装上先装最小集跑起来再补。因为有些包之间会冲突一次性装完出问题很难定位是哪个包引起的。3.2 FEX-Emu 的编译与配置FEX-Emu 我建议从源码编译因为发行版仓库里的版本往往偏旧SIMD 支持不全。编译前先确认你的编译器支持 ARM64 的 NEON 和 SVEgcc -marchnative -Q --helptarget | grep -i neon能看出来。编译流程大致是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_LTOON make -j$(nproc)这里有几个参数值得说。ENABLE_LTOON开启链接时优化能提升翻译后代码的执行效率但编译时间会变长。CMAKE_BUILD_TYPERelease必须开Debug 版本的 FEX-Emu 慢到没法用。-j$(nproc)用满核心但如果你内存不够并行度太高会 OOM可以降到-j4。编译完之后FEX-Emu 需要一个 rootfs 来提供 x86-64 的系统库。这个 rootfs 通常是一个精简的 x86-64 Linux 文件系统FEX-Emu 在里面跑程序。rootfs 的构建可以用 debootstrap 或者直接下载现成的。我倾向于自己构建因为可以控制里面装了什么减少不必要的依赖。配置 FEX-Emu 的时候FEX_ROOTFS环境变量指向 rootfs 路径FEX_APP_CONFIG可以指定每个程序的配置。配置文件里比较重要的项是TSOEnabled控制是否启用强内存模型模拟。单线程程序可以关掉提升性能多线程程序必须开。3.3 Wine 的安装与 prefix 初始化Wine 的安装相对简单但 prefix 初始化有讲究。WINEPREFIX环境变量指定 prefix 路径第一次运行winecfg会初始化 prefix。初始化时会提示装 Wine Mono 和 Wine Gecko这两个是 .NET 和 HTML 渲染的替代实现很多程序依赖它们。热词里有wine gecko官方正版下载说明很多人卡在这一步。我的建议是让 Wine 自动下载如果网络不通可以手动下载对应的 msi 包放到指定目录Wine 会自己识别。手动放置的路径通常是~/.cache/wine/文件名要和 Wine 期望的一致。prefix 初始化完之后建议先跑一个简单的程序验证比如wine notepad。如果记事本能起来说明 Wine 基本正常如果起不来看报错信息通常是缺 DLL 或者图形驱动问题。Wine 的版本选择也有讲究。稳定版stable适合日常使用开发版devel新特性多但可能不稳定staging 版包含一些未合并的补丁对游戏兼容性更好。Madeira 这类项目通常会固定一个版本你跟着用就行不要自己乱升级。3.4 DXMT 的集成与图形后端选择DXMT 的集成是整条链路里最麻烦的一步。它需要和 Wine 的图形驱动对接Wine 通过WINEDLLOVERRIDES环境变量来决定用哪个 d3d 实现。要让 DXMT 生效需要把d3d11、dxgi这些 DLL 覆盖成 DXMT 提供的版本。export WINEDLLOVERRIDESd3d11,dxgin,b这里的n,b表示优先用 nativeDXMT 提供的失败再 fallback 到 builtinWine 自带的。这个顺序很重要反了的话 DXMT 就不生效。图形后端的选择上Metal 是 iOS 和 macOS 的原生选择Linux 上则要看具体环境。如果是在 Linux 上跑DXMT 需要一层 Metal 到 Vulkan 的转换这个转换层本身也有开销。所以 Linux 上我更推荐 DXVKDXMT 留给 Apple 平台。DXMT 的配置里有个DXMT_MAX_FRAME_LATENCY参数控制最大帧延迟。移动端为了省电可以设成 2 或 3桌面端设成 1 追求低延迟。这个参数对游戏体验影响很大值得根据场景调。4. 中文乱码与字体问题的系统排查4.1 乱码的三种典型表现wine 乱码这个热词背后其实是三类不同的问题表现相似但根因不同。第一类是方块乱码字符显示成一个个方框。这是字体缺失系统里没有能渲染这些字符的字体。第二类是问号乱码所有非 ASCII 字符都变成?。这是编码问题程序用的编码和 Wine 的 locale 不匹配。第三类是错位乱码字符能显示但顺序乱了或者显示成别的字。这是字体映射问题Wine 把某个字符映射到了错误的字形。分清楚是哪一类排查方向就明确了。方块查字体问号查 locale错位查字体映射表。4.2 字体安装与注册表配置解决方块乱码核心是装中文字体并让 Wine 认识它们。Linux 系统层面装fonts-noto-cjk或者fonts-wqy-zenhei然后fc-cache -fv刷新字体缓存。但光装系统字体不够Wine 有自己的字体映射机制。Wine 会把 Windows 字体名比如SimSun、Microsoft YaHei映射到系统字体。这个映射关系在注册表里路径是HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\FontSubstitutes。我通常的做法是直接改注册表把常用的 Windows 字体名映射到 Noto Sans CJKwine reg add HKLM\Software\Microsoft\Windows NT\CurrentVersion\FontSubstitutes /v SimSun /d Noto Sans CJK SC /f wine reg add HKLM\Software\Microsoft\Windows NT\CurrentVersion\FontSubstitutes /v Microsoft YaHei /d Noto Sans CJK SC /f这样程序请求 SimSun 时Wine 会给它 Noto Sans CJK中文就能正常显示了。4.3 locale 与编码设置问号乱码通常是 locale 没设对。Wine 依赖系统的 locale 来决定默认编码。如果系统 locale 是C或者POSIXWine 会用 ASCII 编码非 ASCII 字符全变问号。解决办法是设LANG和LC_ALLexport LANGzh_CN.UTF-8 export LC_ALLzh_CN.UTF-8但这里有个坑如果系统没生成zh_CN.UTF-8这个 locale设了也没用。用locale -a检查没有的话用locale-gen生成。还有一个更隐蔽的坑有些程序自己带编码设置不读系统 locale。这种情况需要在程序内部设置或者用WINEDLLOVERRIDES覆盖相关的 DLL。这个就比较个案了得具体程序具体分析。4.4 字体平滑与渲染质量调优字体能显示之后还有个渲染质量的问题。Wine 默认的字体渲染可能发虚或者锯齿严重。这跟 fontconfig 的配置有关可以调~/.config/fontconfig/fonts.conf里的hinting、antialias、rgba参数。移动端屏幕小、PPI 高我建议开antialias和hintingrgba设成rgb或者none。桌面端可以关hinting追求更平滑的效果。这个没有绝对最优看个人喜好和屏幕特性。5. 性能调优与常见崩溃排查5.1 翻译缓存的预热与持久化FEX-Emu 的翻译缓存默认在内存里程序退出就没了。下次启动又要重新翻译冷启动很慢。FEX-Emu 支持把缓存持久化到磁盘通过FEX_APP_CONFIG里的CachePath配置。持久化缓存的好处是第二次启动快很多坏处是缓存文件可能很大而且程序更新后缓存可能失效。我的做法是给常用程序开持久化不常用的关掉平衡磁盘和速度。预热缓存有个技巧第一次跑程序时尽量把主要功能都点一遍让 FEX-Emu 把主要代码路径都翻译并缓存下来。这样后续使用就快了。我试过跑一个大型软件第一次冷启动花了三分钟预热之后第二次启动只要二十秒。5.2 内存占用控制整条链路的内存占用主要来自三块FEX-Emu 的翻译缓存、Wine 的 PE 加载器、DXMT 的资源映射。移动端内存紧张需要控制。FEX-Emu 这边可以限制缓存大小超过阈值就淘汰旧缓存。Wine 这边可以关掉不必要的服务比如wineboot里的一些后台进程。DXMT 这边可以降低纹理质量、减少 staging buffer 数量。我实测下来一个中等复杂度的 Windows 程序整条链路的内存占用在 1.5GB 到 3GB 之间。如果你的设备只有 4GB 内存跑起来会很吃力建议加 swap 或者换更轻量的程序。5.3 常见崩溃与排查速查表崩溃是这条链路上最常见的问题我整理了一张速查表崩溃现象可能原因排查方向启动即崩无输出FEX-Emu 翻译失败看 FEX-Emu 日志检查 SIMD 指令支持启动后闪退Wine DLL 加载失败WINEDEBUGloaddll看加载过程图形界面崩溃DXMT 转换错误看 DXMT 日志检查 D3D 特性支持运行中随机崩溃内存模型问题开启 TSO检查多线程同步特定操作崩溃API 未实现WINEDEBUGrelay看最后调用的 API排查的核心思路是分层定位先确定是 FEX-Emu、Wine 还是 DXMT 的问题再深入那一层。WINEDEBUG环境变量是 Wine 排查的利器loaddll看 DLL 加载relay看 API 调用seh看异常。但relay输出量巨大只适合定位具体问题不要长时间开着。5.4 多线程程序的稳定性处理多线程程序是这条链路上最容易出问题的。前面提过x86-64 是强内存模型ARM64 是弱内存模型FEX-Emu 需要插入屏障来保证语义。如果屏障插入不够多线程程序会出现数据竞争表现为随机崩溃或者结果错误。FEX-Emu 的TSOEnabled配置控制这个。开启后屏障插入更严格稳定性提升但性能下降。我的经验是多线程程序必须开 TSO单线程程序可以关。判断程序是否多线程可以用ps -eLf看线程数或者看程序文档。还有一个相关的问题是原子操作。x86-64 的原子操作和 ARM64 的实现不一样FEX-Emu 需要正确翻译。如果程序大量使用原子操作比如无锁数据结构FEX-Emu 的翻译质量直接决定程序能不能跑。这个只能靠测试遇到问题就报给 FEX-Emu 社区。6. 移动端特有的适配问题6.1 生命周期与后台管理移动端和桌面端最大的区别是生命周期管理。移动系统会随时把后台应用挂起或杀掉而 Windows 程序通常假设自己一直运行。这个矛盾会导致程序在切到后台后状态丢失切回来时崩溃。处理这个问题的思路是在 Wine 层面拦截生命周期事件把挂起信号转换成 Windows 的电源事件。但 Windows 程序对电源事件的处理千差万别有的程序正确处理了有的没有。所以实际表现就是有的程序切后台没事有的切回来就崩。我的建议是对生命周期敏感的程序尽量在前台用完再切走不要长时间挂后台。如果必须挂后台先保存工作进度。6.2 触摸输入到鼠标键盘的映射Windows 程序假设有鼠标和键盘移动端只有触摸屏。这个映射层通常由 Wine 或者整合层提供。基本的映射是单指点击对应左键双指对应右键长按对应右键菜单。但复杂操作比如拖拽、滚轮、组合键映射起来就很别扭。我在实测中发现触摸映射的质量直接影响可用性。好的映射会考虑触摸的惯性、精度、手势识别差的映射就是简单的一对一用起来很累。如果你要长时间用某个程序建议外接蓝牙鼠标键盘体验会好很多。6.3 功耗与发热控制x86-64 到 ARM64 的翻译本身就有开销再加上 Wine 和 DXMT 的转换整条链路的功耗比原生程序高不少。移动端电池有限发热也受限长时间跑会导致降频性能进一步下降。控制功耗的手段有几个限制 FEX-Emu 的翻译线程数降低 DXMT 的帧率上限关掉不必要的后台服务。我实测下来把帧率限制在 30fps功耗能降三分之一左右发热也明显改善。如果程序对帧率不敏感这个取舍很划算。6.4 存储与权限的边界移动端的存储权限比桌面端严格Wine prefix 的路径可能不在可写目录里。这个需要在初始化 prefix 时指定一个可写路径通常是应用自己的数据目录。另外Windows 程序习惯用的路径比如C:\Program Files在移动端映射到哪里也需要配置。Wine 默认把C:映射到 prefix 下的drive_c这个通常没问题。但如果程序硬编码了绝对路径就可能找不到文件。这种情况需要用winecfg里的驱动器映射来调整。7. 我踩过的坑与实操心得7.1 版本组合的坑FEX-Emu、Wine、DXMT 三者的版本兼容性是个大坑。我试过用最新版的 FEX-Emu 配老版本的 Wine结果 Wine 启动就崩查了半天发现是 FEX-Emu 的某个 ABI 变了老 Wine 不兼容。后来我学乖了要么用整合项目固定好的版本组合要么自己记录一套验证过的版本号不要随便升级。具体来说FEX-Emu 的 rootfs 和 Wine 的版本要匹配rootfs 里的库版本太新或太旧都会出问题。DXMT 和 Wine 的 d3d 接口也要匹配DXMT 更新了 d3d 接口Wine 那边没跟上就会加载失败。7.2 日志分析的技巧排查问题时日志是最重要的线索。但这条链路的日志分散在三个地方FEX-Emu 的日志、Wine 的日志、DXMT 的日志。默认情况下它们各写各的出问题时很难对应起来。我的做法是开一个统一的日志目录用环境变量把三者的日志都指过去然后按时间戳排序看。这样能看出问题的先后顺序定位是哪一层先出的错。FEX-Emu 的日志用FEX_LOG_LEVEL控制Wine 用WINEDEBUGDXMT 用DXMT_LOG_LEVEL。还有一个技巧是给日志加时间戳。默认日志可能没有精确时间加上时间戳后跨层的问题定位会容易很多。7.3 性能与稳定性的取舍这条链路上性能和稳定性往往是矛盾的。开 TSO 稳定但慢关 TSO 快但可能崩开持久化缓存快但占磁盘不开省磁盘但冷启动慢DXMT 高质量渲染好看但费电低质量省电但难看。我的取舍原则是先保证稳定再优化性能。一个跑得慢但稳定的环境比一个跑得快但随时崩的环境有用得多。等稳定了再逐项调优每调一项就测一轮确认没引入新问题再调下一项。7.4 社区资源的利用FEX-Emu、Wine、DXMT 都有自己的社区遇到问题先搜社区大概率有人遇到过。搜的时候用具体的报错信息不要用跑不起来这种模糊描述。报错信息里的函数名、错误码都是搜索的好关键词。如果社区搜不到再考虑自己排查。排查的时候最小化复现是关键。把问题程序简化到最小去掉无关的依赖和操作这样问题更容易定位。我试过把一个复杂程序的问题简化到一个简单的测试用例然后发现是 FEX-Emu 对某个特定指令的翻译有 bug报给社区后很快就修了。8. 后续可以扩展的方向这套链路跑通之后其实还有很多可以折腾的方向。比如把 DXMT 换成 DXVK 加 Vulkan 转换层看看在特定场景下哪个性能更好比如给 FEX-Emu 加自定义的指令优化针对特定程序提升性能比如把整条链路容器化方便在不同设备上迁移。我个人比较感兴趣的是翻译缓存的共享。如果多台设备能共享同一份翻译缓存那新设备的冷启动就能大幅缩短。这个需要缓存格式的标准化和网络传输的支持技术上可行但工程上还有不少工作要做。另外随着 ARM 设备性能的提升这条链路的性能瓶颈会逐渐从翻译开销转移到图形转换。DXMT 这类图形转换层的优化可能会成为下一个重点。如果你在做相关的工作这块值得投入。最后分享一个小技巧调试的时候先用一个最简单的 Windows 程序比如记事本验证整条链路确认没问题再上复杂程序。这样能把链路问题和程序问题分开排查起来事半功倍。我见过太多人一上来就跑大型游戏结果卡在链路问题上白白浪费时间。