Triton:QEMU 的 DirectX 11 驱动
前传文章中,我们介绍了Neptune——一款面向VirtIO的Direct3D协议转发层。它能将Direct3D API调用序列化后跨虚拟机管理程序边界传输,让Linux宿主机上的Linux虚拟机运行Wine游戏时,速度比在虚拟机内直接使用DXVK更快。虽然当时的性能提升不算惊艳,但它为我们的核心目标打下了基础:为Windows虚拟机提供现代图形加速。如今,我们通过全新Windows驱动Triton实现了这一目标,结合Neptune可为QEMU虚拟机提供完整的DirectX 11支持。

什么是Triton?
你可能会问:既然Neptune能序列化Direct3D API调用,而Windows本身就使用Direct3D,那我们岂不是已经大功告成?既然Direct3D在Wine中能运行,那在Windows里也应该没问题,对吧?毕竟Wine不就是一款Windows模拟器吗?简单来说:确实能实现部分功能。Neptune Mesa驱动会生成d3d11.dll和dxgi.dll,这两个文件完整实现了Direct3D API集。只要把它们放到游戏可执行文件所在目录,游戏就会加载这两个文件而非Windows自带驱动,部分游戏就能以此方式运行。此前也有类似尝试,通过DXVK→Vulkan→Venus在应用内部本地运行Direct3D,但这种方法存在不少弊端:
其一,性能表现不佳。窗口 compositor(DWM,桌面窗口管理器)会将游戏帧识别为普通图像,必须通过CPU位块传输(blitting)把GPU图像缓冲区复制到正确的窗口位置。虽然可以针对全屏应用做些优化,实现原生扫描输出,但始终无法获得流畅的桌面体验。
其二,d3d11.dll和dxgi.dll是Windows的核心组件,替换系统文件会导致Windows无法正常运行。即便勉强让系统工作,许多带反作弊机制的游戏也会检测到这类修改,无法正常运行。因此这种方法只能按单个应用加载DLL,且兼容性参差不齐。
其三,要为每个需要图形加速的应用都复制文件,用户体验极差。正确的思路并非实现DirectX API,而是实现DirectX DDI(设备驱动接口)。
DDI 解析
在Windows系统中,应用程序与系统级Direct3D、DXGI库交互。d3d11.dll(及旧版本)负责复杂的状态追踪工作,将整理后的指令流发送给实现DDI的用户态驱动(UMD)。应用程序也会调用dxgi.dll来初始化图形适配器、设置交换链等。UMD同样通过DXGI与内核态驱动(KMD)通信,KMD由显卡厂商(此处指我们)开发,用于驱动实际硬件(或在本项目中驱动虚拟硬件)。
针对Wine,我们实现了自定义的d3d11.dll和dxgi.dll来拦截API调用;而针对Windows,我们需要实现的是UMD和KMD。
这就带来了挑战:既要实现基于DirectX DDI接口的UMD,还要搭建一个与KMD通信的私有接口,让KMD能与VirtIO设备交互。
幸运的是,KMD部分的问题已有人解决:anonymix007[https://github.com/anonymix007/kvm-guest-drivers-windows-venus/tree/viogpu3d-venus]和arehnman[https://github.com/arehnman/kvm-guest-drivers-windows]各自独立开发了适用于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调用。Mesa的DirectX 10 UMD则是OpenGL的替代方案,同样生成Gallium API调用,再由Gallium后端驱动(AMD、Intel、VirGL等)转换为原生显卡驱动API。上游Mesa的DirectX 10仅支持软件光栅化后端,近期有部分工作使其适配VirGL,但macOS版virglrenderer缺少该UMD所需的多项功能,无法为macOS宿主机提供图形加速。不过,它与Mesa代码库的整合方式,为我们提供了Triton整合的清晰范例。
第二个是VirtualBox的开源DirectX 11 UMD,但它无法直接为我们所用。VirtualBox的驱动会将DDI调用转换为中间字节码,再在宿主机端将字节码解释为DirectX API调用。借助AI辅助,将这套字节码生成器和解释器移植到QEMU并非难事,但我们最终放弃了这个方案,原因有二:
一是这种“DDI→字节码→DirectX API”的转换过程容易引发兼容性问题,导致游戏无法运行。从论坛讨论来看,许多游戏无法在VirtualBox中运行正是出于这个原因。如果转换引擎存在功能缺失或bug,需要投入大量精力维护,而我们不想依赖Oracle来解决这些问题。
二是许可证不兼容:VirtualBox采用GPLv3许可证,而virglrenderer使用MIT许可证,QEMU使用LGPLv2许可证,无法直接整合VirtualBox的代码。不过我们还是从中获得了宝贵经验:它列出的已实现和返回错误的DDI原型,为我们提供了最小可用实现的参考标准——这些信息无法从MSDN文档中直接获取,若要实现所有原型,工作量将极为庞大。此外,它的DXBC签名算法也很有参考价值,因为微软并未公开这一算法。
既然我们不想采用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字节码)是微软着色器编译器(FXC)生成的中间表示(IR)代码,属于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时,我们fork了DXVK,使其支持将交换链图像导出为DMAbuf资源。当时我们决定在宿主机端实现交换链,以规避共享纹理的问题。交换链图像本质是纹理,但这类纹理较为特殊,宿主机需要能找到它们并用于在屏幕上显示最终画面。我们的Wine DXGI库会将所有交换链API调用直接转发给宿主机,因此宿主机“知道”哪些纹理会用作后台缓冲区,再通过独立的VirGL指令将纹理数据扫描输出到VM窗口。这种设计简化了虚拟机端驱动,对DXVK的改动也最小,但增加了宿主机virglrenderer进程中交换链逻辑的复杂度。
开发Triton时,我们意识到在宿主机端处理交换链是个错误。在Windows中,DXGI是与UMD交互的系统组件,负责后台缓冲区创建、帧 pacing、模式切换等工作。UMD(大部分情况下)不会对DXGI做特殊处理,因此我们将DDI调用反向转换为API调用的方法,无法适配DXGI。这意味着我们在宿主机端添加的交换链逻辑基本无法发挥作用。
Windows桌面 compositor(DWM)基于共享纹理工作:一个进程的DXGI将内容渲染到自身的后台缓冲区,该缓冲区会共享给DWM进程,由DWM绘制桌面、窗口边框等,最终将合成好的画面设置为扫描输出目标。这意味着除了DMAbuf导出,我们还需要在DXVK中实现DMAbuf导入(不同虚拟机上下文对应不同宿主机上下文)。同时支持导入和导出后,宿主机端的交换链逻辑就不再必要了。为了统一Wine驱动,我们将所有交换链逻辑迁移到了虚拟机端的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驱动,因此DXVK表现出色。但在macOS上,Vulkan需要通过另一层转换——MoltenVK将Vulkan API转换为Metal API。上一篇文章中我们提到让DXVK + MoltenVK工作的独特挑战,简言之:该组合稳定性不足,需要大量额外工作才能保证兼容性。

DXMT
DXMT绕过了Vulkan的问题,直接将D3D11转换为Metal(即将支持D3D12)。与DXVK类似,它最初也是为Wine设计的,因此我们首先需要实现该库的原生版本。通过我们的fork版本,DXMT可编译为macOS共享库,并额外提供纹理和围栏的导入/导出接口。

共享纹理实现
dxmt-native设计中的一大难点是实现跨进程的共享纹理。virglrenderer会为每个渲染上下文启动辅助进程,大致每个虚拟机D3D上下文对应一个独立的virgl_render_server进程。这种严格的进程隔离可避免单个渲染器崩溃导致整个VM挂掉。我们可以强制使用旧的线程隔离模型,让标准MTLTexture句柄跨渲染上下文边界使用(最终适配iOS时也必须这么做),但上游维护者不支持这种方式。
在macOS上跨进程共享Metal资源并不容易,但有几种“成熟方案”:
MTLSharedTextureHandle+ XPC:这是苹果推荐的方式,但需要引入XPC,而QEMU和virglrenderer使用文件描述符和SCM_RIGHTS在进程间传递句柄,MTLSharedTextureHandle不支持这种方式。从性能角度看,我们最终应该在virglrenderer、QEMU和SPICE中统一采用这种方案,但在初始版本中,我们不希望对多个项目进行重大架构修改。IOSurface:可以渲染到IOSurface,再将全局句柄共享给其他进程。我们在UTM中就是用这种方式实现加速渲染,但该技术已被苹果弃用。此外还需要额外的GPU位块传输操作将数据写入IOSurface,这意味着要在virglrenderer中设置渲染管线,增加了复杂度。CALayerHost:Chrome等旧版macOS应用使用的私有API,用于跨进程渲染。它的延迟比IOSurface更高(CoreAnimation位于图形栈上层),且将CALayer转换回MTLTexture的过程更为复杂。该技术可能适用于离线渲染,但无法用于需要合成的共享纹理。
以上方案都无法通过SCM_RIGHTS实现跨进程纹理共享,但在Venus的讨论中,@Drakulix(正在为macOS适配Wayland)提出了一个新思路:使用shm_open()创建共享内存对象(可表示为支持SCM_RIGHTS的文件描述符),再通过newBufferWithBytesNoCopy:length:options:deallocator:将其映射为MTLBuffer。这样就能得到一个CPU和GPU均可访问的内存区域,在另一个进程中重复此操作即可实现共享。这套方案在Apple Silicon上可行,因为其采用UMA(统一内存架构),CPU和GPU共享同一物理地址空间。唯一的缺点是只能用于线性纹理,内存效率不高,但只要共享纹理数量不多,就不会有太大问题。
共享围栏实现
跨进程不同上下文共享纹理只是一方面,另一方面是同步问题。当生产者进程A向共享纹理绘制内容,消费者进程B将所有共享纹理合成为最终扫描输出图像时,如果A正在绘制而B开始合成,就会出现画面撕裂。为避免这种情况,需要使用围栏机制:A在B绘制时阻塞,B在A绘制时阻塞。更复杂的是,需要使用GPU围栏,因为GPU与CPU异步执行。理想情况下,B的GPU进程可以直接等待A的GPU围栏,无需A的CPU进程轮询,但要实现这一点必须使用MTLSharedEventHandle,而这又需要XPC。不过,我们可以借助两个事实实现模拟围栏:
- 大多数共享围栏事件发生在帧完成边界,因此CPU侧等待带来的额外延迟被限制为每帧一次。
- 围栏事件的生产者在GPU上执行,而消费者必须在CPU上等待,这意味着只有一侧会消耗CPU资源。
我们的模拟围栏工作流程如下:生产者调用ID3D11DeviceContext::ClearUnorderedAccessViewUint,向消费者进程映射的共享内存缓冲区中写入数据。该API可向共享内存写入任意整数,我们用它写入时间线值。由于写入操作由GPU执行,会与A的其他绘制调用同步,因此当GPU完成时间线值写入时,我们就知道所有绘制操作已完成。消费者必须在CPU上轮询共享内存(苹果GPU没有内存值轮询指令),一旦检测到时间线值更新,就知道绘制已完成,可以安全地使用纹理。消费者CPU必须等到围栏事件触发后才能提交绘制调用,因此可能会出现GPU空闲等待下一次提交的情况,从而引入延迟。
D3DMetal
macOS上最后一个DirectX API实现是苹果为游戏移植工具包(GPT)开发的D3DMetal.framework。它最初用于让开发者在Apple Silicon上测试Windows游戏,实现了基于Metal的D3D11和D3D12,还包含将DXBC/DXIL(微软专有GPU着色器字节码格式)转换为AIR(苹果专有GPU着色器字节码格式)的转译器。与DXMT类似,它也是为Wine设计的,同样不支持共享纹理和共享围栏,因此我们需要用同样的方式模拟这些功能。不同的是,它并非开源,我们需要通过swizzling和虚表补丁来拦截API调用并修改输出。
我们通过d3dmetal-native实现了这一点:它是D3DMetal.framework的包装器,使其能在Wine外运行,并支持上述额外功能。由于我们设计的API接口与DXMT兼容,可在virglrenderer中轻松切换两者。结果显示,其性能比DXMT有显著提升。

需要注意的是,D3DMetal仅提供x86_64版本(最初用于Rosetta + Wine),因此整个virgl_render_server进程必须在Rosetta下运行。即便如此,它的性能仍优于原生ARM64上运行的DXMT。
遗憾的是,D3DMetal的许可证条款明确禁止将其用于“开发、测试或评估Apple品牌产品适用的视频游戏”之外的用途,且只能“仅用于非商业目的”分发。这意味着我们不能将D3DMetal作为捆绑应用的一部分。有趣的是,商业Wine发行版CrossOver已捆绑D3DMetal,据了解他们与苹果达成了特殊协议。如果有人了解相关协议细节,请联系我们——我们很希望能将D3DMetal纳入UTM,以提升性能。
试用指南
本文所述所有工作均为开源项目,如果你喜欢折腾,可以尝试部署并反馈意见。我们正积极推进代码上游合并,不久后会更新UTM以支持这些功能,届时无需编译多个项目即可体验。
提示:可以让AI参考本文帮你完成部署。
代码
macOS 编译指南
以下是macOS专属编译步骤,Linux编译步骤与上一篇文章基本一致。
所有组件都会安装到同一个临时前缀目录,各组件通过该目录的pkgconfig目录相互查找,因此请先设置以下环境变量,并在整个编译过程中保持不变:
export SRC=/path/to/checkouts # git仓库存放路径
export PREFIX=/path/to/prefix # 临时安装根目录:包含bin/ lib/ libexec/ share/
export ANGLE_INC="$SRC/WebKit/Source/ThirdParty/ANGLE/include"
export ANGLE_LIB="$PREFIX/ANGLE.xcarchive/Products/usr/local/lib"
编译前需准备:Xcode(含Metal工具链)及命令行工具、Meson 1.3+、Ninja、pkg-config、CMake,以及LLVM 15(精确主版本号,需包含头文件和静态库,用于编译DXMT)。
ANGLE 和 libepoxy
QEMU的-display cocoa,gl=es路径和virglrenderer的GL后端均基于ANGLE-on-Metal,由WebKit代码库编译,再搭配一个调度到ANGLE的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
框架本身不会被捆绑:需从苹果游戏移植工具包中获取,并在运行时通过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动态库;-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_INC" \
-Dvenus=false \
-Dneptune=true \
-Dvtest=false \
-Drender-server-worker=process \
-Dcheck-gl-errors=false \
-Ddefault_library=static \
"--pkg-config-path=$PREFIX/lib/pkgconfig-x86_64" \
"--prefix=$PREFIX"
meson compile -C "$SRC/virglrenderer/build-x86_64"
仅需编译——安装此配置会覆盖步骤1中生成的原生库。
3. 合并两个切片
lipo -create \
"$SRC/virglrenderer/virgl_render_server.arm64" \
"$SRC/virglrenderer/build-x86_64/server/virgl_render_server" \
-output "$PREFIX/libexec/virgl_render_server"
lipo -archs "$PREFIX/libexec/virgl_render_server" # 输出应为:x86_64 arm64
两个后端都不会被静态链接:渲染服务器会在arm64切片中dlopen加载libdxmt-native.dylib,在x86_64切片中加载libd3dmetal-native.dylib,因此运行时这两个库必须在动态加载器的搜索路径中(详见运行指南)。它们也不是编译依赖项——缺少某个库只会导致对应后端无法运行,不会影响编译。
QEMU
编译时无需启用特定的Neptune选项——virglrenderer会通过pkg-config被自动检测到,因此只需将PKG_CONFIG_PATH指向刚刚安装的前缀目录:
git clone -b utm-edition https://github.com/utmapp/qemu.git "$SRC/qemu"
mkdir -p "$SRC/qemu/build" && cd "$SRC/qemu/build"
PKG_CONFIG_PATH="$PREFIX/lib/pkgconfig" ../configure \
"--extra-cflags=-I$ANGLE_INC" \
"--extra-ldflags=-L$ANGLE_LIB" \
"--prefix=$PREFIX" \
--target-list=aarch64-softmmu
make -j"$(getconf _NPROCESSORS_ONLN)" install
配置摘要应显示virglrenderer: YES和Cocoa: YES;如果virglrenderer未被检测到,后续的-device virtio-gpu-gl-pci参数会因设备不存在而报错。make install还会将UEFI固件安装到$PREFIX/share/qemu;请为VM从构建目录的pc-bios/edk2-arm-vars.fd复制一份可写的变量存储文件。
Windows驱动编译指南
需要Windows机器(或VM)来编译Windows驱动。完整编译说明见此处,也可使用build-mesa简化UMD的编译流程。如果只想测试驱动,我们提供了预编译的签名驱动。注意:这些驱动仍非常不稳定,请勿在重要VM上安装!
运行指南
需要Windows ARM64虚拟机镜像(可自行构建或使用UTM并复制磁盘镜像)。仅当使用桥接(vmnet)网络时需要sudo——使用用户模式网络时,QEMU无需特权即可运行。
export VM=/path/to/vm # 磁盘镜像 + EFI变量存储文件路径
export D3DMETAL=/path/to/D3DMetal.framework
D3DMETAL_FRAMEWORK_PATH="$D3DMETAL" \
DYLD_FALLBACK_LIBRARY_PATH="$PREFIX/lib:$ANGLE_LIB" \
ANGLE_DEFAULT_PLATFORM=metal \
VIRGL_LOG_LEVEL=debug \
"$PREFIX/bin/qemu-system-aarch64" \
-machine virt \
-accel hvf,ipa-granule-size=0x1000 \
-cpu host \
-smp cpus=4,sockets=1,cores=4,threads=1 \
-m 4096 \
-nodefaults \
-vga none \
-device virtio-ramfb-gl,hostmem=8G,blob=true,venus=true,neptune=true \
-display cocoa,gl=es \
-drive if=pflash,format=raw,unit=0,file.filename="$PREFIX/share/qemu/edk2-aarch64-code.fd",readonly=on \
-drive if=pflash,unit=1,file.filename="$VM/efi_vars.fd" \
-device nvme,drive=disk,serial=disk,bootindex=1 \
-drive if=none,media=disk,id=disk,file.filename="$VM/windows.qcow2",discard=unmap,detect-zeroes=unmap \
-device nec-usb-xhci,id=usb-bus \
-device usb-tablet,bus=usb-bus.0 \
-device usb-kbd,bus=usb-bus.0 \
-device virtio-net-pci,netdev=net0 \
-netdev user,id=net0,hostfwd=tcp::2222-:22
与图形相关的关键参数:
-device virtio-ramfb-gl,hostmem=8G,blob=true,venus=true,neptune=true:neptune=true会启用Neptune功能集(venus=true额外启用Venus以支持虚拟机Vulkan)。必须同时设置blob=true和hostmem窗口。-accel hvf,ipa-granule-size=0x1000:HVF以此粒度映射虚拟机内存。4KiB页面是Venus的必需配置,对Neptune是可选配置。-display cocoa,gl=es:在Triton的扫描输出功能启动前,Cocoa窗口通过ANGLE/Metal进行扫描输出。virtio-ramfb-gl是UTM fork版QEMU特有的设备,可为Windows提供POST显示,在安装驱动前即可使用。如果要移植到原生QEMU,可先使用ramfb,安装驱动后再改为virtio-gpu-gl-pci并重启。
环境变量说明
| 变量 | 作用 |
|---|---|
DYLD_FALLBACK_LIBRARY_PATH |
QEMU通过此路径查找libvirglrenderer,渲染服务器通过此路径查找libdxmt-native.dylib/libd3dmetal-native.dylib——两者均通过名称dlopen加载,因此前缀目录的lib必须在此路径中。 |
RENDER_SERVER_EXEC_PATH |
覆盖渲染服务器二进制文件路径;默认使用编译时固化的libexec路径。 |
NPT_BACKEND |
可选值为d3dmetal(默认)或dxmt。该变量决定Neptune工作进程使用通用渲染服务器的哪个切片:D3DMetal使用Rosetta下的x86_64切片,DXMT使用原生arm64切片。 |
D3DMETAL_FRAMEWORK_PATH |
libd3dmetal-native.dylib通过此路径查找D3DMetal.framework;未设置时,会在dylib所在目录及同级Frameworks目录中搜索。 |
NPT_D3D11_LIBRARY_PATH、NPT_DXGI_LIBRARY_PATH、NPT_D3D12_LIBRARY_PATH |
覆盖各D3D接口使用的后端dylib路径;可用于指向构建目录中的版本,而非已安装的版本。 |
NPT_WA_FLAGS |
位掩码,强制启用宿主机端的 workaround(着色器签名合成等)。设置NPT_WA_FLAGS=0可禁用所有 workaround,查看后端原始行为。 |
VIRGL_LOG_LEVEL/VIRGL_LOG_FILE |
宿主机渲染器日志配置。设置为debug可显示Neptune宿主机模块的npt:日志;默认输出到QEMU标准输出,也可指定日志文件路径。 |
DMN_LOG、DXMT_LOG_LEVEL |
分别控制d3dmetal-native和DXMT的后端日志。 |
VK_DRIVER_FILES |
MoltenVK ICD路径;仅当需要同时使用Venus(虚拟机Vulkan)和Triton时才需设置。 |