第 66 章:在 Android 上运行 Windows 游戏

现代 ARM64 安卓手机的 GPU 性能足以运行桌面 PC 游戏,但这些游戏本身是 x86‑64 Windows 二进制程序,依赖 Win32 API、Wine 底层的 glibc 风格 Linux 内核 ABI 以及桌面端图形驱动。在未取得 Root 权限的原生安卓设备中,上述组件一概不存在。GameNative 以及它所基于的 Winlator 运行时这类项目,完全在用户态下弥合了这一鸿沟:无需 Root、无需内核补丁、无需自定义 ROM。它们搭建起一套多层转换链路,每一层解决一部分适配差异,整套体系构建在本书其余章节剖析过的 AOSP 底层能力之上。

本章以 GameNative 作为实践案例,自上而下完整讲解整套技术栈:安卓应用、APK 内部打包的 glibc 根文件系统、bionic/glibc 的边界与跨边界重定向机制、x86‑64 到 AArch64 的转换器(FEX 与 Box64)、作为 Windows API 层的 Wine、直达 Adreno 物理 GPU 的 Direct3D 转 Vulkan 图形链路,以及最终对接 AAudio 的音频通路。每一层都是独立的技术模块,文中会分别介绍其架构、设计初衷,以及它们与前文介绍的安卓内部组件的对接位置。

关于源码引用说明:安卓相关代码引用 AOSP 代码树,附带文件路径与行号,格式与本书其他章节保持一致。转换栈相关项目(GameNative、Winlator、Wine、FEX、Box64)属于外部仓库,代码迭代速度快,因此仅给出仓库内相对文件路径,不再标注行号,否则印刷完成即已失效。

66.1 问题概述与分层架构

66.1.1 两类相互独立的转换工作

在 ARM 手机上运行 Windows 游戏,并不是单一问题,而是两个容易被混淆、彼此独立的问题:

  1. 指令集转换:游戏输出 x86‑64 机器指令流,ARM64 CPU 无法直接执行。必须有组件将 x86‑64 转换成 AArch64,可采用提前编译或者即时编译的方式。这是 FEX 或 Box64 的工作,属于纯粹的 CPU 模拟;它完全不感知 Windows、文件、套接字,只处理操作码、寄存器与标志位。
  2. API 与 ABI 转换 :游戏调用 Windows API,例如CreateFileWNtUserCreateWindowExD3D11CreateDevice。这些函数在 Linux 中并不存在,需要组件基于 POSIX/Linux 内核实现一套 Windows API。这是 Wine 的工作,它不是模拟器:内部完全没有指令转换逻辑,仅用原生代码重新实现 Windows 的行为。

将这两项工作解耦,是整套架构最重要的设计思想。整套系统的性能,取决于尽可能缩小模拟执行范围,理想情况下仅模拟游戏自身代码,而 Wine、图形驱动、音频服务全部以原生 ARM64 代码运行。后文会反复提及该设计思路;最新的 ARM64EC 方案(见 66.5.8),其设计目标正是把 "被模拟执行" 的范围收缩到仅游戏本体。

66.1.2 整套技术栈总览

从上层 Windows 游戏到底层安卓内核,各层自上而下排列如下:

示意图:安卓平台上 Windows 游戏转换栈

整套栈可以划分为三大部分:

  1. 虚拟机环境层:普通安卓应用私有存储空间中运行的 Linux x86‑64 环境。
  2. 应用层:原生 ARM64 安卓代码,包含接收游戏画面的内置 X 服务器、处理音频的 PulseAudio 服务、下载游戏的商店客户端。
  3. 平台层:标准 AOSP 组件,即第 13 章 Vulkan 加载器与 ANativeWindow、第 15 章 AAudio、第 24 章 SurfaceFlinger 合成器。

关键点:Direct3D 与音频链路会提前跳出模拟环境。游戏 D3D 调用交给 DXVK;在高性能配置下 DXVK 以原生 ARM64 运行并输出 Vulkan 指令,重量级图形转换本身不经过模拟。音频路径同理。只有游戏指向 Wine、Wine 再进入模拟器的实线箭头部分,才是需要 JIT 翻译的 x86‑64 代码。

66.1.3 无需 Root,无需内核改动

虚拟机环境全部以普通已安装应用的权限运行。不使用 su、不加载内核模块 insmod、不修改 SELinux 策略、不改动系统镜像。该约束使得这类应用可以在锁 Bootloader 的设备上直接从应用商店安装,同时也带来本章大部分精巧的工程实现。

桌面端 Wine 可以 chroot 切换根文件系统、挂载文件系统、直接 dlopen 系统 glibc 版本的 GPU 驱动;安卓应用无法完成以上任意操作。每一项缺失的能力,都由用户态替代组件实现:

桌面端能力 无 Root 安卓下的限制 用户态替代方案
chroot 进入根文件系统 需要 CAP_SYS_ADMIN 权限 PRoot(基于 ptrace 路径改写)
挂载根文件系统镜像 需要 Root 权限 解压至应用私有存储
System V 共享内存 内核做了限制 基于 ashmem/memfd 的兼容层
dlopen 加载 glibc 版本 GPU 驱动 驱动是 bionic 编译的库 与原生渲染器 IPC 通信,或者使用 Thunk 跳板
运行系统 PulseAudio 服务 无法访问系统守护进程 应用内打包 PulseAudio 服务端

66.1.4 基于 AOSP 视角看无 Root 应用的约束

三项 AOSP 机制决定这套技术栈能力边界,全部在前面章节介绍过:

  1. 链接器命名空间(第 7 章) :应用原生库加载在隔离链接命名空间,无法随意访问系统库。控制该行为的android_dlopen_ext标志定义于bionic/libc/include/android/dlext.hANDROID_DLEXT_USE_NAMESPACE允许调用者指定库加载到哪一个命名空间;ANDROID_DLEXT_USE_LIBRARY_FD支持直接通过文件描述符加载库,例如存放在 APK 内未压缩的.so文件。Vulkan 加载器打开设备驱动时就使用命名空间标志,详见 66.7.4。
  2. W^X 写执行互斥保护(第 7 章) :JIT 需要先写入机器码,随后执行。自 API 26 起动态链接器拒绝加载同时具备可写、可执行属性的 ELF 段;bionic/linker/linker_phdr.cpp:1057输出 "同时包含可写和可执行加载段" 诊断信息,bionic/linker/linker_phdr.cpp:1061记录 W+E 警告。因此 FEX、Box64 这类转换器不能将页同时映射为PROT_WRITE | PROT_EXEC;必须先映射为可写,填充机器码,再调用mprotect切换为可执行,遵守平台强制的写执行互斥规则。
  3. 应用数据目录执行限制 :新版安卓越来越严格限制从应用可写数据目录直接执行二进制。转换栈不能直接把应用 data 目录路径传给内核调用execve,必须通过自研加载器(PRoot 或者重定向库)运行虚拟机可执行程序。GameNative 闭源库libredirect.so就承担该工作,见 66.4.4。

66.2 GameNative:集成化应用

GameNative 是安卓端本地运行 Windows PC 游戏的应用,内置 Steam、Epic 游戏商店、GOG、Amazon Games 商店对接。它是整套完整栈最具代表性公开实现,作为本章讲解载体。项目采用 GPL‑3.0 许可证,包名app.gamenative

66.2.1 代码来源:Pluvia 与 Winlator 的组合产物

不要把 GameNative 简单理解为某个项目的 Fork,它是两套上游代码的融合产物:

  • 应用与 Steam 客户端层 :源自 Pluvia,轻量级非官方安卓 Steam 客户端。源码特征明显:Application 子类为PluviaApp.kt,Room 数据库PluviaDatabase.kt,Steam 连接使用 JavaSteam 库。Pluvia 实现账号登录、游戏库界面、SteamPipe 仓库下载器。
  • 运行时与兼容层 :源自 Winlator。完整com.winlator.* Java 代码树放在app/src/main/java/com/winlator/,子包包含 container、core、xserver、xenvironment、winhandler、box86_64、fexcore、inputcontrols、renderer。C++ 原生代码app/src/main/cpp/winlator/同样取自 Winlator。

从代码溯源角度,GameNative = Pluvia(商店前端) + Winlator(Wine 与模拟运行时),外层用 GPL‑3.0 许可的 Kotlin 代码整合。很多文章简单描述它是 "Winlator 的一个分支",忽略双源码来源;本章后续内容主要聚焦 Winlator 这部分,也就是真正负责运行游戏的模块。

66.2.2 应用架构

面向安卓的部分使用 Kotlin,Jetpack Compose UI,Dagger Hilt 做依赖注入。对本章最重要的三个组件:

  1. SteamService前台 Service:维护 Steam 会话;Epic、GOG、Amazon 各平台拥有同类服务。
  2. XServerScreen:Compose 全屏画布,游戏渲染输出目标,由XServerViewModel驱动。在这里启动应用内置 X 服务器、音频服务、图形渲染器。
  3. 引入的 Winlator 运行时:实际负责容器、根文件系统、Wine、模拟器逻辑,由com.winlator.xenvironment包调度。

66.2.3 容器模型

一个容器代表一套隔离的 Windows 游戏环境:包含 Wine 前缀以及配套配置(图形驱动、模拟器、音频驱动、DLL 覆盖、环境变量)。com.winlator.container.ContainerContainerManager定义、管理容器。默认配置可以直观看到 GameNative 栈构成。

取自app/src/main/java/com/winlator/container/Container.java

java 复制代码
public static final String DEFAULT_EMULATOR        = "FEXCore";
public static final String DEFAULT_AUDIO_DRIVER    = "pulseaudio";
public static final String DEFAULT_DXWRAPPER       = "dxvk";
// graphics driver default resolves to "vortek"

固定组件版本定义在app/src/main/java/com/winlator/core/DefaultVersion.java:Box64、Box86、FEXCore 编译版本号、Turnip(Mesa 的 Adreno Vulkan 驱动)、DXVK、VKD3D 等。Wine 默认版本写在WineInfo.java。阅读这两份文件就可以完整掌握默认流水线,本章末尾实操练习就包含阅读这两个文件。

66.2.4 默认技术栈

容器默认配置组合之后,新建 GameNative 容器运行组件如下:

  1. Wine / Proton 采用 ARM64EC 配置作为 Windows API 层(66.6),FEXCore 翻译游戏 x86‑64 代码,WoW64 辅助组件处理 32 位代码(66.5.8)。
  2. DXVK 负责 Direct3D 9/10/11 转 Vulkan;VKD3D‑Proton 处理 Direct3D12(66.7.2)。
  3. Vortek 作为通向设备 Adreno 驱动的 Vulkan 通路(66.7.3);可选用 Turnip(Mesa)作为备选。
  4. PulseAudio 作为音频服务(66.8)。

同时打包经典 glibc 启动模式的 Box64,用于不支持 ARM64EC 的 Wine 构建版本;该路径由另一套启动组件选择(66.5.6)。

66.2.5 打包方式:组件如何下发到设备

GameNative 在单个应用内携带大量预编译原生软件,使用三种分发方式:

  1. 安卓进程原生库 :放置jniLibs/arm64‑v8a/,由包管理器解压到应用nativeLibraryDir。这些库加载进安卓 bionic 进程:PulseAudio 库libpulse.so及相关组件、图形渲染器libvortekrenderer.solibvirglrenderer.so、内置 X 服务器libwinlator.so,以及闭源libsteambootstrap.so
  2. 虚拟机组件 :以压缩 tar 包存放在 assets 目录(Zstandard .tzst、xz .txz),运行时通过com.winlator.core.TarCompressorUtils解压到根文件系统。Wine、FEX、Box64、DXVK、VKD3D、Vulkan 驱动、Windows DLL 全部以此方式部署。解压完成后使用内置 patchelf 工具修补 ELF 解释器与 RPATH 库搜索路径。
  3. 大型或可选资源按需下载 :Ubuntu 根文件系统作为独立安卓动态特性模块(ubuntufs/,dist:on‑demand)下发;图形驱动、额外 DXVK 版本、Proton 容器模板,根据assets/*_download.json清单从 GameNative 下载服务器拉取。保证基础 APK 体积可控,数 GB 的 Linux 用户态环境只在首次运行游戏时下载。

66.2.6 完整启动流程

Diagram: launching a game in GameNative

XEnvironment维护有序组件列表,每个组件对应com.winlator.xenvironment.components下的类,分别启动子系统:X 服务器、PulseAudioComponent、图形渲染器(VortekRendererComponent 或者 VirGL 渲染器)、System V 共享内存组件,最后程序启动器生成运行在模拟器下的 Wine。启动器分为两种:默认 ARM64EC/FEX 路径BionicProgramLauncherComponent;传统 glibc/Box64 路径GuestProgramLauncherComponent

66.2.7 许可证与 GPL 聚合问题

GameNative 和引入的 Winlator 代码均为 GPL‑3.0。大部分捆绑运行时拥有上游许可证,多数为宽松协议或者 LGPL:Wine、PulseAudio 为 LGPL‑2.1;Mesa(Turnip、Zink)主要 MIT;DXVK zlib 协议;Box64、FEX MIT/BSD 系列。项目THIRD_PARTY_NOTICES书面提供 Copyleft 组件源码获取途径。

两个刻意闭源组件,也是法律层面值得讨论的点:

  1. libredirect.so(存放在 assets/redirect.tzst):通过LD_PRELOAD只加载进 Wine/Proton 子进程,绝不进入安卓应用主进程地址空间。改写旧包标识符,适配新版安卓 data 目录执行限制。
  2. libsteambootstrap.so JNI 垫片,实现进程内 Steam 客户端,用于成就、云存档、游戏内悬浮窗。

维护方观点:两者作为独立程序 / 子进程,仅通过操作系统接口通信;依据 GPL‑3.0 第 5 条,属于软件聚合,不属于 GPL 应用的衍生作品。该说法仅为项目立场,尚未经过法律裁定。GPL‑3.0 应用配套分发闭源 LD_PRELOAD 库与 JNI 垫片是否合规,仍是尚未定论的 GPL 法律问题。去掉这两个库,应用依旧可以编译运行,只是兼容性下降;这也是维护方论证组件相互独立的依据。

66.3 rootfs:应用内部的 glibc Linux 用户态环境

66.3.1 rootfs 内部包含什么

Wine、模拟器虚拟机库、游戏,都期待标准布局 Linux 文件系统:/usr/lib/bin、ld‑linux 动态链接器,最重要的是 glibc C 库。安卓应用没有这些,只提供 bionic 库与应用私有 data 目录。因此整套栈打包一套小型 Linux 发行版,Winlator 内部称之为 imagefs,解压至应用私有存储。

rootfs 基于 Ubuntu(原版 Winlator 使用 20.04 Focal Fossa)。内部是 glibc 用户态环境;Wine 安装路径/opt;每个容器独立 Wine 前缀位于/home/xuser/.wine。路径常量定义在com.winlator.xenvironment.ImageFs

66.3.2 打包与解压

用户态环境以压缩包分发,首次使用解压,不会执行挂载操作(应用没有挂载权限)。核心代码ImageFsInstaller将 Wine tar 包解压到<rootfs>/opt/<version>TarCompressorUtils处理.tzst.txz两种压缩格式。解压完成,ELF 二进制用 patchelf 修正解释器与库搜索路径,指向 rootfs 内部,而不是不存在的系统路径。

rootfs 体积巨大,因此做成按需加载动态特性模块(66.2.5);完整 Ubuntu+Wine 用户态可达数百 MB 到数 GB,不适合放在基础 APK。

66.3.3 进入 rootfs:PRoot

桌面环境用 chroot 切换根目录;无 Root 安卓无法调用。早期 Winlator 解决方案是 PRoot:完全基于 Linux ptrace 系统调用,在用户态重新实现 chroot 与 bind‑mount 绑定挂载。

PRoot 将虚拟机程序作为被跟踪子进程启动。每当子进程发起带路径参数系统调用,PRoot 在内核边界 ptrace 拦截,修改被跟踪进程内存内的路径字符串;例如把/usr/lib/...改写为<app data>/imagefs/usr/lib/...,之后放行系统调用。虚拟机进程以为自己运行在根目录/,实际上所有路径被实时重映射。跨execve创建子进程时同样生效,保证子进程继续被隔离。--bind=/dev--bind=/proc这类绑定挂载,也是通过改写路径前缀模拟。

无 Root 运行的性能代价:每一个携带路径的系统调用,都要在 PRoot 进程经历 ptrace 暂停‑继续往返。该开销是新 bionic 方案(66.3.6)希望彻底抛弃 PRoot 的主要原因。

66.3.4 glibc 对比 bionic:最核心的不兼容矛盾

rootfs 给 Wine 提供它所需要的 glibc 环境。但引出整套栈最核心难题:进程内存在 glibc,而所有安卓原生库(包含 GPU 驱动)全部基于 bionic 编译。不能直接把 bionic 的 so dlopen 进 glibc 进程。两个 C 库在线程局部存储 TLS 布局、动态链接器内部结构、errno、pthread_t、栈保护 canary 定义互不兼容。把一个 libc 编译的共享库加载进另一个 libc 进程,会破坏运行状态直接崩溃。

"bionic 重定向" 技术就是为了解决该问题,详见 66.4。这里简单说明两类解决思路:

  1. 保留 glibc 环境,不直接链接,改用进程间通信访问安卓服务;PulseAudio 服务、Vortek 渲染器、SysV‑SHM 服务全部是通过 Socket 访问的独立端点。
  2. 放弃 glibc,直接把 Wine 编译运行在 bionic 之上(新一代分支方案)。

66.3.5 基于 ashmem 实现 System V 共享内存

System V 共享内存是 bionic 重定向非常典型实例。X11、DXVK 大量使用shmget/shmat在进程间传递大块缓冲区。安卓内核限制 System V SHM 接口。Winlator 的 glibc rootfs 携带打过补丁的 glibc,重新实现shmget/shmat/shmdt,底层依托安卓匿名共享内存,由安卓侧com.winlator.sysvshm服务代理。

安卓侧原语为ASharedMemory_createframeworks/native/include/android/sharedmem.h:78),NDK 接口返回引用计数匿名共享内存文件描述符。底层 API 30 以上使用memfd_createbionic/libc/include/sys/mman.h:183);旧设备回退 ashmem 驱动。重定向逻辑:虚拟机内部 System V shmget调用转换为主机端ASharedMemory风格 fd,通过 Socket 传回。虚拟机继续使用它原本依赖的 System V API;主机使用内核真正允许的安卓原语完成功能。

66.3.6 bionic 版 Wine 替代方案

新版 Winlator 分支,同时也是 GameNative 默认路径,选择第二条路线:把 Wine 编译给 bionic,彻底舍弃 PRoot。Wine 运行在 bionic 之后,和安卓 GPU 驱动不存在 libc 不匹配,也消除每次系统调用 ptrace 开销。代价是原本 Wine 依赖 glibc 的全部能力,都必须在 bionic 环境补齐;该方案高度依赖 ARM64EC 版本 Wine 构建(66.5.8)以及一套重定向库,用来替代 PRoot 完成路径、行为修正,下一节详细展开。

66.4 Bionic 重定向与 C 库边界

"Bionic 重定向" 是统称,代表让基于 glibc 的 Linux 软件,和基于 bionic 的安卓系统协同工作的一系列技术。该概念背后包含多类不同问题,对应不同解决方案。

66.4.1 为什么两个 C 库不能共存于同一个进程

再次明确核心约束,后续全部方案由此推导:单个 Linux 进程,只能拥有一套动态链接器,一套 C 库,由它提供链接器内部状态、TLS 块、malloc、系统调用封装。glibc 与 bionic 是两套完全独立实现,内部布局互不兼容。glibc 进程线程使用 glibc pthread 结构体、glibc TLS 模型;bionic 编译的 so 会按照 bionic 偏移读取线程控制块,读到的只会是无效垃圾。没有任何开关可以让两者共存。

跨越两者边界,不能依靠动态链接。整套栈只有三种可行手段:进程间通信 IPC、ABI Thunk 跳板、整个进程统一选用其中一套 C 库。

66.4.2 链接器命名空间与 dlext

即便栈运行原生 bionic 代码,依然受第 7 章链接器命名空间隔离约束。应用原生库加载在受限命名空间,看不到绝大多数系统库。驱动加载逻辑调用android_dlopen_ext搭配ANDROID_DLEXT_USE_NAMESPACE,指定目标命名空间。像 adrenotools 这类自定义驱动加载工具,用来侧载比设备自带更新的 Turnip 驱动,就依赖这套机制,将 so 加载到可以访问 GPU 内核接口的命名空间。配套标志ANDROID_DLEXT_USE_LIBRARY_FD支持直接从文件描述符加载库。

66.4.3 W^X 与 JIT 即时编译器

转换器属于 JIT,必须遵守平台写执行互斥 W^X 规则(66.1.4)。安卓上所有转换器通用流程:将代码缓冲区映射为PROT_READ | PROT_WRITE,写入 AArch64 指令,清空对应地址指令缓存;调用mprotect修改页面属性为PROT_READ | PROT_EXEC,再跳转到生成代码执行。页面绝不会同时具备可写、可执行,规避链接器 W+E 段拒绝逻辑(bionic/linker/linker_phdr.cpp:1057)。

对于进一步限制从应用 data 目录内存执行的安卓版本,转换器不能映射 data 目录文件,而是自己分配匿名内存存放 JIT 代码页。

66.4.4 路径重定向:PRoot 对比 libredirect

PRoot(66.3.3)是路径重定向一种:在内核 ABI 层通过 ptrace 改写路径。通用性强,但性能差。

Bionic 路径采用预加载库替代:GameNative 的libredirect.so,通过LD_PRELOAD注入 Wine 子进程。不使用 ptrace 捕获每一次系统调用,而是 Hook 拦截所有接收路径参数的 libc 函数,在进程内部原地修改参数,没有跨进程跟踪往返开销。同时适配新版安卓禁止从 data 目录执行程序的限制,模拟无 Root 应用无法获取的内核行为。

取舍:用通用性换取速度。预加载库只能捕获被 Hook 拦截的库函数调用;PRoot 在内核边界可以捕获全部行为。但是预加载方案消除 PRoot 最大性能开销:每次系统调用 ptrace 往返。

66.4.5 IPC 作为逃逸方案

最稳妥的跨边界手段,是干脆不在同一个进程内跨越 libc 边界。栈中有三类服务运行在安卓 bionic 侧独立端点,虚拟机端通过 Unix 域 Socket 访问:

  1. PulseAudio 服务端,虚拟机 Wine 音频后端作为普通 PulseAudio 客户端连接(66.8)。
  2. Vortek Vulkan 渲染器:虚拟机只包含精简 Vulkan ICD,把命令序列化,通过 Socket 发给原生渲染服务(66.7.3)。
  3. System V SHM 服务(66.3.5)。

边界依靠 Socket,两端 C 库、指令集、内存模型完全可以不一样;唯一约定是线上通信协议。Binder 跨不同运行时进程也是同样原理(第 9 章)。代价是序列化拷贝;但是彻底绕开 libc 冲突。所以音频、命令提交这类对性能有要求但非最热路径采用 IPC;真正最热 CPU 翻译路径不使用 IPC。

66.5 FEX 与 Box64:x86‑64 转 AArch64 指令翻译

这一层完成 66.1.1 描述的指令集模拟。两大实现,GameNative 两者全部内置:默认选用 FEX(FEXCore 引擎),同时携带 Box64。两者目标一致,设计理念截然相反;理解二者差异,可以看懂用户遇到的兼容性、性能取舍。

本章内容补充第 19 章:AOSP 自带进程内二进制转换器 Native Bridge,Berberis 是 AOSP 组件,用于在 x86‑64 设备运行 riscv64 应用代码(Android17 把 CPU 仿真核心收拢至frameworks/libs/binary_translation/cpu_emulation/);历史闭源 Houdini 用于 Intel x86 设备运行 ARM 应用。注意:它们运行的对象是安卓 APK 原生代码,转换方向与本章相反,整套 Windows 游戏栈完全不使用它们。FEX、Box64 是同样的动态重编译 dynarec 思想,但面向完整 Linux/Windows x86‑64 程序;运行在普通应用进程,不是 Native Bridge 插件;做 x86‑64 到 AArch64 转换,AOSP 没有内置对应转换器。

66.5.1 动态重编译器 dynarec 工作原理

动态重编译器 dynarec,也就是针对外部机器码的 JIT。读取一块 x86‑64 指令,一次性翻译成 AArch64,缓存结果;后续虚拟机执行到该地址直接跳转缓存翻译结果。翻译代码把虚拟机寄存器映射为主机寄存器,虚拟机内存直接复用主机内存;只在基本块边界回到分发器,决定下一段执行。dynarec 通常比解释器快 5‑10 倍,指令解码开销仅在翻译阶段支付一次,而不是每次执行。

x86 转 ARM 有三处典型难点,也是 FEX、Box64 重点工程投入地方:

  1. 标志位 Flags:几乎每一条 x86 算术指令都会修改 EFLAGS 寄存器。ARM 每条运算后都完整计算标志位开销巨大;优秀 dynarec 会延迟、消除标志位计算,只有代码真正读取标志时才生成对应计算逻辑。
  2. 向量指令:SSE、AVX、x87 浮点,映射到 ARM NEON 向量寄存器(支持 SVE 则使用 SVE),需要精细处理 NaN 与舍入模式。
  3. 内存模型:x86 是强内存模型 TSO 全存储序;ARM 内存模型更弱。保证多线程正确性实现复杂,开销很高,单独小节讲解(66.5.3)。

66.5.2 FEXCore 流水线

FEX 是运行在 AArch64 主机的用户态 x86、x86‑64 模拟器。仿真引擎为库 FEXCore;翻译流水线清晰分为四阶段:

Diagram: the FEXCore translation pipeline

对照 FEX 源码树各阶段:

  1. 前端解码器:读取原始字节流,切分指令边界,处理 x86 传统前缀、REX、AVX 使用的 VEX/EVEX 编码。
  2. OpcodeDispatcherFEXCore/Source/Interface/Core/OpcodeDispatcher.cpp,向量、x87 逻辑拆分到OpcodeDispatcher/Vector.cppOpcodeDispatcher/X87.cpp),把每一条解码指令转换为 FEX 自定义 SSA 形式中间表示 IR;IR 操作码定义文件FEXCore/Source/Interface/IR/IR.json
  3. PassManagerFEXCore/Source/Interface/IR/PassManager.cpp)对 IR 执行优化。性能相关重要优化:寄存器分配(IR 值映射 ARM 物理寄存器)、冗余 EFLAGS 标志消除、x87 栈优化。
  4. JIT 后端FEXCore/Source/Interface/Core/JIT/JIT.cpp)读取优化 IR,输出原生 AArch64 指令写入代码缓存。FEX 使用自研 ARM64 指令发射器,不使用现成汇编器,保障生成代码快速紧凑。

FEX 支持 MMX、SSE 至 SSE4、x87、AVX/AVX2。翻译粒度大于单基本块,多块合并翻译;缓存翻译块使用查询缓存完成块间跳转。自修改代码检测:当虚拟机向存有翻译代码页写入数据,失效对应缓存块。

66.5.3 TSO 内存模型难题

仿真层最重要性能细节。x86 保证 TSO 全存储序:一个 CPU 核的写内存,按照程序执行顺序对其他核可见。ARM 不保证;写操作可以重排序,必须显式插入内存屏障或者 acquire/release 指令。直接把 x86 多线程游戏内存操作翻译成普通 ARM 加载存储,会产生竞态、程序崩溃。

FEX 默认保证正确性,软件模拟 TSO:使用 ARM FEAT_LRCPC 扩展 acquire/release 读写指令,FEAT_LSE2 处理 x86 允许而 ARM 原生不支持的非对齐原子。软件模拟 TSO 代价高昂;受影响内存访问场景,性能下降可达 10 倍,向量密集游戏代码感受明显。

性能出口:硬件 TSO。部分 ARM 核心可以开启硬件全存储序(Apple Silicon,Rosetta2 依靠该特性获得高性能)。主机 CPU 暴露该能力时,FEX 优先启用,关闭昂贵软件模拟,性能大幅提升。FEX 也提供调参开关,可以关闭向量访问、栈内存 TSO 模拟(栈属于线程私有内存,安全),牺牲部分稳定性换取速度。

手机端现实意义:骁龙 CPU 内存排序硬件能力,会直接影响仿真性能,不完全取决于 CPU 主频。

66.5.4 FEX Thunk 跳板:调用原生 ARM64 图形库

倘若 FEX 把游戏进程全部指令(包括 GPU 驱动)全部翻译,性能完全不可用。Thunk 跳板(库转发)解决该痛点。Thunk 允许被模拟 x86 虚拟机代码直接调用原生 AArch64 主机库;GPU 驱动这类重负载库直接运行原生,不逐条翻译。

设计分为两端:

  1. 虚拟机侧 :FEX 提供小型 x86 桩库,对外暴露标准虚拟机 ABI(例如虚拟机侧libvulkan.so),内部不实现驱动逻辑,只是转发每一次函数调用跨过边界。
  2. 主机侧:原生 ARM64 Thunk 库接收转发调用,调用真实主机库。

FEX 源码内置图形库两端实现:Vulkan Thunk ThunkLibs/libvulkan/Guest.cppThunkLibs/libvulkan/Host.cpp;OpenGL ThunkLibs/libGL/

Diagram: a thunked Vulkan call crossing the emulation boundary

难点在参数编组 Marshal:32 位场景指针大小差异、结构体内存布局、调用约定不一致;回调(主机库回调用到虚拟机的函数指针)需要双向跳板 Trampoline。FEX Thunk 处理双向函数指针,边界转换数据结构布局;配置文件ThunksDB.json映射虚拟机库对应主机实现 Overlay。

Thunk 跳板是 FEX 图形可以接近原生性能的最大原因:只仿真游戏绘制调用准备逻辑;驱动工作全部原生执行。

66.5.5 FEX 对 x86‑64 rootfs 依赖

FEX 作为独立 Linux 模拟器完整仿真 x86‑64 进程,需要 x86‑64 根文件系统,提供虚拟机 glibc 与基础库;桌面 Linux FEXRootFSFetcher专门下载 SquashFS/EroFS 镜像。这是全仿真模式,也是 FEX 和 Box64 主要区别,Box64 不仿真完整虚拟机用户态,而是包装主机库。

但 GameNative 默认 ARM64EC 配置下,FEX 不再充当完整 Linux 模拟器。它作为纯 CPU 模块接入 Wine(66.5.8);Windows 系统调用由 Wine 处理,不再依赖虚拟机 glibc。因此不再需要独立 x86 rootfs。FEX 需要 rootfs 是 "全仿真路径" 特性,不是 ARM64EC 默认路径;舍弃 rootfs 也是 ARM64EC 实实在在收益。

66.5.6 Box64:包装原生主机库

ptitSeb 开发的 Box64,同样是 dynarec 动态重编译器,但系统库处理思路不一样。不依靠 rootfs 仿真 x86‑64 glibc、GL、Vulkan;Box64 检测虚拟机即将调用知名系统库,替换为主机 ARM64 原生实现,每一次调用做调用约定转换包装。手写包装代码放在 Box64 源码src/wrapped/目录;例如 Vulkan src/wrapped/wrappedvulkan.c,C 库src/wrapped/wrappedlibc.c

带来的结果:对于已经实现包装的库,Box64 不需要完整 x86‑64 rootfs,直接复用主机库。体积比 FEX 轻量;这是早期 Winlator 默认方案;启动链路就是 proot → box64 → wine。GameNative 的GuestProgramLauncherComponent就组装这套box64 <guest exe>命令用于 glibc 路径。代价是正确性取决于包装实现质量;包装行为与虚拟机预期存在偏差时,会出现全仿真模式不会遇到的故障。

32 位处理逻辑对称:Box64 处理 x86‑64;配套 Box86 处理 32 位 x86。主机没有 32 位 ARM 环境时,Box64 实验模式 BOX32,在 64 位环境内仿真 32 位环境。Box64 文档docs/WINE.md整理 Wine 各变体矩阵(x86、x86‑64、x86‑64 WoW64、ARM64 WoW64),以及对应需要 Box 组合,是理解模拟器与 Wine 配合的一手参考资料。

66.5.7 FEX 对比 Box64

Diagram: the two emulation philosophies
维度 FEX Box64
系统库来源 来自 x86‑64 rootfs 仿真库 包装复用主机 ARM64 原生库
磁盘占用 更大(完整 rootfs) 更轻,复用主机库
原生库逃逸方式 Thunk 跳板(GL、Vulkan) Wrapper 包装(libc、GL、Vulkan 等更多库)
32 位代码处理 共用核心;依靠 WoW64/ARM64EC 独立 Box86,或 BOX32 模式
精度策略 完整 IR 重编译,严格 TSO 内存模型 实用主义,依赖 Wrapper 实现完整度
GameNative 中角色 默认(FEXCore) 备选内置组件

两者没有绝对优劣。FEX 仿真精度高,适合重度 MOD、小众游戏;Box64 体积小、Wrapper 成熟,长期作为 Winlator 默认。GameNative 两者全部内置,每个容器可以切换选择。

66.5.8 ARM64EC 与 WoW64:只仿真游戏代码

最重要的架构演进,而非小增量优化:不再把 Wine 自身放入模拟器。全仿真模式 JIT 同时翻译游戏、整套 Wine 以及下层 x86 Linux 库;模拟器处在每一条指令热路径上。ARM64EC 模式把 Wine 编译为原生 ARM64;只仿真游戏的 x86‑64 代码

ARM64EC(Emulation‑Compatible,仿真兼容)是微软定义 ABI,Wine 做了重实现。原生 ARM64 代码布局可以和被仿真 x86‑64 代码互相调用。Wine 系统 DLL 编译为该 ABI;被仿真游戏调用 Windows API 时,执行流切换进入原生 ARM64 Wine,不再继续仿真。模拟器降级为 CPU 核心,在 ABI 边界以可加载 DLL 形式接入 Wine。该配置中模拟器作为 Wine 加载 Windows DLL:FEX 对应 64 位libarm64ecfex.dll;32 位 WoW64 路径使用libwow64fex.dll(FEX)或者wowbox64.dll(Box64)。GameNative BionicProgramLauncherComponent根据 Wine 构建是否 ARM64EC,选择对应组件。

示意图 ------ 通过 ARM64EC 缩小仿真执行区域

收益巨大:Windows API 业务、图形转换、音频链路全部原生 ARM64 运行;仅仅游戏指令流被翻译。这是 GameNative 默认架构,也是手机上整套栈可以实际可用的根本原因。

66.6 Wine:实现 Windows API

Wine 负责欺骗游戏,让游戏认为运行在 Windows。模拟器处理机器指令;Wine 处理语义:游戏每一次 Windows API 调用,由 Wine 基于 Linux 内核重新实现该 API。

66.6.1 Wine 不是模拟器

名称递归缩写:Wine Is Not an Emulator。在 ARM 安卓平台,该区分不是文字游戏,而是架构核心。Wine 内部完全没有指令翻译。读取 Windows PE 二进制,加载进 Linux 进程,使用原生代码对接 Linux 内核,响应 Windows API、NT 系统调用。

在 x86‑64 Linux,Wine 原生直接运行;在 ARM 安卓,Wine 要么被模拟器运行(全仿真模式),要么编译为 ARM64EC 原生。无论哪一种,x86 到 ARM 指令翻译永远交给 FEX 或者 Box64。两层职责严格正交,对应 66.1.1 描述。

66.6.2 PE/Unix 分离架构

现代 Wine 有清晰边界:Windows PE 代码,Unix 后端代码。Wine 内置 DLL(ntdll、kernel32、kernelbase、user32、gdi32)编译为标准 PE 文件;站在游戏视角,调用的就是普通 Windows DLL。

一部分必须访问主机操作系统的 DLL,拆分为 PE 部分 + Unix 部分。典型例子 ntdll:

  • PE 侧dlls/ntdll/编译输出ntdll.dll
  • Unix 侧dlls/ntdll/unix/编译输出 ELF 库ntdll.so,程序启动 dlopen 加载。关键源文件dlls/ntdll/unix/loader.cdlls/ntdll/unix/virtual.cdlls/ntdll/unix/signal_x86_64.c
  • PE/Unix 两部分契约定义在dlls/ntdll/unixlib.h

进程启动流程:普通 Linux 进程;小型加载器 dlopen ntdll.so,构建 Windows 进程控制结构 PEB、TEB,映射 PE 版 ntdll.dll 和主可执行文件,连接 wineserver,跳转到 PE 侧初始化;之后执行流行为如同 Windows。

示意图:Wine PE/Unix 两部分与系统调用边界

两套跨边界机制:

  1. NT 系统调用(NtCreateFileNtUserCreateWindowEx)经过__wine_syscall_dispatcherdlls/ntdll/unix/signal_x86_64.c);保存 CPU 上下文,切换 Unix 栈,索引系统调用表,调用 Unix 侧实现。
  2. 需要绕过 NT 调用表的 DLL,使用WINE_UNIX_CALL

Wine 把 Windows API 调用经过显式 NT 风格系统调用边界,正是这一点,才可以实现 66.5.8 ARM64EC、WoW64 切换;边界本身已经存在,只需要做 Hook 拦截。

66.6.3 wineserver

wineserver 是一个独立守护进程,它为 Wine 提供一套近似于 Windows 内核在 Windows 系统上所承担的服务。所有共用同一个 Wine 运行前缀(prefix)的 Wine 进程,都会通过 wineserver 共享原本在 NT 系统中存放于内核空间的资源:句柄与对象命名空间、同步对象(事件、互斥锁、信号量)、进程与线程、窗口管理状态以及注册表数据。

客户端通过 AF_UNIX 套接字与它通信,经由 wine_server_call 完成请求封包传递;服务端的请求处理逻辑位于 server/request.c。套接字存放于每个独立 prefix 对应的目录,该目录由 prefix 所在设备与 inode 值作为唯一标识,因此每一套运行环境都会拥有独立的 wineserver 实例。一个关键实现细节:套接字借助标准 SCM_RIGHTS 附属数据机制在进程间传递文件描述符,因此 Windows 句柄可以绑定一份由服务端转交的真实 Linux 文件描述符。

该架构带来的性能短板在手机设备上尤为突出。游戏会频繁执行线程同步操作,传统模式下每次同步都需要向 wineserver 发起一次请求 ,导致每一次同步操作都产生一轮套接字往返开销。Wine 逐步将同步逻辑从服务端下沉剥离:esync(基于 eventfd)、fsync(基于 futex),以及最新的 ntsync------ 这是一个 Linux 内核驱动,提供 /dev/ntsync 设备节点,直接在内核中实现 NT 原生同步原语。高速同步通路是否可用取决于宿主内核:Android 内核可能并未启用 /dev/ntsync,此时整个调用栈会降级回退到 Android 原生支持的、基于 futex 的 fsync 方案。

66.6.4 WINEPREFIX 前缀、C 盘、注册表

Wine 前缀(WINEPREFIX,本环境 rootfs 内路径/home/xuser/.wine)是一套完整独立虚拟 Windows 安装:虚拟 C 盘、注册表、独立 wineserver。内部drive_c/windows/system32存放 64 位系统 DLL;drive_c/windows/syswow64存放 32 位 DLL(保留 Windows 命名倒置习惯);dosdevices 符号链接把盘符映射主机路径。注册表以文本.reg存放在前缀根目录:system.reg对应 HKLM;user.reg对应 HKCU;注册表数据由 wineserver 实时维护。

该目录树就是容器可以随意复制、删除的本质。GameNative 每一个游戏容器,本质就是每一个独立 Wine 前缀。相互冲突 DLL、注册表需求的多个游戏之间互相隔离。

66.6.5 DLL 覆盖:DXVK 如何替换 Wine 原生 Direct3D

Wine 对每一个 DLL,选择使用内置实现,或者使用放在前缀目录下外部原生 DLL。选择逻辑受环境变量WINEDLLOVERRIDES、注册表HKCU\Software\Wine\DllOverrides控制。整套图形高速通路就依靠该机制实现:

DXVK、VKD3D‑Proton 提供 PE DLL,文件名与 Wine 内置 Direct3D DLL 完全一致(d3d9.dlld3d11.dlldxgi.dlld3d12.dll);安装脚本复制到前缀system32/syswow64,注册表标记这些 DLL 类型为 native。游戏调用D3D11CreateDevice,Wine 加载器优先加载 DXVK 文件,不再调用 Wine wined3d 内置实现。Direct3D 调用就此转为 Vulkan(66.7),而不是 OpenGL。

66.6.6 Wine 与模拟器对接

结合 66.5 与 66.6:

  1. ARM64EC 配置 :Wine 为原生 ARM64EC;DLL 在系统调用边界切换到原生代码;模拟器(libarm64ecfex.dll或者 WoW64 辅助库)只负责运行游戏 x86‑64 指令流。
  2. 全仿真配置:整套 Wine 都是 x86‑64 代码,运行于 Box64/FEX,依赖 rootfs glibc。

两种配置对外给游戏呈现完全一致 Windows 环境;区别在于游戏下层有多少代码被仿真、多少代码原生执行,这是帧率表现最大决定因素。

66.7 图形:Direct3D 通向 Adreno GPU

图形链路决定整套栈成败;也是本书最长转换链路:x86‑64 游戏发起 Direct3D 调用,最终转化 Vulkan 命令交给手机 Adreno 驱动,SurfaceFlinger 做画面合成。链路每一环对应独立技术组件。

66.7.1 转换总链路

示意图:完整的 Direct3D 到显示屏渲染链路

链路被 66.4 讲的 libc 边界切分成两半:边界上方 Windows / 虚拟机世界,Direct3D 转为 Vulkan;边界下方安卓侧,Vulkan 抵达真实驱动,帧输出到显示器。工程难点全部集中在如何跨越边界。

66.7.2 D3D 转 Vulkan:DXVK、VKD3D 以及备选回退

多个转换器覆盖 Direct3D 版本,全部通过 Wine DLL 覆盖机制安装(66.6.5):

  1. DXVK :Direct3D9/10/11 转 Vulkan,输出d3d9.dlld3d11.dlldxgi.dll等。GameNative 容器DEFAULT_DXWRAPPER默认选用 DXVK。
  2. VKD3D‑Proton :Direct3D12 转 Vulkan,输出d3d12.dlld3d12core.dll。Valve 维护的 vkd3d 分支,面向游戏性能优化。
  3. D8VK:DXVK 扩展,支持 Direct3D8。
  4. wined3d:Wine 内置回退方案,Direct3D 转 OpenGL,性能更低,仅用于 DXVK 不兼容游戏。
  5. cnc‑ddraw:处理老 DirectDraw 游戏。

架构关键点:ARM64EC 模式,这些转换器本身就是原生 ARM64 代码(66.5.8)。只有游戏 D3D 调用源头来自仿真代码;把一帧绘制调用转化 Vulkan 命令缓冲区的繁重工作,全部原生运行。这是手机 DXVK 性能远高于整套 Wine 全部被 x86 仿真的根本原因。

66.7.3 访问真实 GPU:Turnip 与 Vortek

Vulkan 命令要送达物理 Adreno GPU,有两条完全不同路线,是图形设计最重要分叉:

  1. Turnip:Mesa 开源 Adreno Vulkan 驱动。栈内置编译版本,也可以通过 adrenotools 侧载更新版本(66.4.2),虚拟机获得完整可用 Vulkan 实现,直接和 Adreno 内核接口交互。手机出厂自带 Vulkan 驱动面对 Wine、DXVK 生成的特殊负载,经常残缺、存在 Bug;使用经过验证 Mesa 驱动规避这些问题。FEX 环境虚拟机通过 Thunk 跳板访问 Turnip(66.5.4):虚拟机侧 libvulkan 桩转发调用到原生 Turnip。
  2. Vortek :Winlator 自研 Vulkan 兼容层,不走 Thunk,选择 66.4.5 IPC 方案。客户端‑服务端架构:虚拟机链接精简 Vortek Vulkan ICD(从 assets vortek 压缩包部署进 rootfs),序列化每一条 Vulkan 调用为命令流,Unix Socket 发送给安卓原生服务端libvortekrenderer.so,由VortekRendererComponent启动。服务端回放命令交给设备真实 Vulkan 驱动;途中还可以修补 SPIR‑V 着色器、解码主机驱动不支持纹理格式、模拟缺失功能。GameNative 默认图形驱动选择 Vortek,Socket 隔离天然对 glibc/bionic 不兼容具备强容错:虚拟机与主机驱动不共享地址空间。

示意图:Turnip (Thunk) 对比 Vortek (IPC) 访问 GPU Vortek

针对不支持 Vulkan 的场景或是低端硬件环境,还存在另外几套渲染方案:Zink(Mesa 项目实现的基于 Vulkan 的 OpenGL 后端)、VirGL(一套自带客户端‑服务端架构的 virtio‑gpu 虚拟 3D 渲染器),以及 llvmpipe(兜底使用的纯 CPU 软件光栅化器)。

Zink 值得单独说明,它与 AOSP 中的一个组件在设计上互为对等实现:当虚拟机侧需要使用 OpenGL,但宿主设备仅提供完善的 Vulkan 驱动时,Zink 会将 OpenGL 调用转换为 Vulkan 指令。这一点和 Android 原生的 ANGLE 组件(external/angle/)作用完全一致 ------ANGLE 用来为调用 OpenGL ES 的本地应用完成 API 转译(参见第 13 章)。在绝大多数设备上,ANGLE 并非系统默认的 GLES 驱动,EGL 加载器源码中也标注了这一点(frameworks/native/opengl/libs/EGL/Loader.cpp:555);ANGLE 是以单应用指定或全局配置的方式启用。

Windows 游戏这条渲染链路并不会走 ANGLE。游戏运行环境会在根文件系统内部自行完成 GL 到 Vulkan 的转换(使用 Zink,或是 wined3d 输出 OpenGL 指令),再通过 Vulkan 加载器访问硬件。二者只是设计思路相似,代码链路并不共用

66.7.4 Android Vulkan 加载器

无论虚拟机选择哪一条通路,底层最终都是第 13 章 AOSP Vulkan 加载器。加载器发现、加载设备 Vulkan 驱动代码位于frameworks/native/vulkan/libvulkan/driver.cppLoadDriver函数(frameworks/native/vulkan/libvulkan/driver.cpp:153)调用android_dlopen_extframeworks/native/vulkan/libvulkan/driver.cpp:171),配合命名空间标志打开 HAL 驱动vulkan.<board>.so;驱动处在受限链接命名空间。Vortek 服务端这类原生渲染器、Thunk 调用 Turnip,本质都是该加载器的普通客户端。整套方案不需要修改平台,正是因为 GPU 访问完全复用任意安卓游戏都在使用的公开 Vulkan 接口。

66.7.5 画面输出:应用内 Surface

渲染帧最终转化屏幕像素。游戏没有真实 X 显示器、窗口系统;使用 GameNative 内置应用 X 服务器,com.winlator.xserver,原生代码存放app/src/main/cpp/winlator/,接收游戏输出,管理游戏绘制窗口。X 服务器内容由 Vulkan 渲染输出到安卓 Surface。

最后一步使用第 13 章原生窗口 API。帧交给合成器依靠ANativeWindow;CPU 访问路径使用ANativeWindow_lockframeworks/native/libs/nativewindow/include/android/native_window.h:179)、ANativeWindow_unlockAndPostframeworks/native/libs/nativewindow/include/android/native_window.h:188);零拷贝 GPU 路径分配AHardwareBufferframeworks/native/libs/nativewindow/include/android/hardware_buffer.h:479AHardwareBuffer_allocate),把它绑定为 Vulkan 交换链图像;GPU 直接渲染到 SurfaceFlinger 可以扫描输出缓冲区。从 SurfaceFlinger(第 24 章)开始,游戏画面只是另一层普通图层参与合成输出。

66.7.6 帧生成

可选增强特性:GameNative 可以插入 Vulkan 隐式层,拦截vkQueuePresentKHR,在渲染帧之间插值生成中间帧。属于标准 Vulkan 层,插入加载器层链路;体现图形栈大量能力基于 Vulkan 标准扩展点实现,而非私有钩子。

66.8 音频:从 WASAPI 到 AAudio

音频链路相比图形链路更短,但同样要跨越 libc 边界,该问题复用 66.4.5 的 IPC 模式:Android 侧运行真实音频服务,客户机端以客户端身份与其建立连接。

66.8.1 Wine 的音频后端

Windows 游戏通过多种前端 API 输出音频:现代的 WASAPI(经由 mmdevapi)、传统 DirectSound、老旧的 winmm/waveOut。Wine 将全部前端接口统一交由运行时选定的后端驱动处理。 本文重点关注 winepulse.drv(面向 PulseAudio);另外还有 winealsa.drv(ALSA)、wineoss.drv(OSS)。每个后端由 PE 驱动 DLL + Unix .so 组成,后者调用宿主机音频客户端库。 在 ARM64EC 配置下,后端及其客户端库为原生 ARM64,音频混音与传输不经过模拟;只有游戏调用前端 API 的这部分会走模拟器

66.8.2 App 内部的 PulseAudio 服务

未 Root 应用无法使用系统 PulseAudio 守护进程,因此组件自带一套。 GameNative 的 PulseAudioComponent 从打包的原生库(libpulse.so,以及 jniLibs/ 下 PulseAudio13 的 libpulsecommon/libpulsecore)启动 PulseAudio 服务。 Wine 的 winepulse.drv 通过 PulseAudio 标准套接字协议,和该服务通信,和桌面端 PulseAudio 使用方式完全一致。 基于 PulseAudio 有线协议,客户机侧(glibc,可能被模拟)与服务端(bionic,原生)可以跨 libc 边界互操作,不需要共享地址空间

示意图:从游戏到扬声器的完整音频传输链路

66.8.3 ALSA 桥接

Winlator 还提供一套底层 ALSA 通路。Rootfs 内置自定义 ALSA PCM 插件(Winlator 的 android_alsamodule_pcm_android_aserver.c),对外暴露 android aserver PCM 设备;Android 侧 com.winlator.alsaserver 实现服务端点,原生客户端(app/src/main/cpp/winlator 音频代码)将 PCM 音频帧投递到应用。 无论走 PulseAudio 通路还是 ALSA 通路,架构完全一致:客户机音频 API + 套接字 + Android 原生端点。

66.8.4 通过 AAudio 输出到扬声器

Android 端点最终将解码后的 PCM 数据写入 Android 音频流。首选 API 是第 15 章介绍的 AAudio:调用 AAudio_createStreamBuilder 创建流,完成配置并打开,使用 AAudioStream_write 送入音频帧,音频帧经由 Audio HAL 输出到扬声器。 (旧版本、受 targetSdk 限制的构建会使用 AudioTrack / OpenSL ES;AAudio 是现代低延迟通路)。 音频链路最终使用的 AOSP 子系统,和普通原生 Android 游戏完全相同。

66.8.5 延迟

音频经过客户机 API、套接字、服务转发会引入延迟。该组件做了针对性调优:PulseAudio 使用刻意增大的缓冲区(容器设置 PULSE_LATENCY_MSEC,约一百多毫秒),牺牲响应性,换取在模拟器抖动、IPC 干扰下无爆音的稳定播放。 务实取舍:经过多层组件,无法做到极致低音频延迟;但对绝大多数游戏,稳定播放远比几十毫秒的滞后更重要。

66.9 整合回顾:三条完整调用路径

本章最清晰的总结方式:跟踪三种操作,从游戏向下走到 Android 平台,观察每层采用的不同通路。

示意图:三类操作,三条不同的系统栈执行路径

  1. 一次 Direct3D 11 绘制调用。游戏的 ID3D11DeviceContext::Draw 属于 x86‑64 指令调用(仿真执行)。调用进入 DXVK;在 ARM64EC 环境下 DXVK 为原生代码,负责构建 Vulkan 命令缓冲区,随后通过转换层(Turnip)或者套接字(Vortek)跨越 libc 边界提交给 Adreno 驱动,最终画面交由 SurfaceFlinger 完成合成。整条链路中绝大部分逻辑都以原生方式运行,仅有最初的调用点是仿真执行的。
  2. 一次 CreateFile 文件打开调用。游戏发起文件打开请求。该 Windows API 调用转为 NT 系统调用,经由 Wine 的 __wine_syscall_dispatcher 分发至 ntdll.so;该组件将 NT 格式路径转换为 Unix 路径(nt_to_unix_file_name,对应源码路径 dlls/ntdll/unix/file.c),并以 create_file 请求的形式把 Unix 路径发送给 wineserver。服务端打开文件后返回一个 Windows 句柄;后续需要时再通过 SCM_RIGHTS 机制从服务端获取底层对应的 Linux 文件描述符。过程中 PRoot 或 libredirect 会对路径重定向至容器根文件系统,最终由 Linux 的 openat 在应用私有存储空间完成打开操作。该流程不涉及 GPU、音频组件,和第一条图形链路的实现机制完全不同。
  3. 一次音频缓冲区写入操作。游戏的 WASAPI 写入请求交由 winepulse.drv 处理(ARM64EC 下为原生代码),驱动将 PCM 音频数据通过 PulseAudio 套接字发送至应用内服务端,再写入 AAudio 音频流,最终输出到扬声器播放。

三种操作,三条完全独立路径;只有被框在模拟器内部的部分,才会逐条翻译指令。整套技术栈的核心思路就是尽可能缩小需要模拟的范围

66.10 Android 17 对该组件栈带来的变化

本章所有翻译组件都不属于 AOSP 内置,Android17 并不会自带 Windows 游戏运行时。Android17 只是改变这套上层组件依赖的底层平台基础。有三处关键变更,分别对上层组件产生帮助或者约束。

66.10.1 依赖的平台接口保持稳定

整套方案的前提:只使用公开、稳定的 AOSP 接口: Vulkan 加载器、ANativeWindow原生窗口 API、AHardwareBuffer、AAudio、ASharedMemoryandroid_dlopen_ext命名空间标志、链接器 W^X 保护规则。 Android17 中,上述接口契约保持不变。这也就是 Winlator 这类应用可以跨 Android 版本工作,不需要平台补丁的原因:底层没有新增私有接口,直接复用系统公开的 Vulkan、音频、链接器能力。

66.10.2 Berberis 不是本组件栈,Android17 对其做了目录重构

很容易误以为这里用到 Android 内置二进制翻译组件 Berberis,但实际上没有。 Android17 重构了 Berberis(第 19 章):CPU 模拟核心全部收拢到 frameworks/libs/binary_translation/cpu_emulation/;三层引擎(解释器、轻量翻译器、重度优化器)、分层调度器都放在该目录下。 但 Berberis 的用途是:RISC‑V64 客户机代码 → x86‑64 宿主机,并且通过 Native Bridge 接入 ART。方向完全相反(不是 x86‑64 → AArch64);集成点也不一样(用于 ART 内 APK 代码,不是完整 Windows 进程),不能用来跑 PC 游戏。 Android17 的 Berberis 目录重构仅影响第 19 章,不改变本章内容;x86‑64 到 AArch64 翻译仍然完全依靠 FEX / Box64,AOSP 没有内置同类翻译器。

66.10.3 ANGLE 的适用边界

Android 的 ANGLE 是系统自带 GLES→Vulkan 翻译层,概念上和客户机内部 Zink、wined3d‑to‑GL 路径有重叠。 Android17 下 ANGLE 并非绝大多数设备的默认 GLES 驱动(参考 Loader.cpp 注释),需要 App 主动开启或系统配置。 对本组件栈没有影响:客户机内部自行完成 GL / D3D → Vulkan 转换,直接调用 Vulkan 加载器访问 GPU。设备的 ANGLE 开关既不会增益、也不会干扰 Windows 游戏运行。二者是 libc 边界两侧两套独立的同类解决方案,不存在链路复用。

66.11 动手实践

实验需要拉取 GameNative 源码,设备 / 模拟器安装 Winlator 类应用;读源码仅需要克隆仓库;设备侧操作前提:合法安装应用与拥有版权的游戏。

  1. 读取默认组件配置:克隆 GameNative,查看 Container.javaDefaultVersion.java,记录默认模拟器、图形驱动、音频驱动、DX 封装层,以及 Wine、DXVK、VKD3D、Turnip 的锁定版本,两份文件即可还原完整默认流水线。
  2. 找到启动命令:阅读 GuestProgramLauncherComponent.java,定位组装 box64 <待运行exe> 的逻辑;阅读 BionicProgramLauncherComponent.java,找到区分普通 Wine 构建与 ARM64EC Wine 构建的分支,对比两条启动链路。
  3. 查看打包组件:浏览 app/src/main/assets/,识别 FEX、Box64、DXVK、Vulkan 驱动的 .tzst/.txz 压缩包,以及按需下载的 *_download.json 清单;区分哪些组件内置在 APK,哪些运行时下载。
  4. 设备上观测进程:游戏运行时,adb shell ps -A 查看同 UID 下 Wine、wineserver、PulseAudio、X Server 进程;针对 Wine 进程读取 /proc/<pid>/maps,观察 JIT 匿名可执行映射以及 rootfs 库。
  5. 查看套接字:adb shell ls -l /proc/<pid>/fd,找到 Wine 和 wineserver、Vulkan 渲染器、PulseAudio 之间的 Unix 域套接字,这就是 66.4.5 描述的跨边界通信实体。
  6. 探查 Wine 容器前缀:定位应用数据目录下 Wine 容器,查看 drive_c/windows/system32 下 DXVK 覆盖 DLL;查看 user.regSoftware\Wine\DllOverrides DLL 覆盖配置项。
  7. 确认 AOSP 底层调用点:AOSP 源码打开 frameworks/native/vulkan/libvulkan/driver.cppLoadDriver,确认驱动使用带命名空间的 android_dlopen_ext 加载,整套图形栈最终流入该公开接口。

66.12 小结

在未 Root ARM Android 手机运行 Windows 游戏,是一套专用翻译层堆叠而成;每一层解决 Windows x86‑64 游戏与 AOSP 底层之间的一类鸿沟。

解决问题 核心技术
GameNative App 调度、打包、应用外壳 Pluvia + Winlator 移植,GPL‑3.0
rootfs(imagefs) 在无 root 环境提供 glibc Linux 用户态 Ubuntu 根文件树存放 App 存储,PRoot /bionic loader 进入
bionic 重定向层 bionic 系统上跑 glibc 软件 路径重写、SysV‑SHM over ashmem、IPC 服务
FEX / Box64 AArch64 执行 x86‑64 指令集 动态重编译、TSO 模拟、thunk 与库包装
ARM64EC / WoW64 仅模拟游戏本体 ARM64 原生 Wine,模拟器以 DLL 形式存在
Wine Linux 上实现 Windows API PE/Unix 分离模型、wineserver、DLL 覆盖机制
DXVK / VKD3D Vulkan GPU 上跑 Direct3D D3D 转 Vulkan,以原生 DLL 覆盖方式部署
Turnip / Vortek Vulkan 对接真实 Adreno 显卡 Mesa 驱动 thunk 调用,或者套接字 C/S 模式
PulseAudio Windows 音频输出到扬声器 App 内置音频服务,后端输出 AAudio

核心要点总结:

  1. 两套完全独立的翻译:指令集翻译(FEX/Box64)和 Windows API 翻译(Wine)互不相关,混淆二者是理解这类程序最常见误区。
  2. 尽可能少模拟:所有性能收益,都来自把工作移出模拟器沙盒:GPU 驱动 thunk 封装、ARM64 原生 DXVK;尤其是 ARM64EC,只保留游戏代码被模拟。
  3. libc 边界是最大难点:glibc 与 bionic 不能共存于同一进程;跨边界三种手段(IPC、ABI thunk、bionic 原生运行)决定整套架构设计。
  4. 底层全部是标准 AOSP 接口 :Vulkan 加载器、ANativeWindowAHardwareBuffer、AAudio、链接器命名空间、ASharedMemory、W^X 保护都是普通 App 可用公开接口。上层翻译栈没有新增底层私有能力,巧妙的是在多层之上把 Windows 游戏调用转换成这些标准平台接口。

关键源码参考

文件 用途
bionic/libc/include/android/dlext.h:115 ANDROID_DLEXT_USE_NAMESPACE,指定链接器命名空间加载驱动
bionic/libc/include/android/dlext.h:80 ANDROID_DLEXT_USE_LIBRARY_FD,通过文件描述符加载库
bionic/libc/linker/linker_phdr.cpp:1057 W^X,拒绝同时可写 + 可执行 ELF 段
bionic/libc/include/sys/mman.h:183 memfd_create,匿名共享内存后端
frameworks/native/include/android/sharedmem.h:78 ASharedMemory_create,SysV‑SHM 重定向宿主机端
frameworks/native/vulkan/libvulkan/driver.cpp:153 LoadDriver,查找并加载设备 Vulkan 驱动
frameworks/native/libs/nativewindow/include/android/native_window.h:179 ANativeWindow_lock,CPU 渲染提交通路
frameworks/native/libs/nativewindow/include/android/native_window.h:188 ANativeWindow_unlockAndPost,向合成器提交帧
frameworks/native/libs/nativewindow/include/android/hardware_buffer.h:479 AHardwareBuffer_allocate,零拷贝 GPU 显示缓冲区
frameworks/av/media/libaaudio/include/aaudio/AAudio.h:1216 AAudio_createStreamBuilder,音频通路 Androi
相关推荐
云边有个稻草人5 小时前
飞牛 NAS 远程访问实战:星空组网连接 Mac 与安卓,从 Compose 部署到 5G 验证
android·5g·macos
聚美智数5 小时前
手机号归属地-手机号归属地查询-手机归属地-运营商归属地
android·智能手机
恋猫de小郭5 小时前
Shopify 从 React Native 回到 Swift/Kotlin,但是你以为有手就行??
android·前端·ios
菠萝加点糖5 小时前
Android ChipGroup 使用说明
android
我命由我123455 小时前
Android Compose 开发,使用 ConstraintLayout,但是引入的是旧的 ConstraintLayout
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
小鱼干..6 小时前
http://101.43.154.45:60083/start/index.php?page=hello
android
hai_android6 小时前
Android WorkManager 笔记
android·java
Android打工仔6 小时前
不要再把 Flow 当作一个容器
android·kotlin
我命由我123456 小时前
Android 开发问题:android.permission.CAMERA...duplicated with element declared at
android·java·java-ee·android studio·android jetpack·android-studio·android runtime