Triton:为 QEMU 打造的 DirectX 11 驱动
在前作中,我们介绍了 Neptune——一个基于 VirtIO 的 Direct3D 协议转发层。借助 Neptune,我们可以在虚拟机边界之间序列化 Direct3D API 调用,从而让 Linux 客户机在 Linux 宿主机上运行 Wine 游戏的速度,超过了直接在客户机中使用 DXVK 的方案。坦白说,那次成果算不上惊艳,但它为我们真正的目标奠定了基础:为 Windows 客户机提供现代图形加速。如今,我们通过打造一款全新的 Windows 驱动 Triton 实现了这一目标,它与 Neptune 一同将完整的 DirectX 11 支持带给了 QEMU 虚拟机。
Tron 是什么?
你可能会想:如果 Neptune 能序列化 Direct3D API 调用,而 Windows 又使用 Direct3D,那这事不就成了吗?既然 Direct3D 能在 Wine 里跑,那在 Windows 里也应该能跑吧?毕竟 Wine 不就是一个 Windows 模拟器吗?简短的回答是:某种程度上确实可以。Neptune 的 Mesa 驱动会构建一个 d3d11.dll 和 dxgi.dll,完整实现了 Direct3D API,所以只要把这两个文件放到游戏可执行文件旁边,程序就会加载它们而不是 Windows 自带的驱动,用这种方式确实能让一些游戏跑起来。之前的一些尝试方案用的也是这个思路,通过 DXVK→Vulkan→Venus 在应用程序内部本地运行 Direct3D。不过这种做法有不少缺点。首先,也是最关键的,你无法获得好的性能,因为窗口合成器(DWM)把你的画面当成一张位图来处理,只能用 CPU blit 把 GPU 图像缓冲区拷贝到正确的窗口位置。全屏应用也许可以想办法直接扫描输出,但桌面体验永远不可能流畅。其次,d3d11.dll 和 dxgi.dll 是 Windows 的核心组件,你不能直接替换系统文件还指望 Windows 正常运行。即便你真让它跑起来了,很多带反作弊的游戏也会专门检测这种修改,你照样玩不了。这也是为什么这种 DLL 替换只能针对单个应用程序来加载(而且兼容性参差不齐)。最后还有一点:每个想用图形加速的应用都得手动拷一份文件,用户体验实在太差了。正确的做法不是去实现 DirectX API,而是去实现 DirectX 的 DDI(设备驱动接口)。
DDI
在 Windows 中,应用程序通过系统的 Direct3D 和 DXGI 库进行通信。d3d11.dll(以及更早版本)负责完成复杂的状态跟踪工作,并把更精简的命令流发送给实现 DDI 的用户模式驱动(UMD)。应用程序还会调用 dxgi.dll 来初始化图形适配器、配置 swapchain 等。UMD 同样需要经由 DXGI 与内核模式驱动(KMD)通信。KMD 由显卡厂商(也就是我们)实现,用于驱动真实硬件(在我们的场景下则是虚拟硬件)。在 Wine 中,我们实现了一套自定义的 d3d11.dll 和 dxgi.dll 来拦截 API 调用;而到了 Windows 平台,我们则需要改去实现 UMD 和 KMD。
因此挑战就在于:实现符合 DirectX DDI 接口的 UMD,并同时搭建一套与 KMD 通信的私有接口,由 KMD 再去和 VirtIO 设备交互。
幸运的是,后半部分已经有现成的方案。anonymix007 和 arehnman 此前各自独立开发过 Venus(Vulkan)的 KMD。由于 Vulkan 是一套完全独立的图形 API,它不需要实现 DirectX DDI,其 UMD 的思路更接近于"替换 d3d11.dll"的做法——直接与 KMD 对接,把指令送往 QEMU。Neptune 以 Venus 为蓝本,因此两者在内核层的高级接口(DMA、命令缓冲等)非常相似,UMD 与 KMD 之间的接口也完全一致。最终我们选择以 anonymix007 的分支作为基础,因为他们的实现在 KMD 侧支持更多特性。
剩下的就是真正的硬骨头了:我们必须实现 DirectX 11 的 DDI。设计新系统时,借鉴前人解决类似问题的经验总是明智之举。然而,开源的 DDI 实现寥寥无几。Windows 显卡驱动是一个相当小众的领域,相关专家大多集中在为数不多的几家显卡厂商中。这也是 QEMU 在 Windows GPU 加速方面一直难以取得进展的原因之一。
幸运的是,有两个可行的开源实现可以供我们参考。首先,Mesa 有一个 DirectX 10 UMD。如果你没读过上一篇文章,简单来说,Mesa 在 Linux 上实现了 OpenGL。它会对 OpenGL 进行状态追踪,并发出 Gallium API 调用。这个 DirectX 10 UMD 是 OpenGL 之外的另一种选择,同样发出 Gallium API 调用。之后,Gallium 后端驱动(AMD、Intel、VirGL 等)会把这些调用转换为各个原生的图形驱动接口。上游 Mesa 的 DirectX 10 只支持软栅后端,近期才有一些让它在 VirGL 上跑起来的工作。不幸的是,macOS 上的 virglrenderer 缺少该 UMD 所需的许多特性,因此这条路走不通,无法在 macOS 宿主机上获得图形加速。不过,它与 Mesa 代码库的集成为我们集成 Triton 提供了一个干净的范例。
VirtualBox 拥有唯一的可工作的开源 DirectX 11 UMD,但这个驱动并不能直接被我们拿来用。他们的做法是:把 DDI 调用翻译成一种中间字节码,然后在宿主机端把字节码解释成 DirectX API 调用。虽然(借助 AI 的帮助)把这个字节码发射器和解释器移植进 QEMU 并不难,但我们最终没有这么做,原因有以下几点。首先,把 DDI 转成字节码、再把字节码还原成 DirectX API 这个过程容易产生 bug,进而影响游戏兼容性。事实上,从论坛上的讨论来看,确实有不少游戏在 VirtualBox 上跑不起来,问题就出在这里。一旦翻译引擎缺少某个特性或出现 bug,就需要大量持续的维护工作,而我们不想依赖 Oracle。其次,VirtualBox 的 GPLv3 与 virglrenderer 的 MIT 许可证以及 QEMU 的 LGPLv2 之间存在许可证不兼容的问题。VirtualBox 的代码无法直接集成,但我们确实从中学到了不少有价值的东西:他们列出了哪些 DDI 原型已经实现、哪些只是返回错误,这为我们勾勒出了一个可工作实现的最小需求范围。这份信息在 MSDN 文档里找不到,而如果试图把所有原型都实现一遍,工程量将大到难以承受。他们的 DXBC 签名算法对我们理解相关机制也很有帮助——因为 Microsoft 并没有公开过任何相关文档。
既然我们不想走 VirtualBox 那种用中间传输格式来传递 DDI 调用的路线,就可以换个更好的思路。如果你把 d3d11.dll 理解成把 DirectX API 调用大致转成 UMD DDI 调用的组件,那么我们的 UMD 要做的就是把这些 DDI 调用再转回 DirectX API 调用。这样做有什么好处?因为这样就能直接复用我们已经测试过且运行良好的 Neptune 协议,不必再为序列化 DDI 调用发明一套新的传输层。在宿主机侧,我们不需要做任何额外工作就能执行这些调用。VirtualBox 在客户端需要一个发射器和传输层,在宿主机侧又需要解释器和分发器。每多一步,就多一份延迟,也多一份引入错误和不兼容的风险。我们仍然在客户端保留发射器和传输层,但在宿主机侧不再需要解释器——反序列化后的 Neptune 命令本身就是 DirectX 11 API 调用,可以直接分发,无需再做解析。少一层转换,就少一处出错的机会。把 DDI 转回 API 还有一个好处:D3D11 里大多数 DDI 调用都有对应的 API 调用,所以转换过程非常简单,通常只是把一些 API 句柄映射成设备句柄,偶尔查一下 API 和 DDI 之间枚举值的差异。不过,最大的收益,同时也是整个方案中最复杂的部分,就是 DXBC 着色器代码。
DXBC
DXBC(DirectX Byte Code)是微软着色器编译器(FXC)输出的中间代码。具体来说,它是较老的(DirectX 12 之前的)格式,由 DirectX 使用的着色器语言 HLSL 编译而来。由于 Triton 做的是从 DDI 到 API 的反向转换,它不需要对这个着色器字节码进行反汇编和转换。这在复杂度和兼容性方面对我们来说是一个巨大的优势。不过,事情也没那么简单,毕竟不能直接把字节码原封不动地传给宿主机。
编译器(FXC)在输出 DXBC 字节码的同时还会附带各种元数据。d3d11.dll 期望看到这些元数据并加以消费。调用 DDI 时传入的只有字节码本身,也就是说,我们要把字节码"逆向还原"为对应的 API 调用,就必须先通过解释字节码把所有元数据重建出来。最终我们依然原封不动地透传字节码,但由于看不到原始的 DXContainer 文件,只能自行合成宿主机 DirectX 渲染器所要求的各个字段。这部分工作大量依赖反复试错,虽然由 AI 助手来完成,但它也是整套实现中最薄弱、最容易出错的一环。
宿主渲染器
目前的整体流程是这样的:
- 应用程序向系统库发起 DirectX 和 DXGI API 调用。
- 系统库通过 DDI 调用触发 Triton。
- Triton 的 DDI 层将原始 DXBC 字节码重新封装回 DXContainer,再向 Neptune 发起 DirectX 和 DXGI API 调用。
- Neptune 的 UMD 将 API 调用序列化,通过由 KMD 管理的环形缓冲区传递。
- KMD 利用 VirtIO 接口将命令发送到宿主机。
- QEMU 宿主机端处理该命令,并把 Neptune 调用转交给 virglrenderer。
- virglrenderer 中的 Neptune 宿主模块对 API 调用进行反序列化,转发给宿主侧的 DirectX 实现。
- 宿主 DirectX 实现完成帧的渲染。
我们把目光放到最后一步。DirectX API 调用抵达宿主之后,还需要真正去执行渲染。为 Linux 上的 Wine 适配 Neptune 时,我们分叉了 DXVK,让交换链图像能够作为 DMAbuf 资源导出。当时我们选择在宿主侧实现交换链,目的是规避共享纹理带来的难题。交换链图像在内部就是一张纹理,但这类纹理比较特殊:宿主必须能够定位它们,并借助它们把最终画面显示到屏幕上。我们的 Wine DXGI 库会把所有交换链相关的 API 调用直接转发到宿主,这样宿主就能"知道"哪些纹理会被当作后台缓冲区。随后通过单独的 VirGL 命令,把纹理 blob 扫描输出到虚拟机窗口。这种做法虽然让客户机驱动更简洁、对 DXVK 的改动也更少,但代价是宿主 virglrenderer 进程中的交换链逻辑变得相当复杂。
在推进 Triton 的过程中,我们意识到在宿主侧处理交换链是个错误的设计。在 Windows 上,DXGI 是系统组件,负责与 UMD 通信。DXGI 负责创建后台缓冲、帧调度、模式切换等,而 UMD 基本不会对 DXGI 做特殊处理,因此我们把 DDI 调用"逆向转换"回 API 调用的方法在 DXGI 上行不通。这意味着我们在宿主侧添加的交换链逻辑大部分被绕过了。桌面合成器(DWM)操作的是共享纹理:某个进程的 DXGI 将内容渲染到自己的后台缓冲,然后该缓冲被共享给 DWM 进程,由 DWM 绘制桌面、窗口边框等,最终合成后的帧被送去扫描输出。因此,除了 DMAbuf 导出之外,我们还需要在 DXVK 中实现 DMAbuf 导入(不同的 guest 上下文映射到不同的宿主上下文)。当导入和导出都实现之后,宿主侧的交换链逻辑就不再必要了。为了让 Wine 驱动更加统一,我们把所有交换链逻辑移到了 guest 侧的 Neptune 驱动中。这样做还有一个额外的好处:更贴近 Venus 的设计思路,使 virglrenderer 保持干净。
macOS
让 virglrenderer 在 macOS 上运行本身就有些挑战,但既然 Venus 现在已经能在 macOS 上运行,后端的主要难题基本都已解决。剩下的工作就是把 Neptune 接到一个宿主侧的 DirectX 渲染器上。目前有三个主要项目可以实现在 macOS 上跑 DirectX,但它们都是以运行 Wine 为主要目标而设计的,缺少 Neptune 和 Triton 所需的共享纹理和共享同步机制。
DXVK + MoltenVK
DXVK 是我们在 Linux 宿主机上使用的方案,它把 D3D11 API 转译为 Vulkan API,再借助宿主机的 Vulkan 驱动来完成渲染。在 Linux 上这套方案效果很好,因为 Vulkan 是一等公民,而且目前几乎所有现代显卡都有成熟的 Vulkan 驱动。但在 macOS 上,Vulkan 还要再经过一层转译——MoltenVK——才能转成 Metal API。在上一篇文章中,我们讨论过让 DXVK + MoltenVK 正常工作的特殊挑战,简单来说就是:它很不稳定,兼容性方面还需要大量的改进。
DXMT
DXMT 绕开了 Vulkan 的问题,直接把 D3D11 转译为 Metal(D3D12 支持也即将到来)。和 DXVK 一样,这个项目最初主要是为 Wine 设计的,所以第一步是实现该库的原生版本。在我们的 fork 中,DXMT 可以编译为 macOS 共享库,并额外导出一些接口,用于纹理和 fence 的跨进程导入导出。
共享纹理
dxmt-native 设计中的一个主要难点在于实现可以跨越进程边界的共享纹理。我们之所以需要跨进程,是因为 virglrenderer 会为每个渲染器上下文派生辅助进程,因此几乎每个客户端 D3D 上下文都对应着一个独立的 virgl_render_server 进程。这种严格的进程隔离能保证单个渲染器崩溃时不会拖垮整个虚拟机。当然,我们也可以强制使用旧的基于线程的隔离模型,在渲染器上下文之间直接传递标准的 MTLTexture 句柄(未来移植到 iOS 时不得不这么做),但上游维护者不愿意支持这种方式。跨进程共享 Metal 资源本身就很棘手,不过仍有几种"支持较好"的实现路径。
MTLSharedTextureHandle+ XPC:这是 Apple 推荐的方式,但它需要引入 XPC,而 XPC 本身又有一堆麻烦。QEMU 和 virglrenderer 目前都使用文件描述符加SCM_RIGHTS在进程间传递句柄,但MTLSharedTextureHandle不支持这种方式。长远来看,为了获得最佳性能,我们应该在整个 virglrenderer、QEMU 和 SPICE 中统一采用这个方案;但在这次初步移植中,我们不想对各个项目做大的架构改动。IOSurface:可以渲染到 IOSurface,然后把它的全局句柄共享给其他进程。UTM 的加速渲染就是这么实现的,但这用的是 Apple 早已废弃的技术。此外,你还得为多出来的这一次 GPU blit 到 IOSurface 的操作付出性能代价,这也意味着要在 virglrenderer 中搭建一套渲染管线,增加了复杂度。CALayerHost:Chrome 和一些老版本 macOS 应用在跨进程渲染时使用的私有 API。从延迟角度看,它比 IOSurface 更差(CoreAnimation 处在图形栈更上层),反过来(从CALayer再转回MTLTexture)就更复杂了。这种方式也许能用于离线渲染,但无法满足需要合成的共享纹理场景。
这些方案都没办法让我们用 SCM_RIGHTS 顺畅地跨进程共享纹理。不过在 Venus 的讨论中,@Drakulix(当时正在把 Wayland 移植到 macOS)提出了一个新思路:用 shm_open() 创建一个共享内存对象(它可以表示为一个文件描述符,兼容 SCM_RIGHTS),然后用 newBufferWithBytesNoCopy:length:options:deallocator: 将其映射成一个 MTLBuffer。这样就得到了一块 CPU 和 GPU 都能访问的统一内存区域,在另一个进程里也可以重复这一套流程。之所以这一切在 Apple Silicon 上可行,是因为 UMA(统一内存架构)让 CPU 和 GPU 共享同一套物理地址空间。唯一的缺点是它只能用于线性纹理,内存利用率不高。不过只要共享纹理的数量不多,这应该不成问题。
共享 Fence
在不同进程的不同上下文之间共享纹理只是问题的一半,另一半则是同步。当生产者进程A正在向共享纹理绘制,而消费者进程B同时将所有共享纹理合成到最终扫描输出图像时,如果A的绘制尚未完成而B就开始合成,就会出现撕裂现象。为防止这种情况,需要围栏机制,让A在B绘制时阻塞,B在A绘制时阻塞。更复杂的是,由于GPU与CPU异步执行,必须使用GPU围栏。理想情况下,B的GPU进程能直接消费A的GPU围栏,无需A的CPU进程轮询。实现这一点的唯一途径是使用MTLSharedEventHandle,这依赖于XPC。不过,我们可以依靠两个事实,通过模拟围栏来接近这一目标。
- 我们的大多数共享围栏事件发生在帧完成边界,这意味着CPU端等待的额外延迟被限制为每完成一帧仅一次。
- 围栏事件的生产者可在GPU上执行,而消费者必须在CPU上等待,这样我们只需在一侧浪费CPU周期。
我们的模拟围栏工作方式如下:生产者调用ID3D11DeviceContext::ClearUnorderedAccessViewUint,传入消费者进程映射的共享内存缓冲区地址。此API调用允许我们向共享内存写入任意整数,并借此写入时间线值。该写入由GPU完成,因此与A的其他绘制调用有序排列,当GPU中发生时间线值写入时,我们便知道所有绘制已完成。消费者必须轮询共享内存(由于Apple GPU上没有内存值轮询指令,这必须在CPU上完成)。一旦看到更新的时间线值,它就知道绘制已完成,可以安全消费纹理。消费者CPU在观察到围栏事件前无法排队其绘制调用,因此会引入延迟,而GPU可能在此期间空闲等待下一次提交。
D3DMetal
macOS 上最后一个 DirectX API 实现是苹果为 Game Porting Toolkit 打造的。GPT 最初是为了让开发者在 Apple Silicon 上测试 Windows 游戏而设计,它包含了D3DMetal.framework,这是基于 Metal 的 D3D11 和 D3D12 实现,同时还有一个从 DXBC/DXIL(微软专有的 GPU 着色器字节码格式)到 AIR(苹果专有的 GPU 着色器字节码格式)的转译器。和 DXMT 一样,它专为 Wine 设计;也像 DXMT 一样,它不支持共享纹理或共享围栏,因此我们需要用同样的方式模拟这些功能。不过,与 DXMT 不同的是,它并非开源,所以我们得借助 swizzling 和 vtable 修补来拦截 API 调用并修改输出结果。
这正是我们在 d3dmetal-native 中所做的。它是 D3DMetal.framework 的一个封装,使其能在 Wine 之外运行,并支持这些额外功能。由于我们将 API 接口设计为与 DXMT 兼容,因此在 virglrenderer 中可以轻松切换这两种接口。最终,性能相比 DXMT 有了显著提升。
virgl_render_server 进程。即便如此,它的性能仍然优于在原生 ARM64 上运行的 DXMT。
遗憾的是,D3DMetal 的许可条款明确禁止将其用于“开发、测试或评估苹果品牌产品上的视频游戏”之外的任何用途,并且只能“仅限非商业目的”分发。这意味着我们不能将 D3DMetal 作为捆绑应用的一部分。有趣的是,CrossOver,一个商业 Wine 发行版,确实捆绑了 D3DMetal,我听说他们与苹果达成了特殊协议才得以如此。如果有人了解这一安排,请与我们联系,因为我们非常希望将 D3DMetal 纳入 UTM,以提升性能表现。
试试看
这里描述的所有工作都是开源的,所以如果你喜欢折腾,可以亲自尝试并给我们反馈。我们正在积极将这些改动尽可能地上游化,并很快会更新 UTM 以支持这些功能,这样任何人都无需编译多个项目即可体验。
提示: 把你的 AI 指向这个页面,让它帮你完成设置。
代码
构建(macOS)
这些构建说明专门针对 macOS。Linux 的构建说明与上一篇文章基本一致,没有变化。
所有组件都安装到一个统一的暂存前缀中,各部件通过该前缀的 pkgconfig 目录相互定位,所以请先设置好这些环境变量,并在整个会话期间保持它们:
ANGLE 与 libepoxy
QEMU 的 `-display cocoa,gl=es` 路径和 virglrenderer 的 GL 后端都经由 ANGLE-on-Metal 实现,这部分由 WebKit 源码树构建,同时还需要一个能分发调用的 libepoxy。这与 Venus 项目中的做法一致,但它是后续所有工作的前提。 ``` git clone --filter=tree:0 --no-checkout https://github.com/utmapp/WebKit.git "$SRC/WebKit" git -C "$SRC/WebKit" sparse-checkout init git -C "$SRC/WebKit" sparse-checkout set Source/ThirdParty/ANGLE Configurations Tools/ccache git -C "$SRC/WebKit" checkout 6a7f464047e2f6f2b65fe315aaad5d1ff3229cb7 cd "$SRC/WebKit/Source/ThirdParty/ANGLE" xcodebuild archive \ -archivePath "$PREFIX/ANGLE" \ -scheme ANGLE \ -sdk macosx \ -arch arm64 \ -configuration Release \ WEBCORE_LIBRARY_DIR=/usr/local/lib \ NORMAL_UMBRELLA_FRAMEWORKS_DIR="" \ CODE_SIGNING_ALLOWED=NO \ MACOSX_DEPLOYMENT_TARGET=11.0 ``` ``` git clone -b macos-venus https://github.com/utmapp/libepoxy.git "$SRC/libepoxy" meson setup "$SRC/libepoxy/build" "$SRC/libepoxy" \ "-Dc_args=-I$ANGLE_INC" \ -Degl=yes \ -Dx11=false \ "--prefix=$PREFIX" meson install -C "$SRC/libepoxy/build" ```DXMT
DXMT 默认按 Wine 交叉编译方式构建;如果不传入交叉文件,则会进行原生构建,把所有模块链接成一个 `libdxmt-native.dylib`,导出 D3D11/DXGI 入口点以及 Neptune 渲染服务器所需的嵌入 API(Win32 风格事件和共享纹理)。 ``` export LLVM15=/path/to/llvm@15 # arm64 LLVM 15 安装根目录 git clone https://github.com/utmapp/dxmt.git "$SRC/dxmt" cd "$SRC/dxmt" meson setup build-native \ "-Dnative_llvm_path=$LLVM15" \ --buildtype=release \ "--prefix=$PREFIX" meson install -C build-native ```Homebrew 的 llvm@15 可用于 $LLVM15;DXMT 的 docs/DEVELOPMENT.md 也记录了从源码构建的方法。
d3dmetal-native
D3DMetal.framework 仅提供 x86_64 版本,因此该库及其加载它的每个进程都必须是 x86_64 架构。仓库中包含了选择 -arch x86_64 的交叉编译文件,Rosetta 2 可以透明地运行生成的结果(包括测试套件)。
git clone https://github.com/utmapp/d3dmetal-native.git "$SRC/d3dmetal-native"
cd "$SRC/d3dmetal-native"
meson setup build \
--cross-file build-macos-x86_64.txt \
-Dtests=disabled \
"--prefix=$PREFIX"
meson install -C build
框架本身不随库分发:请从 Apple 的 Game Porting Toolkit 获取,并在运行时通过 D3DMETAL_FRAMEWORK_PATH 指向它(或使用 -Ddev_framework_path=... 编译时指定备用路径)。如果 macOS 将其隔离,请执行 xattr -dr com.apple.quarantine D3DMetal.framework。
virglrenderer
这个组件比较特殊,因为单个 QEMU 进程需要同时驱动一个原生 arm64 库和一个以任一架构运行的渲染服务器。它是对同一源码树的两套 Meson 配置:
- 原生 arm64 — 生成 QEMU 链接的
libvirglrenderer,以及托管 Venus 和 Neptune DXMT 后端的渲染服务器。这是唯一会被安装的配置。 - x86_64 交叉编译 — 为 Neptune D3DMetal 后端生成相同的渲染服务器,由于框架是 x86_64,因此该服务器也必须是 x86_64。它静态链接 virglrenderer,且不会被安装。
随后,两个服务器通过 lipo 融合为一个通用二进制文件。运行时,父进程根据上下文选择工作进程的切片——Venus 上下文使用 arm64 切片,Neptune 上下文默认在 Rosetta 下使用 x86_64/D3DMetal,或通过 NPT_BACKEND=dxmt 使用 arm64/DXMT——因此只需配置一个渲染服务器路径,即构建时内置的那个。
git clone -b macos-next https://github.com/utmapp/virglrenderer.git "$SRC/virglrenderer"
1. 原生 arm64(库 + 渲染服务器)。
meson setup "$SRC/virglrenderer/build-arm64" "$SRC/virglrenderer" \ "-Dc_args=-I$ANGLE_INC" \ -Dvenus=true \ -Dneptune=true \ -Drender-server-worker=process \ -Dcheck-gl-errors=false \ "--pkg-config-path=$PREFIX/lib/pkgconfig" \ "--prefix=$PREFIX" meson install -C "$SRC/virglrenderer/build-arm64" cp "$PREFIX/libexec/virgl_render_server" "$SRC/virglrenderer/virgl_render_server.arm64" 保存**已安装**的服务器,而不是构建树中的那个:只有已安装的副本带有指向 `$PREFIX/lib/libvirglrenderer.1.dylib` 的 `install_name` 路径,而 `lipo` 即将覆盖它。 **2. Rosetta x86_64 渲染服务器。** 这个版本使用 d3dmetal-native 仓库中的交叉文件进行交叉编译。`-Ddefault_library=static` 将 virglrenderer 链接到服务器中,因此融合后的二进制文件不需要 x86_64 dylib;而 `-Dvtest=false` 则移除了唯一引入 GL 的目标——前缀中的 libepoxy 仅支持 arm64。出于同样的原因,x86_64 配置不能尝试使用 EGL,因此给它一个独立的 pkg-config 目录并关闭 EGL: cp -R "$PREFIX/lib/pkgconfig" "$PREFIX/lib/pkgconfig-x86_64" sed -i '' 's/epoxy_has_egl=1/epoxy_has_egl=0/' "$PREFIX/lib/pkgconfig-x86_64/epoxy.pc" meson setup "$SRC/virglrenderer/build-x86_64" "$SRC/virglrenderer" \ --cross-file "$SRC/d3dmetal-native/build-macos-x86_64.txt" \ "-Dc_args=-I$ANGLE_IN