Triton:QEMU 的 DirectX 11 驱动程序
Triton: DirectX 11 Driver for QEMU

原始链接: https://blog.getutm.app/2026/introducing-triton-directx-11-driver-for-qemu/

本项目引入了 **Triton**——一款用于 QEMU 的新型 Windows 驱动程序,旨在为虚拟机提供完整的 DirectX 11 支持。Triton 基于前代 **Neptune** 协议(该协议可在虚拟机管理程序中实现 Direct3D API 调用的序列化),使 macOS 及其他平台上的 Windows 客户机能够获得现代图形加速支持。 与以往替换 Windows 系统 DLL 的方法(该方法存在性能低下、易被反作弊系统检测及不稳定等问题)不同,Triton 实现了 Windows 设备驱动程序接口(DDI)。通过将客户机的 DDI 调用转换回 DirectX API 调用,系统利用经过优化的 Neptune 协议与宿主机进行通信。 处理 DXBC 着色器字节码是一项关键挑战;Triton 通过重建宿主机渲染器所需的元数据,使字节码能够在不经修改的情况下通过。在 macOS 宿主机上,该系统通过集成 **DXMT** 或 **D3DMetal**(Apple 的游戏移植工具包)来提供硬件加速。通过对共享内存和仿真围栏(Fences)的精妙运用以处理进程隔离,系统确保了客户机与宿主机 GPU 之间的同步。这一进展代表了虚拟化游戏性能的重大飞跃,使 Windows 客户机能够有效地利用宿主机原生图形库。

Hacker News 最新 | 往日 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 Triton:QEMU 的 DirectX 11 驱动程序 (getutm.app) 18 分,electricant 发布于 2 小时前 | 隐藏 | 往日 | 收藏 | 讨论 | 帮助 考虑申请 YC 2026 年秋季批次!申请截止日期为 7 月 27 日。 社区准则 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

In the prequel, we introduced Neptune, a Direct3D protocol forwarding layer for VirtIO. Neptune allowed us to serialize Direct3D API calls across the hypervisor boundary and this allowed us to run Wine games on a Linux guest with a Linux host faster than with DXVK directly in the guest. Admittedly, the payoff there was not that exciting but it laid the groundwork for our real goal: modern graphics acceleration for Windows guests. We have now achieved this by building a brand new Windows driver called Triton which along with Neptune brings full DirectX 11 support to QEMU virtual machines.

Screenshot of game running on QEMU on macOS host
Crash Bandicoot Trilogy (x64) running on Windows 11 ARM64 virtualized on macOS through QEMU

You might be wondering: if Neptune can serialize Direct3D API calls and Windows uses Direct3D, then aren’t we already done? If Direct3D works in Wine, it should also work in Windows, right? After all, what is Wine, if not a Windows emulator? The short answer is: you sort of can. The Neptune Mesa drivers build a d3d11.dll and dxgi.dll which fully implements the Direct3D API set and so if you just put those files next to the game’s executable, it should load them instead of Windows’ own drivers and you can get some games to run that way. This is also the approach done by previous attempts which used DXVK→Vulkan→Venus to run Direct3D locally inside an application. There are a few disadvantages to this approach. First and most importantly, you cannot get good performance because the window compositor (DWM) “sees” your frame as an image so it needs to use CPU blitting to copy the GPU image buffer to the correct window location. You might be able to do some tricks for full-screen applications to scanout natively but you will never get a smooth desktop experience. Second, because d3d11.dll and dxgi.dll are core components of Windows, you cannot replace the system files themselves and expect Windows to still work. Even if you do manage to get it to work, you will not be able to play many games with anti-cheats which specifically detect this kind of modification. That is why it is only possible to get the DLL to load on a per-application basis (and compatibility varies). Which brings us to the last point: needing to copy files to every application you want working graphics acceleration is not a user friendly experience. The correct approach is not to implement the DirectX APIs but to implement the DirectX DDIs (Device Driver Interface).

DDI

User mode Application Direct3D 11 (d3d11.dll) User-mode driver (DDI) DXGI (dxgi.dll) Kernel mode Kernel-mode driver Hardware / Virtualization

In Windows, the application communicates with the system Direct3D and DXGI libraries. The d3d11.dll (as well as older versions) do the complicated work of state tracking and send a more sanitized stream of commands to the user-mode driver (UMD) which implements the DDI. The application also talks to dxgi.dll to initialize the graphics adapters, set up the swapchain, etc. The UMD also goes through the DXGI to talk with the kernel-mode driver (KMD). The KMD is implemented by the graphics vendor (us) to drive the actual hardware (or in our case the virtual hardware). For Wine, we implemented a custom d3d11.dll and dxgi.dll to intercept the API calls and now for Windows, we need to instead implement the UMD and KMD.

So that’s the challenge: implement the UMD with the DirectX DDI interface and also set up a private interface with the KMD which communicates with the VirtIO device.

Lucky for us, the second part has already been solved. Both anonymix007 and arehnman had independently been working on a KMD for Venus (Vulkan). Since Vulkan is a completely independent graphics API, it does not need to implement the DirectX DDI and its UMD is similar to the “replace d3d11.dll” approach in that it talks directly with the KMD to drive commands to QEMU. Since Neptune is modelled after Venus, the high level kernel interfaces (for DMA, command buffers, etc) is very similar and the interface between UMD and KMD is exactly the same. Ultimately, we chose to use anonymix007’s branch as the base because their implementation had more features working on the KMD side.

That leaves us with the hard part. We have to implement the DDI for DirectX 11. When you are designing a new system, it is always wise to understand how others who have come before have solved similar problems. Unfortunately, there are not many open source DDI implementations to draw inspiration from. Windows graphics drivers is a very niche subject and most of the experts work in one of the handful of graphics hardware vendors. This is one of the reasons that QEMU has never got far with Windows GPU acceleration.

Fortunately for us, there are two working open source implementation that we can learn from. First, Mesa has a DirectX 10 UMD. If you did not read the last article, the short version is that Mesa implements OpenGL for Linux. Mesa performs state tracking for OpenGL and emits Gallium API calls. The Mesa DirectX 10 UMD is an alternative to OpenGL that emits the same Gallium API calls. Then the Gallium backend driver (AMD, Intel, VirGL, etc) converts them into native graphics driver APIs. The upstream Mesa only supports the software rasterization backend for DirectX 10 but there was some recent work to get it working with VirGL. Unfortunately, the macOS virglrenderer lacks support for many of the features that this UMD requires so it is not a viable way to get graphics acceleration for macOS hosts. However, the integration with the Mesa codebase provides us with a clean example for integrating Triton.

VirtualBox has the only working open source DirectX 11 UMD. However, this driver cannot really be “adopted” for our use. The way their driver works is that they translate the DDI calls into an intermediate bytecode and then on the host side, they interpret the bytecode into DirectX API calls. While it would be easy (with AI help) to just port this bytecode emitter and interpreter into QEMU, we decided against it for a couple of reasons. First, we think this conversion of DDI into a bytecode and then lifting that bytecode back to DirectX API can result in bugs that limit compatibility with games. Indeed, reading forum threads online, it seems like many games do not run in VirtualBox for this reason. If there is a missing feature or bug in the translation engine, it would require a lot of active maintenance effort to fix and we do not want to depend on Oracle for that. Second, there is a licence incompatibility between VirtualBox’s GPLv3 and virglrenderer’s MIT License or QEMU’s LGPLv2. VirtualBox’s code can’t be integrated but we did learn some valuable insight from it. Their list of which DDI prototypes were implemented and which ones returned error is used as the minimal requirements for a working implementation. This information isn’t readily available from MSDN documentation and trying to implement every single prototype would be massive in scope. Their DXBC signature algorithm is also helpful to understand because Microsoft does not publish it anywhere.

Since we didn’t want to take the VirtualBox approach of using an intermediate transport format for DDI calls, we can do something better. If you imagine d3d11.dll as a component that roughly transforms DirectX API calls into UMD DDI calls, then what we want our UMD to do is to transform the DDI call back to DirectX API calls. Why is this useful? Because then we can use our tested and working Neptune protocol without having to invent a new transport for serializing DDI calls. On the host side, we do not have to do any extra work to execute those calls. VirtualBox needs an emitter and transport layer on the guest as well as an interpreter and dispatcher on the host. Each step adds latency and the chance to introduce errors and incompatibility. We still need an emitter and transport on the guest but on the host side we do not need an interpreter because the deserialized Neptune commands ARE DirectX 11 API calls and can be dispatched without any additional parsing. One less transform step means less opportunity for mistakes. Another advantage of a DDI→API transform is that most DDI calls in D3D11 have an API equivalent meaning that the transform is as simple as mapping some API handles to device handles and sometimes doing a lookup for API→DDI enum differences. The biggest win however, also turns out to be the most complicated part of the story, which is the DXBC shader code.

DXBC

DXBC (DirectX Byte Code) is the IR code that Microsoft’s shader compiler (FXC) emits. Specifically, it is the older (pre-DirectX 12) format and is compiled from HLSL, the shader language that DirectX uses. Since Triton acts as a reverse transform from DDI to API, it does not need to disassemble and convert this shader bytecode. This is a huge win for us in terms of complexity and compatibility. Unfortunately, it is not as simple as passing the bytecode unmodified to the host.

Application authors HLSL shader source HLSL source FXC HLSL → DXBC shader compiler emits DXContainer header + parts Header magic, version, part table SHDR DXBC bytecode ISGN input signature (metadata) OSGN output signature (metadata) other metadata parts ID3D11Device::CreateVertexShader passes the entire container d3d11.dll Direct3D 11 runtime pfnCreateVertexShader passes the SHDR part only UMD (Triton) user-mode driver

The compiler (FXC) emits the DXBC bytecode along with other metadata. d3d11.dll expects to see this metadata and consumes it. When the DDI is called, only the bytecode is passed. That means for us to make a valid “inverse transform” back to the API call, we need to re-construct all that metadata by interpreting the bytecode. In the end we still pass through the bytecode unmodified but since we don’t see the original DXContainer file, we have to synthesize fields that the host DirectX renderer expects. This was a lot of trial and error that the AI assistant handled but it is the weakest and most error prone part of our implementation.

Here’s what we have so far:

GUEST HOST guest → host boundary Application · DirectX / DXGI API calls System libraries · DDI calls into Triton Triton · DXBC → DXContainer → Neptune Neptune UMD · serialize into the ring buffer KMD · VirtIO commands to the host QEMU host · hands calls to virglrenderer Neptune host · deserialize → host DirectX Host DirectX · renders the frame 1 2 3 4 5 6 7 8
  1. Application makes DirectX and DXGI API calls to the system libraries.
  2. System libraries invoke Triton through DDI calls.
  3. Triton DDI converts raw DXBC bytecode back into DXContainer and makes DirectX and DXGI API calls to Neptune.
  4. Neptune UMD serializes the API calls and passes them through a ring buffer managed by the KMD.
  5. KMD uses the VirtIO interface to send commands to the host.
  6. QEMU host handles the command and passes Neptune calls to virglrenderer.
  7. Neptune host module in virglrenderer deserializes the API calls and forwards them to the host side DirectX implementation.
  8. Host DirectX implementation renders the frame.

Let’s zoom in on the last point. Once the DirectX API calls make it to the host, we still need to render it. When we brought up Neptune for Wine on Linux, we forked DXVK to support exporting the swapchain images as DMAbuf resources. At the time, we decided to implement the swapchain on the host side in order to sidestep the issue of shared textures. Internally, swapchain images are represented as textures but these textures are special in that the host needs to be able to find them and use them to show the final frame on screen. Our Wine DXGI library forwards all the swapchain API calls directly to the host and therefore the host “knows” which textures will be used as a backbuffer. Then separate VirGL commands can be used to scan out the texture blob to the VM window. This trades simplicity in the guest driver and minimal changes in DXVK for the complexity of swapchain logic in the host virglrenderer process.

When bringing up Triton, we realized that host side swapchain handling was a mistake. On Windows, DXGI is a system component which talks to the UMD. DXGI handles the backbuffer creation, frame pacing, mode switching, etc. The UMD (mostly) doesn’t give special treatment to DXGI and so our method of doing an “inverse transform” of DDI calls back to API calls does not really work with DXGI. That means all the swapchain logic we added to the host side is largely bypassed. The desktop compositor (DWM) operates on shared textures. One process’s DXGI renders content to its own backbuffer and that backbuffer is shared with the DWM process which draws the desktop, window chrome, etc. The final frame that DWM constructs is set for scan out. This means that in addition to DMAbuf exports, we also need to implement DMAbuf imports in DXVK as well (separate guest contexts map to separate host contexts). Once we have both import and export implemented, there is no need for host side swapchain logic anymore and so to make the Wine driver more unified, we moved all the swapchain logic into the guest Neptune driver. An added benefit of this move is that it more closely tracks with how Venus is designed and so virglrenderer is kept clean.

Screenshot of Windows running on QEMU on Ubuntu host
Windows DWM compositing working with DXVK shared textures on QEMU KVM running on Ubuntu

macOS

There’s some challenge in getting virglrenderer working on macOS but since Venus now runs in macOS, most of the backend challenges have been fleshed out. The remaining task is to connect Neptune to a host side DirectX renderer. There are three major projects that can handle the goal of DirectX on macOS. However, they all been designed with running Wine as the main target. They lack the shared textures and shared fences features that Neptune and Triton requires.

DXVK + MoltenVK

DXVK is the project we used on Linux hosts. It translates D3D11 API to Vulkan API and then uses the host Vulkan driver to do the rendering. On Linux, this works great because Vulkan is a first class citizen and all modern graphics hardware have a good Vulkan driver at this point. On macOS though, Vulkan is handled by another translation layer, MoltenVK, which translates Vulkan API to Metal API. In the last post, we talked about the unique challenge of getting DXVK + MoltenVK working and the short version is: it is unstable and requires a lot more work for compatibility.

Crash Bandicoot running on patched MoltenVK + Venus
Crash Bandicoot running on patched MoltenVK + Venus + DXVK (guest)

DXMT

DXMT sidesteps the Vulkan issue by translating D3D11 directly to Metal (with D3D12 coming soon). Just like DXVK, the project is designed primarily with Wine in mind so the first step was to implement a native variant of the library. With our fork, DXMT can be built as a macOS shared library and exposes some additional exports for import/export of textures and fences.

Screenshot of FireStrike result with DXMT backend
FireStrike (x64) for Windows 11 ARM64 running on macOS host with DXMT backend

Shared Textures

One major hurdle in the design of dxmt-native is in the implementation of shared textures that can cross the process boundary. We need to cross the process boundary because virglrenderer spawns helper processes for each renderer context so roughly every guest D3D context corresponds to a separate virgl_render_server process. This strict process isolation allows a renderer to crash without taking down the entire VM. Now, we can force the older thread based isolation model and use standard MTLTexture handles across renderer context boundary (and we will have to for the eventual iOS port), but upstream maintainers do not want to support this. Sharing Metal resources across processes can be tricky but there are a few “well supported” way of doing it.

  1. MTLSharedTextureHandle + XPC: This is the Apple preferred way but it requires bringing in XPC which is its own can of worms. Both QEMU and virglrenderer use file descriptors and SCM_RIGHTS to pass handles between processes but MTLSharedTextureHandle does not support this. For the best performance though, we should eventually adopt this across virglrenderer, QEMU, and SPICE but for this initial bringup, we want to not make major architectural changes across projects.
  2. IOSurface: You can render to an IOSurface and share the global handle with any other process. This is how we implement the accelerated rendering on UTM but it is using technology long deprecated by Apple. You also need to pay the penalty of an additional GPU blit into the IOSurface and that also means having to set up a render pipeline in virglrenderer which adds complexity.
  3. CALayerHost: A private API that is used by Chrome and other older macOS apps where rendering is done in a separate process. This is strictly worse than IOSurface in terms of latency (CoreAnimation is higher up the graphics stack) and the reverse (going from CALayer back to MTLTexture) is even more complicated. This technique may work for offline rendering but will not work for shared textures that must be composited.

None of these give us a good way to share textures across different processes using SCM_RIGHTS but a new idea was brought up (during the Venus discussion) by @Drakulix (who was working on bringing Wayland to macOS). Their idea is to use shm_open() to create a shared memory object (which can be represented as a file descriptor that works across SCM_RIGHTS) and then map it to a MTLBuffer using newBufferWithBytesNoCopy:length:options:deallocator:. That gives us a single memory region that can be seen by the CPU and GPU and you can repeat this in the other process as well. All of this works in Apple Silicon because of UMA (Unified Memory Architecture) meaning that CPU and GPU share a single physical address space. The only downside is that you can only do this with linear textures which is not memory efficient. However, as long as the number of shared textures is small, this should not be an issue.

Shared Fences

Sharing textures between different contexts in different processes is one half of the equation. The other half is synchronization. When you have producer process A drawing to a shared texture and consumer process B compositing all the shared textures into the final scanout image, you will run into tearing if A is in middle of drawing when B starts compositing. To prevent this, you need fences which allows process A to block while B is drawing and B to block while A is drawing. To make matters more complicated, you need GPU fences because the GPU executes asynchronously to the CPU. Ideally, B’s GPU process can consume A’s GPU fence without any polling by A’s CPU process. The only way to achieve this is to use MTLSharedEventHandle which requires XPC. However, we can get most of the way there with emulated fences by relying on two facts.

  1. Most of our shared fence events happen at frame completion boundaries. That means the added latency of a CPU side wait is limited to one fence per completed frame.
  2. The fence event producer can execute on the GPU while the fence consumer must wait on the CPU. That means we only need to waste CPU cycles on one side.
CPU GPU Producer process (A) Consumer process (B) Producer CPU 1 · submit draws + fence write ClearUnorderedAccessViewUint Consumer CPU 4 · spin-poll shared memory on the CPU — no GPU poll on Apple 5 · submit composite once seen Producer GPU 2 · execute all draws 3 · write timeline value ordered after the draws seeing it ⇒ draws are done Consumer GPU 6 · composite shared texture into the final scanout image safe — draws already done Shared memory UMA · mapped by both timeline value the fence shared texture rendered pixels

The way our emulated fence works is as follows: the producer calls ID3D11DeviceContext::ClearUnorderedAccessViewUint with an address in a shared memory buffer mapped by the consumer process. This API call lets us write an arbitrary integer to shared memory and we use it to write a timeline value. The write is done by the GPU so it is ordered with A’s other draw calls meaning that when the timeline value write occurs in the GPU, we know that all the draws are complete. The consumer must poll on the shared memory (this must be done on the CPU because there is no memory value polling instruction on Apple GPUs). Once it sees the updated timeline value, it knows the draws are complete and that it is safe to consume the texture. The consumer CPU cannot queue its draw calls until it sees the fence event and therefore there is introduced latency while the GPU is potentially idle waiting for the next submission.

D3DMetal

The last DirectX API implementation for macOS is made by Apple for the Game Porting Toolkit. Originally designed for developers to test their Windows game on Apple Silicon, the GPT includes D3DMetal.framework, an implementation of D3D11 and D3D12 on top of Metal as well as a transpiler from DXBC/DXIL (Microsoft’s proprietary GPU shader bytecode format) to AIR (Apple’s proprietary GPU shader bytecode format). Just like DXMT, it is designed to work with Wine. Just like DXMT, it does not support shared textures or shared fences so we need to emulate them in the same way. However, unlike DXMT, it is not open source so we need to use swizzling and vtable patching to intercept API calls and change the output.

That is what we have done with d3dmetal-native. It is a wrapper around D3DMetal.framework that allows it to work outside of Wine and support these additional features. Since we designed the API interface to be compatible with DXMT, we can easily switch between the two interfaces in virglrenderer. The result is a significant improvement in performance over DXMT.

Screenshot of FireStrike result with D3DMetal backend
FireStrike (x64) for Windows 11 ARM64 running on macOS host with D3DMetal backend (Rosetta)

Note that since D3DMetal only has an x86_64 slice (as it was intended for use with Rosetta + Wine), we have to run the entire virgl_render_server process in Rosetta. Even then it still outperforms DXMT running on native ARM64.

Unfortunately, D3DMetal’s licence terms explicitly prohibits usage outside of the “sole purpose of developing, testing, or evaluating video games for use on Apple-branded products” and that it can only be distributed “solely for non-commercial purposes.” That means we cannot include D3DMetal as part of a bundled application. Curiously, CrossOver, a commercial Wine distribution, does bundle D3DMetal and I was told that they have a special agreement with Apple in order to do this. If anyone is familiar with this arrangement, please contact us because we would love to include D3DMetal in UTM due to the improvement in performance.

All the work described here is open source so if you like tinkering, you can try it out and give us your feedback. We are actively working to upstream as much of these changes as possible and we will update UTM soon to support these features so anyone can try it without having to compile multiple projects.

Hint: Point your AI to this page and ask it to set it up for you.

Code

Building (macOS)

Everything installs into one staging prefix, and the pieces find each other through that prefix’s pkgconfig directory, so set these first and keep them for the whole session:

export SRC=/path/to/checkouts       # where the git repositories live
export PREFIX=/path/to/prefix       # staging install root: bin/ lib/ libexec/ share/
export ANGLE_INC="$SRC/WebKit/Source/ThirdParty/ANGLE/include"
export ANGLE_LIB="$PREFIX/ANGLE.xcarchive/Products/usr/local/lib"

You will need Xcode (with the Metal toolchain) and the command line tools, Meson 1.3+, Ninja, pkg-config, CMake, and an LLVM 15 installation (exact major version, with headers and static libraries) for DXMT.

ANGLE and libepoxy

QEMU’s -display cocoa,gl=es path and virglrenderer’s GL backend go through ANGLE-on-Metal, which the WebKit tree builds, plus a libepoxy that dispatches to it. This is unchanged from the Venus work but it is a prerequisite for everything else.

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 builds as a Wine cross build by default; passing no cross file gives the native build, which links every module into a single libdxmt-native.dylib exporting the D3D11/DXGI entry points plus the embedder API (Win32-style events and shared textures) that the Neptune render server uses.

export LLVM15=/path/to/llvm@15   # arm64 LLVM 15 install root
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’s llvm@15 works for $LLVM15; DXMT’s docs/DEVELOPMENT.md also documents building it from source.

d3dmetal-native

D3DMetal.framework ships as x86_64 only, so this library — and every process that loads it — must be x86_64. The cross file that selects -arch x86_64 is in the repository, and Rosetta 2 runs the results (including the test suite) transparently.

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

The framework itself is not bundled: get it from Apple’s Game Porting Toolkit and point the library at it at runtime with D3DMETAL_FRAMEWORK_PATH (or compile in a fallback with -Ddev_framework_path=...). If macOS quarantines it, xattr -dr com.apple.quarantine D3DMetal.framework.

virglrenderer

This is the interesting one, because a single QEMU process has to drive a native arm64 library and a render server that runs as either architecture. It is two Meson configurations of the same source tree:

  1. Native arm64 — produces both the libvirglrenderer that QEMU links and a render server hosting Venus and the Neptune DXMT backend. This is the only configuration that gets installed.
  2. x86_64 cross build — the same render server for the Neptune D3DMetal backend, which has to be x86_64 because the framework is. It links virglrenderer statically and is never installed.

The two servers are then fused with lipo into one universal binary. At runtime the parent process picks a worker’s slice per context — Venus contexts get the arm64 slice, Neptune contexts get x86_64/D3DMetal under Rosetta by default or arm64/DXMT with NPT_BACKEND=dxmt — so there is only one render server path to configure, and it is the one baked in at build time.

git clone -b macos-next https://github.com/utmapp/virglrenderer.git "$SRC/virglrenderer"

1. Native arm64 (library + render server).

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"

Stash the installed server, not the one in the build tree: only the installed copy carries the install_name‘d path to $PREFIX/lib/libvirglrenderer.1.dylib, and lipo is about to write over it.

2. Rosetta x86_64 render server. This one cross-compiles with the cross file from the d3dmetal-native repository. -Ddefault_library=static links virglrenderer into the server so the fused binary needs no x86_64 dylib, and -Dvtest=false drops the only target that pulls in GL — libepoxy in the prefix is arm64 only. For the same reason the x86_64 configuration must not try to use EGL, so give it its own pkg-config directory with EGL turned off:

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"

Compile only — installing this configuration would overwrite the native library from step 1.

3. Fuse the two slices.

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"   # must print: x86_64 arm64

Neither backend is linked: the render server dlopens libdxmt-native.dylib on the arm64 slice and libd3dmetal-native.dylib on the x86_64 slice, by name, so both must be reachable through the dynamic loader path at run time (see Running). Neither is a build dependency either — a missing one costs you that backend at run time and nothing more.

QEMU

Nothing Neptune-specific has to be enabled at configure time — virglrenderer is picked up through pkg-config, so point PKG_CONFIG_PATH at the prefix you just installed it into:

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

The configure summary should report virglrenderer: YES and Cocoa: YES; if virglrenderer is missing, the -device virtio-gpu-gl-pci line below will fail with an unknown device. make install also puts the UEFI firmware in $PREFIX/share/qemu; make the VM its own writable copy of the variable store from pc-bios/edk2-arm-vars.fd in the build directory.

Building (Windows drivers)

You need a Windows machine (or VM) to build the Windows drivers. Full instructions are here and you may want to use build-mesa for the UMD which simplifies the build process. If you just want to test the driver, we have pre-built signed drivers available. Note these drivers are still very unstable and you should not install them on any VM that you care about!

Running

You need a Windows ARM64 guest (you can find instructions to build an image elsewhere or you can use UTM and copy the disk image). sudo is only needed for bridged (vmnet) networking — with user networking QEMU runs fine unprivileged.

export VM=/path/to/vm                    # disk image + EFI variable store
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

The arguments that matter for graphics:

  • -device virtio-ramfb-gl,hostmem=8G,blob=true,venus=true,neptune=trueneptune=true advertises the Neptune capset (venus=true additionally advertises Venus for guest Vulkan). blob=true plus a hostmem window is required.
  • -accel hvf,ipa-granule-size=0x1000 — HVF maps guest memory at this granularity. The 4KiB pages are required by Venus and optional for Neptune.
  • -display cocoa,gl=es — the Cocoa window scans out through ANGLE/Metal until Triton’s scanout blob engages.
  • virtio-ramfb-gl is a device that only exists in the UTM fork of QEMU and presents Windows with a POST display that can be used before any drivers are installed. If you are porting this to vanilla QEMU, you can use ramfb, install the drivers, then change to virtio-gpu-gl-pci and reboot.

Environment variables

Variable Effect
DYLD_FALLBACK_LIBRARY_PATH How QEMU finds libvirglrenderer, and how the render server finds libdxmt-native.dylib / libd3dmetal-native.dylib — both are dlopened by plain name, so the prefix’s lib directory must be on this path.
RENDER_SERVER_EXEC_PATH Overrides the render server binary; defaults to the libexec path baked in at build time.
NPT_BACKEND d3dmetal (default) or dxmt. This selects which slice of the universal render server a Neptune worker is spawned as: x86_64 under Rosetta for D3DMetal, native arm64 for DXMT.
D3DMETAL_FRAMEWORK_PATH Where libd3dmetal-native.dylib looks for D3DMetal.framework. Without it, it searches next to the dylib and in a sibling Frameworks directory.
NPT_D3D11_LIBRARY_PATH, NPT_DXGI_LIBRARY_PATH, NPT_D3D12_LIBRARY_PATH Override the backend dylib loaded for each D3D interface. Useful to point at a build tree instead of the installed copy.
NPT_WA_FLAGS Bitmask forcing the host-side workaround set (shader signature synthesis and friends). NPT_WA_FLAGS=0 disables all of them to see raw backend behavior.
VIRGL_LOG_LEVEL / VIRGL_LOG_FILE Host renderer logging. debug is what surfaces the npt: lines from the Neptune host module; they go to QEMU’s stdout unless a log file is set.
DMN_LOG, DXMT_LOG_LEVEL Per-backend logging for d3dmetal-native and DXMT respectively.
VK_DRIVER_FILES MoltenVK ICD, only needed if you also want Venus (guest Vulkan) working alongside Triton.
联系我们 contact @ memedata.com