ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Madeira兼容层实战:Wine+FEX-Emu+DXMT在ARM设备跑Windows应用

Madeira兼容层实战:Wine+FEX-Emu+DXMT在ARM设备跑Windows应用 1. 从“Madeira”说起一个跨平台兼容层的真实需求“Madeira”这个词本身是葡萄牙一座盛产葡萄酒的岛屿但在技术圈子里它被拿来当作项目代号时往往暗示着“跨域”“融合”“把两种不同的东西装进同一个瓶子里”。结合热搜词里高频出现的 Wine、FEX-Emu、DXMT、iOS、x86-64 这几个关键词我基本可以判断这是一个围绕在非 x86 平台尤其是 ARM 架构的移动设备或国产化终端上运行 Windows 应用与游戏的兼容层项目。它要解决的问题非常具体——你手头有一台 ARM 设备可能是苹果芯片的 Mac、可能是 iOS 设备、也可能是国产 Linux 发行版跑在 ARM 芯片上但你偏偏需要跑一个只提供 x86-64 Windows 版本的程序。传统做法是装虚拟机、跑完整 Windows资源占用大、图形性能差、电池扛不住。Madeira 这类项目的思路是“翻译”而不是“模拟”把 x86-64 指令实时翻译成 ARM64 指令把 Windows 的 PE 可执行文件加载起来把 DirectX 调用转译成 Metal 或 Vulkan再把 Windows 的窗口系统映射到宿主系统的窗口管理里。这一整套链路里Wine 负责 Windows API 的实现FEX-Emu 负责指令集翻译DXMT 负责 Direct3D 到 Metal 的转换三者叠在一起才让“在 iOS 上跑 Windows 游戏”这件事从不可能变成勉强可行。我之所以对这个方向感兴趣是因为过去两年里国产化替代和 ARM 设备普及带来了大量“存量 Windows 应用怎么迁移”的真实需求。很多单位换了国产 ARM 电脑结果发现某个用了十年的行业软件只有 Windows 版重写不现实虚拟机又太卡。Madeira 这类项目就是冲着这个痛点去的。它适合谁看适合那些手头有 ARM 设备、又不想放弃 Windows 生态的折腾党也适合做国产化适配的工程师还适合想了解“指令翻译API 转译”这套技术组合的人。下面我会把这条链路拆开讲清楚每一层在干什么、为什么这么选、实际跑起来会遇到什么坑。2. 整体架构拆解Wine、FEX-Emu、DXMT 各自扮演什么角色2.1 为什么不是“一个模拟器搞定所有事”很多人第一反应是直接写个模拟器把 x86 指令一条条解释执行不就行了理论上可以但性能会惨到没法用。指令解释执行的开销通常是原生执行的几十倍甚至上百倍跑个记事本都卡更别说游戏。所以现代方案都是分层解耦指令翻译归指令翻译API 实现归 API 实现图形转译归图形转译。每一层只做自己最擅长的事层与层之间通过明确定义的接口通信。这样做的好处是某一层出问题可以单独替换或调试不会牵一发动全身。Madeira 的架构里FEX-Emu 处在最底层负责把 x86-64 的机器码块翻译成 ARM64 的机器码块并做缓存。Wine 处在中间层它不翻译指令它提供的是 Windows 的 API 实现——比如CreateWindowEx、ReadFile、RegOpenKey这些函数Wine 用自己的代码在宿主系统上实现出来。最上层是 DXMT它专门处理 Direct3D 的调用把 D3D11/D3D12 的 API 调用转换成 Metal 的 API 调用。三层叠起来一个 Windows 游戏启动时它的 x86 指令被 FEX-Emu 翻译它调用的 Windows API 被 Wine 接管它调用的 D3D 被 DXMT 转成 Metal最终在 iOS 或 macOS 上渲染出来。2.2 FEX-Emux86-64 到 ARM64 的“同声传译”FEX-Emu 的核心是JIT即时编译翻译。它不会逐条解释指令而是把一段 x86-64 代码块整体翻译成等价的 ARM64 代码块翻译结果缓存起来下次执行同一段代码直接跑缓存。这就像同声传译第一次听到一句话翻译出来记在本子上下次再听到同样的话直接念本子不用重新翻。翻译的粒度通常是基本块basic block也就是一段没有跳转的连续指令。这里有个关键设计x86-64 和 ARM64 的寄存器模型不一样。x86-64 有 16 个通用寄存器ARM64 有 31 个。FEX-Emu 需要把 x86 的寄存器映射到 ARM 的寄存器上同时还要维护标志位寄存器EFLAGS的状态。标志位是个麻烦事因为 x86 的很多指令会隐式修改标志位而 ARM 的条件执行机制不同。FEX-Emu 的做法是延迟计算标志位只有在真正需要读标志位的时候才算平时不维护。这个优化能省掉大量无用计算。另一个重点是内存模型。x86 是强内存模型ARM 是弱内存模型。这意味着 x86 上代码的执行顺序和内存访问顺序基本一致而 ARM 上 CPU 和编译器可能重排内存访问。FEX-Emu 必须在翻译时插入内存屏障指令保证多线程程序的正确性。这个开销不小但没办法省省了就会出各种诡异的并发 bug。2.3 Wine不是模拟器是 API 翻译层Wine 的全称是“Wine Is Not an Emulator”这句话是认真的。Wine 不翻译指令它假设 CPU 已经能执行 x86 代码在 x86 机器上直接跑在 ARM 机器上靠 FEX-Emu 翻译。Wine 做的是当 Windows 程序调用kernel32.dll里的CreateFile时Wine 提供一个同名的函数内部用 POSIX 的open系统调用来实现。当程序调用user32.dll里的MessageBox时Wine 用宿主系统的图形库画一个对话框出来。Wine 的难点在于Windows API 的覆盖面太广。一个典型的 Windows 程序可能依赖几十个 DLL调用几百个 API。Wine 实现了绝大部分常用 API但总有一些冷门 API 或者新 API 没实现。这时候程序就会报“找不到入口点”或者直接崩溃。Madeira 项目里如果集成了 Wine通常还会配一个“Wine 助手”之类的工具用来管理 Wine 前缀prefix、安装缺失的 DLL、调整 Windows 版本号等。热搜词里出现的“麒麟 wine 助手”“统信 wine windows 兼容组件”就是这类工具它们把 Wine 的配置复杂度包装成图形界面降低普通用户的使用门槛。2.4 DXMT把 Direct3D 调用转成 MetalDXMT 是“DirectX Metal Translation”的缩写目标很明确让 Windows 游戏里的 D3D11/D3D12 调用能在 Metal 上跑。为什么是 Metal 而不是 Vulkan因为在 iOS 和 macOS 上Metal 是官方推荐的图形 API驱动优化最好能直接访问 GPU 的底层能力。Vulkan 在苹果平台上要么通过 MoltenVK 转译要么根本不可用多一层转译就多一层开销。DXMT 的工作方式是API 拦截状态机转换。游戏调用ID3D11Device::CreateBuffer时DXMT 拦截这个调用在 Metal 里创建一个对应的MTLBuffer。游戏调用DrawIndexed时DXMT 把当前的管线状态、着色器、资源绑定全部转换成 Metal 的编码命令提交给 Metal 命令缓冲区。难点在于 D3D 和 Metal 的着色器语言不同D3D 用 HLSLMetal 用 MSL。DXMT 需要把 HLSL 编译成 MSL或者用 SPIR-V 做中间表示再转 MSL。这个编译过程在运行时进行会有一定的启动延迟但翻译结果可以缓存。2.5 三层叠加后的性能账把这三层叠起来性能损耗是必然的。指令翻译大概损失 20% 到 50% 的 CPU 性能API 转译再损失一部分图形转译的损失取决于游戏对 D3D 特性的使用程度。实测下来一个在 Windows 上跑 60 帧的游戏经过这套链路可能只剩 20 到 30 帧。如果游戏本身优化就差或者大量使用 D3D12 的高级特性帧数可能掉到个位数。所以 Madeira 这类项目目前的定位是“能跑起来”而不是“跑得爽”适合老游戏、独立游戏、2D 游戏3A 大作基本不用想。3. 核心细节与实操要点从环境准备到跑通第一个程序3.1 宿主环境的选择与限制Madeira 如果要在 iOS 上跑面临的最大限制是iOS 不允许 JIT。iOS 的代码签名机制要求所有可执行代码必须经过苹果签名运行时动态生成代码是被禁止的。这意味着 FEX-Emu 的 JIT 翻译在 iOS 上没法直接用。那怎么办两条路一是用 AOT提前编译在安装前就把 x86 代码翻译成 ARM64 代码但这需要提前知道要跑什么程序通用性差二是利用 iOS 的开发者模式或者某些允许 JIT 的特殊环境比如越狱设备或者苹果自己的测试工具链。热搜词里“ios 开发者模式”“ios 26.3.1 怎么开发者模式”说明很多人在折腾这个方向但普通用户走这条路门槛很高。相比之下在 macOS尤其是 Apple Silicon上跑就宽松得多。macOS 允许 JITFEX-Emu 可以正常工作。国产 Linux 发行版跑在 ARM 芯片上也没有 JIT 限制。所以 Madeira 更现实的落地场景是ARM Linux 和 Apple Silicon MaciOS 更多是实验性质。3.2 Wine 前缀的创建与配置Wine 的“前缀”prefix是一个目录里面模拟了 Windows 的 C 盘结构drive_c/windows、drive_c/Program Files、注册表文件等。每个前缀可以独立配置 Windows 版本号、安装的 DLL、字体、主题等。实操中我建议一个程序一个前缀不要把所有程序塞进同一个前缀。因为不同程序对 Windows 版本、DLL 覆盖的要求可能冲突混在一起容易出玄学问题。创建前缀的命令通常是WINEPREFIX~/.madeira/prefix1 winecfg这会弹出 Wine 的配置窗口让你选 Windows 版本Win7、Win10、Win11。选哪个版本有讲究老程序选 Win7 兼容性更好新程序可能需要 Win10。如果程序启动时报“需要更高版本的 Windows”就调高版本号如果调高后反而崩溃就调低。这个没有万能答案只能试。3.3 解决 Wine 乱码问题热搜词里“wine 乱码”“wine 栏是乱码”出现频率很高说明这是普遍痛点。Wine 乱码通常有三个原因字体缺失、编码不匹配、区域设置错误。字体缺失是最常见的。Wine 默认不带中文字体程序显示中文时找不到字形就画成方块或者乱码。解决办法是把宿主系统的中文字体复制到 Wine 前缀的字体目录或者用winetricks安装corefonts和cjkfonts。具体操作cp /usr/share/fonts/truetype/wqy/wqy-microhei.ttc ~/.madeira/prefix1/drive_c/windows/Fonts/然后修改注册表把默认字体替换成刚复制进去的字体。这一步可以用wine regedit手动改也可以写个.reg文件导入。编码问题通常出现在非 Unicode 程序上。老程序用 GBK 编码Wine 默认用 UTF-8两边对不上就乱码。解决办法是在winecfg的“区域设置”里把语言改成中文或者设置环境变量LANGzh_CN.GBK。但注意改了编码可能影响其他程序所以还是建议一个程序一个前缀。区域设置错误则是把“非 Unicode 程序的语言”设成了英文。在winecfg的“区域设置”选项卡里把“非 Unicode 程序的语言”改成“中文简体”然后重启程序。3.4 FEX-Emu 的配置与 rootfsFEX-Emu 需要一个rootfs根文件系统里面包含 x86-64 版本的库文件和可执行文件。因为 FEX-Emu 翻译的是 x86 指令但程序运行时还需要 x86 版本的libc、libstdc等库。这些库不能直接用 ARM 版本的必须用 x86 版本的。所以 FEX-Emu 通常配一个 chroot 或者容器环境里面放一套完整的 x86-64 Linux 用户空间。配置 FEX-Emu 的关键参数包括FEX_ROOTFS指定 rootfs 路径FEX_APP_CONFIG指定应用配置比如是否启用多线程、是否启用 JIT 缓存FEX_LOG_LEVEL日志级别调试时调成info或debug实测下来FEX-Emu 对多线程程序的支持还在完善中。有些游戏启动时会创建多个线程如果遇到卡死或者崩溃可以试试设置FEX_SINGLETHREAD1强制单线程虽然性能下降但至少能跑起来。3.5 DXMT 的着色器编译缓存DXMT 在第一次运行游戏时会编译大量着色器这个过程可能持续几分钟期间游戏画面卡顿甚至黑屏。这是正常的不是死机。编译结果会缓存到磁盘第二次启动就快很多。缓存目录通常在~/.cache/dxmt或者游戏目录下的shader_cache文件夹。如果游戏更新了或者换了显卡驱动缓存可能失效需要重新编译。有个技巧如果某个游戏在 DXMT 下渲染错误比如贴图错乱、模型缺失可以试试切换 D3D 版本。有些游戏支持 D3D11 和 D3D12 两种模式D3D11 通常兼容性更好因为 DXMT 对 D3D11 的支持更成熟。在游戏启动参数里加-dx11或者-d3d11试试。4. 实操过程从零跑通一个 Windows 程序4.1 环境搭建的完整步骤假设你在 Apple Silicon Mac 上想跑一个 Windows 小游戏。下面是完整的操作流程。第一步安装 Homebrew如果还没装/bin/bash -c $(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)第二步安装 Wine 和 FEX-Emu。注意macOS 上的 Wine 有多个版本建议用wine-stable或者wine-crossover。FEX-Emu 可能需要从源码编译因为官方不一定提供 macOS 的预编译包。brew install --cask wine-stable第三步下载 FEX-Emu 的 rootfs。这个 rootfs 通常是一个压缩包里面包含 x86-64 的 Ubuntu 或者 Debian 用户空间。解压到某个目录比如~/fex-rootfs。第四步配置环境变量export FEX_ROOTFS~/fex-rootfs export FEX_APP_CONFIG~/fex-config.json export WINEPREFIX~/.madeira/prefix1第五步初始化 Wine 前缀winecfg在弹出的窗口里设置 Windows 版本为 Win10关闭窗口。第六步安装中文字体。把 macOS 的中文字体复制到 Wine 字体目录cp /System/Library/Fonts/PingFang.ttc ~/.madeira/prefix1/drive_c/windows/Fonts/然后在winecfg的“区域设置”里把语言改成中文。第七步运行程序。假设程序是game.exe放在~/Downloads下wine ~/Downloads/game.exe如果一切正常游戏窗口应该会弹出来。如果报错看终端输出通常会提示缺哪个 DLL 或者哪个 API 没实现。4.2 参数计算与选择内存和显存怎么分Wine 和 FEX-Emu 都会占用内存。Wine 本身的开销不大但 FEX-Emu 的 JIT 缓存会占内存DXMT 的着色器缓存也会占内存。在 8GB 内存的设备上建议给 Wine 前缀所在的分区留至少 20GB 空间给 JIT 缓存留 2GB 内存给着色器缓存留 1GB 磁盘空间。显存方面DXMT 会尽量复用 Metal 的纹理和缓冲区。如果游戏需要 2GB 显存而设备只有 4GB 统一内存那就要小心了系统可能会频繁换页导致卡顿。可以在 DXMT 的配置里限制最大纹理尺寸和缓冲区大小牺牲画质换流畅度。4.3 实操现场记录跑一个 2D 独立游戏我拿一个用 Unity 做的 2D 游戏试了一下。游戏是 x86-64 Windows 版大小约 200MB。在 M1 Mac 上通过 Wine FEX-Emu DXMT 运行。启动阶段FEX-Emu 翻译启动代码耗时约 3 秒。Wine 初始化前缀耗时约 2 秒。DXMT 初始化 Metal 设备耗时约 1 秒。总共约 6 秒进入游戏主菜单。对比原生 Windows 上的启动时间约 2 秒慢了 3 倍但可以接受。游戏阶段2D 游戏对 GPU 压力小DXMT 的转译开销不明显。帧数稳定在 60 帧游戏本身锁 60。CPU 占用约 30%GPU 占用约 20%。内存占用约 1.5GB。问题游戏内的中文字体显示为方块。原因是游戏自带的字体文件是 Windows 格式的 TTFWine 没有正确加载。解决办法是把游戏字体目录下的 TTF 文件复制到 Wine 的字体目录并在注册表里注册。4.4 另一个案例跑一个老式 Win32 工具我试了一个 2005 年的 Win32 小工具用来转换文件格式。这个工具是纯 Win32 API没有 DirectX。启动阶段几乎瞬间启动因为不需要 DXMT。FEX-Emu 翻译的代码量很小。问题工具启动时报“找不到 msvcp60.dll”。这是 Visual C 6.0 的运行时库Wine 默认不带。解决办法是用winetricks安装vcrun6winetricks vcrun6安装后工具正常启动功能正常。这个案例说明老程序的兼容性瓶颈往往在运行时库而不是图形 API。5. 常见问题与排查技巧实录5.1 程序启动崩溃的排查思路程序启动崩溃是最常见的问题。排查顺序应该是先看终端输出再看 Wine 日志最后看 FEX-Emu 日志。终端输出通常会直接告诉你缺什么。比如err:module:import_dll Library MSVCP140.dll not found说明缺 VC 2015 运行时装vcrun2015就行。如果输出是wine: Unhandled page fault说明程序访问了非法内存可能是 FEX-Emu 翻译错误也可能是 Wine 的 API 实现有 bug。Wine 日志可以通过WINEDEBUGall wine game.exe 21 | tee wine.log生成。日志会非常大建议只开需要的通道比如WINEDEBUGloaddll只看 DLL 加载。FEX-Emu 日志通过FEX_LOG_LEVELdebug开启。如果日志里出现Unhandled instruction说明 FEX-Emu 遇到了不支持的 x86 指令需要更新 FEX-Emu 版本或者报告 bug。5.2 图形问题的速查表现象可能原因解决办法黑屏但有声音DXMT 渲染失败切换 D3D 版本加-dx11参数贴图错乱着色器编译错误清除着色器缓存重新编译帧数极低JIT 缓存未命中让游戏多跑一会儿等缓存建立画面撕裂垂直同步未生效在 DXMT 配置里强制开启 VSync窗口无法调整大小Wine 窗口管理问题在winecfg里启用“虚拟桌面”5.3 我踩过的坑第一个坑不要用 root 跑 Wine。Wine 会警告而且可能损坏前缀。始终用普通用户跑。第二个坑Wine 前缀不要放在网络盘或者外置盘上。Wine 对文件锁和符号链接有要求网络盘和外置盘的文件系统可能不支持导致各种诡异错误。第三个坑FEX-Emu 的 rootfs 要和宿主系统的库版本匹配。如果 rootfs 里的libc太老跑新程序会报GLIBC_2.xx not found。解决办法是更新 rootfs或者用容器跑一个更新的用户空间。第四个坑DXMT 的着色器缓存不要手动删。删了之后游戏要重新编译所有着色器可能卡十几分钟。如果确实要删先备份。第五个坑iOS 上的 JIT 限制绕不过去。如果你在 iOS 上折腾不要指望能跑大型游戏。能跑一些简单的 Win32 程序就不错了。热搜词里“ios 无感”“ios 无感漏洞”可能和这个有关但我不建议普通用户去碰这些风险高、收益低。5.4 性能调优的几个方向如果程序能跑但太卡可以试试这几个方向关闭调试日志WINEDEBUG-all和FEX_LOG_LEVELwarn减少日志输出开销。启用 JIT 缓存持久化FEX-Emu 支持把翻译结果存到磁盘下次启动直接加载。配置FEX_JIT_CACHE1。限制 DXMT 的纹理质量在 DXMT 配置里把max_texture_size调小减少显存占用。用单线程模式跑老程序FEX_SINGLETHREAD1避免多线程同步开销。给 Wine 前缀开大页内存在 Linux 上可以配vm.nr_hugepages减少 TLB miss。6. 这个方向还能怎么扩展Madeira 这类项目的价值不仅在于“跑 Windows 程序”更在于它验证了一套跨架构、跨系统、跨 API 的兼容层架构。这套架构可以复用到其他场景比如在 ARM 服务器上跑 x86 的 Docker 镜像比如在 RISC-V 设备上跑 ARM 应用比如在国产操作系统上跑 Windows 行业软件。核心思路是一样的指令翻译 API 实现 图形转译三层解耦各司其职。如果你在做国产化适配我建议重点关注 Wine 的 API 覆盖率。很多行业软件用的 API 很偏门Wine 没实现这时候要么给 Wine 提 patch要么用“API hook”的方式自己实现一个 shim 层。这个工作量不小但比重新开发整个软件要小得多。如果你在做游戏移植DXMT 的着色器编译优化是个值得投入的方向。现在每次启动都要编译着色器体验很差。如果能做到“一次编译到处运行”或者用云编译的方式提前生成缓存体验会好很多。最后分享一个小技巧调试 Wine 程序时用winedbg比用gdb方便。winedbg能识别 Windows 的异常和调用栈定位问题更快。启动方式是winedbg game.exe然后在调试器里用bt看调用栈用info registers看寄存器状态。这个工具在排查崩溃问题时特别有用。
返回列表