Triton: DirectX 11-driver voor QEMU

In een vorig artikel introduceerden we Neptune, een Direct3D-protocol doorstuurlaag voor VirtIO. Met Neptune konden we Direct3D API-aanroepen over de hypervisor-grens serialiseren, waardoor Wine-games op een Linux-gast met een Linux-host sneller draaiden dan met DXVK direct in de gast. Hoewel het resultaat daar nog beperkt was, legde het de basis voor ons uiteindelijke doel: moderne grafische versnelling voor Windows-gasten. Dit hebben we nu gerealiseerd door Triton te bouwen, een gloednieuwe Windows-driver die samen met Neptune volledige DirectX 11-ondersteuning brengt naar QEMU-virtuele machines.

Wat is Triton?

Men zou zich kunnen afvragen: als Neptune Direct3D API-aanroepen kan serialiseren en Windows Direct3D gebruikt, zijn we dan niet al klaar? Als Direct3D werkt in Wine, zou het immers ook moeten werken in Windows; Wine is immers in feite een Windows-emulator.

Het korte antwoord is: dat kan gedeeltelijk. De Neptune Mesa-drivers bouwen een d3d11.dll en dxgi.dll die de Direct3D API-set volledig implementeren. Als je deze bestanden naast het uitvoerbare bestand van een game plaatst, laadt de game deze in plaats van de eigen drivers van Windows, waardoor sommige games werken. Dit is ook de aanpak van eerdere pogingen die DXVK → Vulkan → Venus gebruikten om Direct3D lokaal binnen een applicatie te draaien.

Deze benadering heeft echter enkele nadelen:

  • Prestaties: De window compositor (DWM) ziet het frame als een afbeelding, waardoor CPU-blitting nodig is om de GPU-afbeeldingsbuffer naar de juiste vensterlocatie te kopiëren. Dit voorkomt een vloeiende desktopervaring.
  • Systeemintegriteit: Omdat d3d11.dll en dxgi.dll kerncomponenten van Windows zijn, kun je de systeembestanden zelf niet vervangen zonder dat Windows instabiel wordt.
  • Anti-cheat software: Veel games met anti-cheat detecteren dit soort modificaties, waardoor ze weigeren op te starten.
  • Gebruiksvriendelijkheid: Het is niet gebruiksvriendelijk om handmatig bestanden naar elke applicatie te moeten kopiëren.

De juiste aanpak is daarom niet het implementeren van de DirectX API's, maar het implementeren van de DirectX DDI's (Device Driver Interface).

De Device Driver Interface (DDI)

In Windows communiceert een applicatie met de systeembibliotheken van Direct3D en DXGI. De d3d11.dll voert het complexe werk uit voor state tracking en stuurt een geschoonde stroom commando's naar de User-Mode Driver (UMD), die de DDI implementeert. De applicatie communiceert ook met dxgi.dll om grafische adapters te initialiseren en de swapchain in te stellen. De UMD communiceert via DXGI met de Kernel-Mode Driver (KMD).

De KMD wordt geïmplementeerd door de hardwareleverancier (in ons geval wij) om de eigenlijke hardware (of virtuele hardware) aan te sturen. Waar we voor Wine een aangepaste d3d11.dll en dxgi.dll implementeerden om API-aanroepen te onderscheppen, moeten we voor Windows de UMD en KMD implementeren.

De uitdaging was dus: de UMD implementeren met de DirectX DDI-interface en een private interface opzetten met de KMD die communiceert met het VirtIO-apparaat. Gelukkig was het tweede deel al grotendeels opgelost; zowel anonymix007 als arehnman werkten onafhankelijk aan een KMD voor Venus (Vulkan). Omdat Neptune is gemodelleerd naar Venus, zijn de high-level kernelinterfaces (voor DMA, command buffers, etc.) zeer vergelijkbaar en is de interface tussen UMD en KMD identiek. We hebben uiteindelijk gekozen voor de branch van anonymix007 als basis vanwege de uitgebreidere functionaliteiten aan de KMD-zijde.

Ontwikkeling en Inspiratie

Het implementeren van de DDI voor DirectX 11 was het moeilijkste deel. Er zijn weinig open-source DDI-implementaties beschikbaar, aangezien Windows-grafische drivers een nicheonderwerp zijn waar vooral hardwareleveranciers in gespecialiseerd zijn. Dit is ook de reden waarom QEMU voorheen weinig succes had met Windows GPU-versnelling.

We hebben echter uit twee open-source projecten kunnen leren:

  1. Mesa: Mesa heeft een DirectX 10 UMD. Hoewel de macOS virglrenderer te veel functies mist om dit direct bruikbaar te maken voor macOS-hosts, bood de integratie in de Mesa-codebase een schoon voorbeeld voor het integreren van Triton.
  2. VirtualBox: Dit is de enige werkende open-source DirectX 11 UMD. VirtualBox vertaalt DDI-aanroepen naar een tussenliggende bytecode, die op de hostzijde wordt geïnterpreteerd als DirectX API-aanroepen. We hebben besloten deze methode niet te volgen vanwege:
  • Compatibiliteit: De vertaling naar bytecode en terug kan leiden tot bugs waardoor games niet werken.
  • Onderhoud: We wilden niet afhankelijk zijn van Oracle voor het oplossen van bugs in de vertaalengine.
  • Licenties: Er is een licentie-incompatibiliteit tussen GPLv3 (VirtualBox) en MIT (virglrenderer) of LGPLv2 (QEMU).

We hebben uit VirtualBox wel waardevolle informatie gehaald, zoals de lijst met DDI-prototypes die minimaal geïmplementeerd moeten zijn voor een werkende driver, en het DXBC-signatuur algoritme.

De gekozen aanpak: DDI → API

In plaats van een tussenliggend transportformaat, transformeren we in Triton de DDI-aanroepen direct terug naar DirectX API-aanroepen. Hierdoor kunnen we ons geteste Neptune-protocol gebruiken zonder een nieuw transport voor de serialisatie van DDI-aanroepen te hoeven verzinnen. Op de hostzijde zijn de gedeserialiseerde Neptune-commando's simpelweg DirectX 11 API-aanroepen die direct kunnen worden uitgevoerd, wat latentie vermindert en de kans op fouten verkleint.

DXBC (DirectX Byte Code)

DXBC is de IR-code die de shader-compiler van Microsoft (FXC) genereert vanuit HLSL. Omdat Triton een omgekeerde transformatie doet van DDI naar API, hoeven we deze bytecode niet te disassembleren of te converteren.

Het probleem is dat d3d11.dll metadata consumeert die door de compiler wordt meegeleverd in een DXContainer. Wanneer de DDI echter wordt aangeroepen, wordt alleen de bytecode doorgegeven. Om een geldige API-aanroep op de host te maken, moeten we deze metadata synthetiseren door de bytecode te interpreteren. Dit was een proces van vallen en opstaan, waarbij AI-assistentie hielp om de velden te reconstrueren die de host DirectX-renderer verwacht.

De Host-Renderer

Het volledige proces verloopt als volgt:

  1. Applicatie: Maakt DirectX en DXGI API-aanroepen naar systeembibliotheken.
  2. Systeembibliotheken: Roepen Triton aan via DDI-aanroepen.
  3. Triton: Converteert ruwe DXBC-bytecode terug naar DXContainer en maakt DirectX/DXGI API-aanroepen naar Neptune.
  4. Neptune UMD: Serialiseert de API-aanroepen in een ring buffer beheerd door de KMD.
  5. KMD: Gebruikt de VirtIO-interface om commando's naar de host te sturen.
  6. QEMU Host: Ontvangt het commando en geeft de Neptune-aanroepen door aan virglrenderer.
  7. Neptune Host (in virglrenderer): Deserialiseert de API-aanroepen en stuurt ze door naar de DirectX-implementatie van de host.
  8. Host DirectX: Rendert het frame.

Swapchain en DMAbuf

Bij de ontwikkeling van Neptune voor Wine op Linux hadden we de swapchain aan de hostzijde geïmplementeerd. Bij Triton ontdekten we dat dit een fout was. Op Windows regelt DXGI (een systeemcomponent) de backbuffer-creatie, frame pacing en mode switching.

De desktop compositor (DWM) werkt met gedeelde texturen: één proces rendert naar een backbuffer, die vervolgens wordt gedeeld met het DWM-proces dat het uiteindelijke beeld samenstelt. Om dit te ondersteunen, hebben we zowel DMAbuf exports als imports in DXVK geïmplementeerd. Hierdoor konden we alle swapchain-logica verplaatsen van de host naar de guest Neptune-driver, wat de architectuur schoner maakt en beter aansluit bij het ontwerp van Venus.

Implementatie op macOS

Het koppelen van Neptune aan een DirectX-renderer op macOS is uitdagend. Er zijn drie hoofdprojecten die dit kunnen:

  • DXVK + MoltenVK: Vertaalt D3D11 → Vulkan → Metal. Dit is op macOS echter onstabiel en vereist veel compatibiliteitswerk.
  • DXMT: Vertaalt D3D11 direct naar Metal. We hebben een native variant van deze bibliotheek gebouwd die texturen en fences kan importeren en exporteren.
  • D3DMetal (Apple Game Porting Toolkit): De implementatie van Apple voor D3D11/12 op Metal. Omdat dit niet open-source is, hebben we d3dmetal-native gebouwd: een wrapper die API-aanroepen onderschept via swizzling en vtable patching. Dit biedt aanzienlijk betere prestaties dan DXMT, hoewel het proces in Rosetta (x86_64) moet draaien. Let op: de licentievoorwaarden van D3DMetal verbieden commercieel gebruik buiten het testen van games.

Gedeelde Texturen

Een groot obstakel is het delen van texturen tussen verschillende processen, aangezien virglrenderer helperprocessen per renderer-context start. Op Apple Silicon (UMA - Unified Memory Architecture) hebben we dit opgelost door shm_open() te gebruiken om een shared memory object te creëren, dat vervolgens wordt gemapt naar een MTLBuffer. Dit werkt voor lineaire texturen en is efficiënt genoeg voor een klein aantal gedeelde texturen.

Gedeelde Fences (Synchronisatie)

Om tearing te voorkomen, zijn GPU-fences nodig zodat de consument (DWM) wacht tot de producent (de game) klaar is met tekenen. Omdat we geen directe toegang hebben tot MTLSharedEventHandle zonder XPC, gebruiken we emuleerde fences:

  1. De producent schrijft een tijdlijnwaarde naar ein gedeeld geheugenbuffer via ID3D11DeviceContext::ClearUnorderedAccessViewUint.
  2. Omdat deze schrijfactie door de GPU wordt gedaan, is deze geordend met de andere tekencommando's.
  3. De consument (CPU) poll t dit geheugenadres. Zodra de waarde is bijgewerkt, weet de CPU dat de GPU klaar is en kan het frame veilig worden geconsumeerd.

Zelf uitproberen

Al het beschreven werk is open-source. We werken momenteel aan het upstreamen van deze wijzigingen naar UTM.

Codebronnen

  • QEMU (utm-edition)
  • virglrenderer (macos-next)
  • DXMT
  • d3dmetal-native
  • Windows UMD, KMD en build scripts.

Bouwinstructies (macOS)

De build is specifiek voor macOS. Gebruik de volgende omgevingsvariabelen voor de gehele sessie:

export SRC=/pad/naar/checkouts       # Waar de git repositories staan
export PREFIX=/pad/naar/prefix       # Staging install root
export ANGLE_INC="$SRC/WebKit/Source/ThirdParty/ANGLE/include"
export ANGLE_LIB="$PREFIX/ANGLE.xcarchive/Products/usr/local/lib"

Vereisten: Xcode (met Metal toolchain), Meson 1.3+, Ninja, pkg-config, CMake en LLVM 15 (voor DXMT).

Stappenplan:

  1. ANGLE & libepoxy: Bouw ANGLE via de WebKit tree en installeer libepoxy met de macos-venus branch.
  2. DXMT: Bouw als native build (libdxmt-native.dylib). Vereist LLVM 15.
  3. d3dmetal-native: Bouw via de x8664 cross-file (omdat D3DMetal.framework alleen x8664 is). De framework zelf moet worden verkregen via Apple's Game Porting Toolkit.
  4. virglrenderer: Dit vereist een dubbele configuratie:
  • Een native arm64 build voor de bibliotheek en render server (DXMT/Venus).
  • Een x86_64 cross-build voor de D3DMetal render server.
  • Gebruik lipo om beide slices samen te voegen tot één universeel binair bestand.
  1. QEMU: Bouw de utm-edition met --target-list=aarch64-softmmu, waarbij virglrenderer via pkg-config wordt opgepikt.

Windows Drivers bouwen

Hiervoor is een Windows-machine of VM nodig. Er zijn pre-built signed drivers beschikbaar voor testen, maar deze zijn nog instabiel.

Uitvoeren

Je hebt een Windows ARM64-gast nodig. Gebruik het volgende commando (pas paden aan):

export VM=/pad/naar/vm                    # Disk image + EFI variable store
export D3DMETAL=/pad/naar/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

Belangrijke graphics-argumenten:

  • -device virtio-ramfb-gl,hostmem=8G,blob=true,venus=true,neptune=true: Activeert Neptune (en Venus voor Vulkan). blob=true en de hostmem window zijn vereist.
  • -accel hvf,ipa-granule-size=0x1000: De 4KiB pages zijn nodig voor Venus en optioneel voor Neptune.
  • -display cocoa,gl=es: Gebruikt ANGLE/Metal voor de initiële weergave tot Triton's scanout blob actief wordt.

Omgevingsvariabelen

VariabeleEffect
DYLDFALLBACKLIBRARY_PATHBepaalt hoe QEMU en de render server hun dylibs vinden.
RENDERSERVEREXEC_PATHOverschrijft het pad naar het render server binary.
NPT_BACKENDSelecteert tussen d3dmetal (standaard, x86_64 via Rosetta) of dxmt (native arm64).
D3DMETALFRAMEWORKPATHPad naar D3DMetal.framework voor de d3dmetal-native library.
NPTD3D11/DXGI/D3D12LIBRARY_PATHOverschrijft het geladen backend dylib per interface.
NPTWAFLAGSBitmask voor host-side workarounds (zoals shader signature synthesis).
VIRGLLOGLEVEL / VIRGLLOGFILELogging voor de host renderer (debug toont Neptune-lijnen).
DMNLOG, DXMTLOG_LEVELSpecifieke logging per backend.
VKDRIVERFILESNodig voor MoltenVK ICD indien Venus (guest Vulkan) wordt gebruikt.