Ik kwam, ik promptte, ik vertrok Deel 2: Een GPU-driver vanaf nul bouwen in één maand

Wat we hebben gedaan

TL;DR: Niklas en ik hebben in ongeveer een maand een volledig OpenGL ES 3.0-compatibele GPU-driver gebouwd voor de M4 Mac Mini en MacBook Neo, een proces dat normaal gesproken jaren duurt. Chrome en Firefox draaien inmiddels WebGL op de M4 Mac Mini met werkende compositing. Belangrijker nog: de driver is snel genoeg om Minecraft op 200fps te draaien.

Het bouwen van deze driver vereiste het reverse engineeren van de ongelooflijk complexe firmware ABI (Application Binary Interface) en de user-space componenten van de AGX (Apple's naam voor de GPU). Dit is volledig uitgevoerd op een transparante, verifieerbare 'clean room'-wijze met gebruik van gevestigde technieken. De code is nog niet klaar voor eindgebruikers, maar we streven ernaar deze zo snel mogelijk beschikbaar te stellen.

Hoe we het hebben aangepakt

Voorheen heb ik een hypervisor gebouwd om macOS te reverse engineeren. Het doel werd toen om hier iets nuttigs mee te doen, en wat is daarvoor een beter doel dan het schrijven van een GPU-driver? Een GPU is essentieel voor elk modern systeem; anders moet alles via de CPU worden gerenderd, wat orders van grootte langzamer en minder energie-efficiënt is. Ons doel was om conformante OpenGL-drivers (en binnenkort Vulkan) te implementeren voor de M4 Mac Mini en MacBook Neo.

Normaal gesproken duurt het bouwen van een GPU-driver jaren; ons doel was om het in dagen te doen. Dat bleek overly optimistisch, maar weken is nog steeds een enorme verbetering. In die weken hebben we het volgende gedaan:

  • Het reverse engineeren van de user space van de M4, A18 Pro en (grotendeels) M5 via live probing, waarbij we hardware-ondersteunde functies en instructies ontdekten die niet door de driver van Apple worden gebruikt.
  • Het bouwen van een volledig werkende user-space driver, inclusief een nieuwe aangepaste IR/shader-compiler, command stream builder en diverse andere componenten.
  • Het vanaf nul reverse engineeren van de volledige AGX firmware ABI met behulp van traces van de hypervisor die ik eerder had gebouwd.
  • Het implementeren van een volledige Linux kernel driver voor deze firmware ABI.

Gedurende dit proces hebben we geen Apple-binaries bekeken, alleen hardware-traces (van onze hypervisor) en shaders die we zelf hebben gebouwd. Voor de user-space graphics RE (reverse engineering) waren we zorgvuldig om vereiste Apple-blobs als ondoorzichtige (opaque) objecten te behandelen. Een vriend schreef documentatie over deze blobs, zodat we zelf een clean room-implementatie konden schrijven (die grotendeels is gebouwd door simpelweg dingen te proberen tot ze werkten). We hebben al onze experimenten gepubliceerd, zodat iedereen de herkomst van ons werk kan verifiëren.

Dit artikel is verdeeld in twee delen: user space en kernel space. Dit weerspiegelt de splitsing in alle moderne GPU-drivers: de kernel is verantwoordelijk voor de interface met de firmware, het alloceren van buffers en het beheren van scheduling, terwijl de inhoud van die buffers en wat er wordt gescheduled ondoorzichtig blijft. De user space is verantwoordelijk voor het daadwerkelijke begrip van hoe de GPU werkt en het vullen van die buffers.

Kernel Space

Op Apple Silicon communiceert de kernel driver niet direct met de hardware. In plaats daarvan communiceert deze met de GPU-firmware die een custom RTOS genaamd RTKit draait. Dat betekent dat de eerste stap naar een kernel driver niet het praten met hardware is, maar het uitzoeken van de firmware ABI.

De firmware ABI was veruit het meest frustrerende deel van dit project. In plaats van een redelijke ABI met nette interfaces te ontwerpen, heeft Apple in feite een reguliere kernel driver doormidden gesneden: de ene helft zit in de AGX (de firmware) en de andere helft is de kernel driver, die communiceert via gedeelde structs in het geheugen. Veel van deze structs bevatten velden die eigendom zijn van de firmware (die we nooit mogen wijzigen en via reverse engineering moeten begrijpen), afgewisseld met velden die door de host worden aangestuurd.

Ter illustratie van de complexiteit: de A18 Pro firmware ABI is significant complexer dan de (al zeer ingewikkelde) M1 firmware ABI. De A18 heeft:

  • 1,5x zoveel structs.
  • twee keer zoveel pointers.
  • een aanzienlijk complexer proces voor het indienen van werk.

Mijn aanpak was simpel en gebaseerd op de methode die succesvol werd gebruikt voor de M1/M2 machines: kijken wat macOS deed, dit reproduceren (replay), en vervolgens proberen het zelf te doen. Dit werd mogelijk gemaakt door de hypervisor.

Wanneer ik deze aanpak beschreef aan het LLM (Codex), nam het 'replay' zeer letterlijk: het wachtte op het eerste firmware-zichtbare event (een "kick"), maakte een kopie van de volledige GPU-geheugenstatus en probeerde na een reboot deze status terug te kopiëren naar het host-geheugen. Codex probeerde vervolgens deze objecten in code te reconstrueren door alle pointers te volgen. In opeenvolgende experimenten verminderde Codex het aantal gekopieerde pagina's totdat er geen gereproduceerde status meer was en alles vanuit de bron was gebouwd.

Er waren drie grote problemen, alle veroorzaakt door het onvermogen om een schone capture van het host-werk te krijgen:

  1. Render-werk na firmwarestart: Werk dat was klaargezet vóór de firmwarestart werkte, maar zodra de firmware draaide, werd nieuw werk alleen maar bevestigd (ACKed) en afgesloten zonder dat er iets gebeurde. Toen ik Codex instrueerde om een capture te nemen veel eerder in de levenscyclus van de AGX, ontdekte het LLM bijna direct dat er een enkele byte descriptor ontbrak.
  2. Compute-werk: De AGX ondersteunt render- en compute-werk. In de reguliere GUI-route wordt compute-werk pas gepland na een grote hoeveelheid render-werk. Hierdoor was het onmogelijk om een schone capture van een compute-workload te krijgen. De oplossing kwam uit een andere Codex-sessie: de GUI uitschakelen door in single-user mode te booten, een LaunchDaemon installeren die draait zodra Metal beschikbaar is, en een klein Metal-programma draaien om een pure compute-trace te capturen. Binnen enkele dagen had Codex compute werkend.
  3. Partial renders: Dit gebeurt wanneer de Tiled Vertex Buffer (TVB) niet groot genoeg is om de huidige geometrie op te slaan. De driver moet dan ofwel de TVB vergroten, of een partial render uitvoeren (een deel renderen, de buffer herladen en verder gaan). Dit was zeer complex omdat het in feite 'save and resume' toevoegt aan de GPU-driver. Codex kon dit oplossen door één partial render transactie te reproduceren en vervolgens een Metal-shader aan te passen om meerdere partial renders te triggeren, om zo het proces te leren.

De Kernel-driver bouwen

De overgang van een Python-prototype naar een volledige Linux-driver duurde drie dagen. Het proces was als volgt:

  • De bestaande drm-shim herschrijven in Rust volgens hetzelfde patroon, wat resulteerde in een synchrone Rust-driver.
  • De frontend herschrijven om asynchroon te zijn, terwijl de eigenlijke GPU-submission synchroon bleef.
  • De GPU-submissions refactoren naar asynchroon; in plaats van polling te gebruiken, luistert de driver nu naar firmware-events en koppelt werk aan een 'fence'.
  • Basisoptimalisaties implementeren, zoals batched work submission.

Opvallend was dat Codex agressief de hypervisor gebruikte om te debuggen waarom code niet werkte, inclusief het capturen van de volledige adresruimte en het vergelijken hiervan met bekende goede samples.

User Space

De A18 Pro user space verschilt sterk van de M1/M2; hij heeft nieuwe descriptorformaten, een nieuwe ISA en andere wijzigingen. Het is in feite een andere GPU.

Het reverse engineeren van de user space volgt een vast proces: schrijf een klein Metal-programma, compileer het, draai het, kijk wat er verandert, en begin met het aanpassen van de bits tot het begrip volledig is. Dit is repetitief werk waar LLM's zeer goed in zijn.

De RE-werkzaamheden verliepen in twee fasen. In de eerste fase liet ik Claude kijken naar alle mogelijke Metal-programma's om een disassembler en assembler te bouwen. Hoewel Claude programma's kon disassembleren en reassembleren, kon het niet zelfstandig nieuwe programma's compileren.

In de tweede fase sloeg Niklas zich aan. We gebruikten twee verschillende benaderingen:

  • Mijn aanpak (Hardware-first): Prioriteit geven aan hardware RE en begrijpen hoe alle instructies werkten. Daarna een specificatie schrijven die het LLM kon implementeren. Dit kostte veel tijd aan kleine, inconsequente taken.
  • Niklas' aanpak (Mesa-first): Eerst Mesa bouwen en alleen RE doen wanneer dat nodig was voor specifieke functionaliteit.

Niklas boekte veel snellere progressie omdat zijn agent werd gestuurd door de noodzaak om daadwerkelijk Mesa te bouwen, in plaats van vast te lopen in pedante details.

Tijdens het RE-proces vonden we hardware-ondersteuning die niet door Metal wordt gebruikt, zoals:

  • Een native single-instruction 64-bit add.
  • Anisotropy tot 128x (Metal stopt bij 16x).
  • Een nieuwe modus van de matrix unit.
  • 7-bit immediate support voor uniform_mov.

Mesa-ontwikkeling

Het bouwen van user-space graphics wordt enorm vergemakkelijkt door de Khronos compatibility test suite (CTS), een uitputtende verzameling tests waar de driver aan moet voldoen.

Daarnaast biedt Mesa geweldige abstracties. We hoeven alleen maar te vertalen tussen Gallium (de interne API van Mesa) en de AGX-hardwaresemantiek. Een groot deel hiervan is het vertalen van NIR (Mesa's interne IR) naar de propriëtaire ISA van de AGX. Deze compiler kan later ook voor een Vulkan-driver worden gebruikt.

Niklas kon zo iteratief door de OpenGL-functies gaan tot volledige OpenGL ES 3.0-compliantie was bereikt. We profiteerden hierbij van het bestaande M1/M2-werk; hoewel de hardwaresemantiek verschilt, blijft de algemene structuur vergelijkbaar.

Resultaten

Wat er nog moet gebeuren

Vulkan 1.4, OpenGL 4.6, OpenGL ES 3.2, OpenCL 3.1, Direct3D 12 (via Proton) en ray tracing staan op de planning. We willen dat onze driver net zo goed is als de beste graphics-drivers ter wereld.

Daarnaast willen we dit alles 'upstreamen' naar de officiële projecten. Er zijn echter obstakels: de code heeft meer menselijke review en refactoring nodig. We verwachten ook scepsis, aangezien dit waarschijnlijk de eerste volledig door LLM's geschreven GPU-driver is, waardoor de code waarschijnlijk aan een hogere standaard zal worden getoetst dan menselijk geschreven code.

Voor de Linux kernel driver is het probleem groter, aangezien de M1/M2-driver zelf nog niet is ge-upstreamd. We wachten waarschijnlijk tot die driver eerst is geaccepteerd voordat we onze driver indienen.

Wanneer kan ik het gebruiken?

Geduld, jonge grasshopper. We kijken ernaar uit om deze code binnenkort in jullie handen te krijgen.

Sluit je bij ons aan

Als je hier deel van wilt uitmaken, nodigen we je uit om onze Discord-server te bezoeken om ideeën te bespreken of gewoon te kletsen.

Addendum: Echt waar, Sam?

Ik gebruikte Codex met GPT-5.6 Sol (later GPT-6 Astra) voor de kernel RE-taak. Een van de grootste problemen waren de agressieve cybersecurity-restricties. In 99% van de gevallen was een simpele /goal resume of "ga door" voldoende, maar het verbrak mijn onbemande workflow.

Ik codeerde de simpelste oplossing: een daemon die elke minuut een screenshot maakt, deze vergelijkt met de vorige, en als ze identiek zijn (omdat Codex stopte met progressie) automatisch /goal resume typt. Ik liet dit per ongeluk aanstaan in sommige groepschats. Desondanks zijn GPT-6 Astra en GPT-5.6 Sol absoluut krankzinnig en de beste performers voor firmware ABI RE.

Addendum: M4 vs A18 Pro vs M5

Voor zover ik kan zien, zijn de user-space implementaties van de M4 en A18 Pro nagenoeg identiek, op een paar waarden na die op de M4 groter zijn (wat logisch is gezien de meer cores). De firmware ABI's verschillen echter significant: de tweede RTKit-coprocessor op de A18 Pro maakt alles complexer. In die zin lijkt de A18 Pro qua firmware meer op de M5 dan op de M4.

De user space van de M5 heeft overeenkomsten met de M4 (de ISA is grotendeels een superset), maar sommige delen, zoals de texture descriptors, zijn compleet anders. De user space van de M5 is gedeeltelijk gereversed engineered, de firmware ABI is volledig bekend en er is een prototype drm-shim gebouwd. Mijn primaire targets blijven echter de M4 Mac Mini en MacBook Neo.

***

Voetnoten

  1. Metal Helper Programs: Ik weet niet waarom Claude's header beweert dat dit niet 'clean room' is; ik denk dat het in de war raakte door de instructies om niet naar Apple-binaries te kijken. De documentatie zelf bevat geen besmette informatie.
  2. Lijst van complicaties:
  • Als we de firmware lieten crashen, was een volledige reboot de enige oplossing.
  • Bij fouten in de work-handshake werd er geen fout gemeld; het werk werd ACKed maar nooit voltooid.
  • Sommige problemen zorgden ervoor dat de output veranderde zoals verwacht, maar ook voor willekeurige corruptie op pagina's die niet aangeraakt hadden mogen worden.
  1. https://github.com/mischa85/apple-gpu-firmware-abi
  2. Als we een pagina vonden die een shader bevatte, gooiden we deze weg en vervingen we deze door onze eigen shader als extra copyright-maatregel.