Der Test, der dxcompiler.dll nicht finden konnte
Eine Runtime, die abstürzt, wenn eine optionale DLL fehlt, hat sich still auf eine bestimmte Art von Maschine beschränkt. Echte Portabilität bedeutet, überall zu booten und laut zu degradieren — und genau darauf setzen Partner ihre Deployments auf.
Samstagmorgen, CI-Dashboard, acht Tests im Rot. Alle acht scheiterten mit demselben Windows-Fehlercode: 0xc0000135. Jeder, der Zeit mit nativer Windows-Entwicklung verbracht hat, erkennt den sofort. STATUS_DLL_NOT_FOUND. Der Prozess versuchte, eine benötigte DLL zu laden, und die DLL war nicht auf dem System.
Die betreffende DLL war dxcompiler.dll, die Runtime des Microsoft-DirectX-Shader-Compilers. Die CI-Maschine hatte sie nicht. Produktions-Nutzermaschinen könnten sie ebenfalls nicht haben. Überall außerhalb der gut ausgestatteten Workstation eines Entwicklers hatte die Runtime still angenommen, dxcompiler.dll sei vorhanden, und stürzte hart ab, wenn sie es nicht war.
Bis zum Ende des Samstags waren drei Probleme gefixt und die Testsuite lag bei 89% bestanden, hoch von 71%. Dieser Post handelt von jedem einzelnen.
Warum die Abhängigkeit von Anfang an hart war
Der Shader-Cross-Compilation-Pfad nutzt Microsofts DXC-Compiler, um HLSL-Shader-Quellcode zu nehmen und SPIR-V- oder DXIL-Bytecode zu erzeugen. Die meisten ausgelieferten Shader der Engine sind vorkompiliert. Ein paar Codepfade in der Runtime können zur Laufzeit neue Shader kompilieren: Shader-Permutations-Hot-Reload während der Entwicklung, dynamisches Material-Editing im Editor und ein paar Debug-Pfade.
Die ursprüngliche Implementierung linkte gegen dxcompiler.lib mit einer #pragma comment(lib, "dxcompiler.lib")-Direktive im Shader-Cross-Quellcode. Das erzeugte eine harte Ladezeit-Abhängigkeit. Der Windows-Loader löst Ladezeit-Abhängigkeiten auf, wenn der Prozess startet; wenn die DLL fehlt, erreicht der Prozess niemals main(). Die Runtime-Executable konnte ohne die DLL auf dem System nicht starten.
Das ist die richtige Antwort für einen Entwickler, der das vollständige DXC-SDK installiert hat. Es ist die falsche Antwort für einen CI-Runner, eine Nutzermaschine oder überall sonst, wo DXC nicht vorhanden ist. Die Runtime sollte booten. Das Hot-Reload-Feature sollte „nicht verfügbar” melden. Alles andere sollte weiterlaufen.
Wie der Fix aussah
Drei Änderungen, jede klein, jede präzise.
Der Shader-Cross-Quellcode wanderte von Ladezeit- zu Laufzeit-DLL-Laden. Die #pragma comment(lib, ...)-Direktive wurde durch explizite LoadLibrary- und GetProcAddress-Aufrufe ersetzt. Die DLL wird bei der ersten Verwendung nachgeschlagen, nicht beim Prozessstart. Wenn LoadLibrary fehlschlägt, gibt der Codepfad einen klaren Fehler zurück.
Fallback-Pfade wurden hinzugefügt für den Fall, dass DXC fehlt. Wenn dxcompiler.dll nicht vorhanden ist, fällt das Shader-Cross-Subsystem auf einen von zwei Pfaden zurück. Falls das ältere D3DCompiler verfügbar ist, fällt es darauf zurück mit reduzierter Funktionalität (keine SPIR-V-Ausgabe, nur DXIL). Falls sogar D3DCompiler fehlt, fällt es auf einen Platzhalter-SPIR-V-Blob zurück, der einen laut-magentafarbenen Fragment-Shader erzeugt. Der Platzhalter stellt sicher, dass die Runtime Ende-zu-Ende weiterläuft in Umgebungen, in denen kein Shader-Compiler verfügbar ist; das visuelle Signal macht offensichtlich, dass man im Platzhaltermodus läuft.
Der graziöse Fehlschlagpfad loggt und sendet Telemetrie. Jeder Fallback wird auf WARNING-Ebene geloggt, mit dem konkreten Subsystem, das zurückgefallen ist, und dem Grund. Telemetrie zeichnet den Fallback für Ops-Sichtbarkeit auf. Ein Entwickler, der lokal ohne DXC arbeitet, sieht die Warnung in seiner Konsole und weiß, dass er DXC installieren muss, falls er echte Shader-Kompilierung braucht. Ein Nutzer, der die Runtime mit der Annahme betreibt, dass alles vorkompiliert ist, sieht überhaupt keine Warnung, weil die vorkompilierten Shader auch ohne DXC einwandfrei funktionieren.
Das ist die richtige Form für die Abhängigkeit. Die Runtime ist portabel. Die Fähigkeit ist opt-in, basierend darauf, was installiert ist. Der Fehlermodus ist beobachtbar.
Zwei angrenzende Bugs, die aus demselben Audit herausfielen
Während ich mit offenem Deckel im Shader-Lade-Code war, scheiterten zwei weitere Tests auf ähnliche Weise, und ich habe sie im selben Durchgang gefixt.
ABI-Mismatch bei Audio-Bus-Effekten. Die C-API-Funktionen raku_audio_bus_add_effect und raku_audio_bus_remove_effect waren mit einer (handle, struct*)-Signatur geschrieben worden, aber die Testsuite rief sie mit (handle, handle) auf, weil das der Rest der Audio-API so verwendete. Die Tests segfaulteten, weil die Struct-Zeiger-Dereferenzierung las, was auch immer sich an der Adresse befand, die den Bits des zweiten Handles ähnelte. ABI-Mismatch.
Der Fix war, die C-API an den Rest des Audio-Moduls anzupassen: (handle, handle). Das ist die richtige Form für die API, weil Effektketten im Audio-Bus erstklassige Objekte sind, die die Runtime verfolgt. Die Struct-Zeiger-Version war ein Überbleibsel eines früheren API-Designs, das den Rest des Refactorings nicht überlebt hatte. Die Testsuite lag richtig; die Implementierung war veraltet.
Speicherleck-Test-Stubs überschreiben echte Implementierungen. Das Asset-Streaming-Subsystem hat einen Speicherleck-Test, der Referenzzählung bei gestreamten Assets prüft. Der Test scheiterte, weil eine Stub-Implementierung von AssetStreamingManager in der Testfixture liegen geblieben war und die echte Implementierung der Runtime-DLL überschrieb. Der Test prüfte den Stub, nicht den echten Code. Der Stub hatte ein Speicherleck. Die echte Implementierung nicht. Der Test hatte recht damit, dass es ein Leck gab; er lag falsch damit, wessen Leck es war.
Der Fix war, den Stub aus der Testfixture zu entfernen und RAKU_STREAMING_API-Exports zur echten Implementierung hinzuzufügen, damit der Test sauber dagegen linken konnte. Einmal korrekt gelinkt, bestand der Test gegen den echten Code.
Was die drei Fixes zusammen freigeschaltet haben
Acht Tests wurden durch den dxcompiler-Fix grün. Zwei durch den Audio-Bus-Fix. Drei durch den Speicherleck-Fix. Die Gesamtzahl bestandener Tests stieg von 39/55 auf 49/55. Die 89%-Zahl ist der richtige Meilenstein, um ihn zu feiern, aber der tiefere Meilenstein ist, dass die Runtime jetzt ohne DXC funktioniert, was bedeutet, dass sie in Umgebungen funktionieren wird, die ich noch nicht vorhergesehen habe.
Was ich gelernt habe
Drei Dinge.
Harte Ladezeit-Abhängigkeiten sind ein Portabilitätsdefekt. Überall dort, wo deine Runtime eine Ladezeit-Abhängigkeit auf eine DLL oder ein Shared Object hat, das nicht universell vorhanden ist, hat sich die Runtime auf Umgebungen beschränkt, die diese DLL enthalten. Diese Beschränkung ist in Ordnung, wenn sie absichtlich gemacht wird. Es ist schlecht, sie unabsichtlich zu machen. Das Audit von „was benötigt unsere Runtime zur Ladezeit” lohnt sich.
0xc0000135 ist der häufigste Windows-DLL-Fehlercode, und die Meldung verrät nichts. Der Fehler sagt dir, dass eine DLL fehlt. Er sagt dir nicht, welche. Die Diagnosewerkzeuge, um das herauszufinden (Process Monitor, Dependencies.exe, das neue Windows-ETW-Tracing), funktionieren alle, aber sie erfordern alle, dass man weiß, dass sie existieren, und sie einrichtet, bevor der Fehler passiert. Die Runtime gibt jetzt eine klare Fehlermeldung aus, wenn ein graziöser Fallback auslöst, und benennt die fehlende DLL. Der zukünftige Ich wird dankbar sein.
ABI-Mismatches zwischen Tests und Implementierungen liegen meist auf der Testseite richtig. Die Tests waren gegen die API geschrieben, die der Rest der Codebasis offenlegte. Wenn die Tests der Implementierung widersprachen, hatten die Tests recht. Das ist das Gegenteil des üblichen Instinkts, der „den Test fixen” ist. Der richtige Instinkt ist, sich die breitere API-Oberfläche anzusehen und zu fragen, welche Seite der Ausreißer ist.
Was Partner und Entwickler daraus mitnehmen sollten
Wenn du eine Engine für eine Partnerschaft evaluierst und in Umgebungen deployst, in denen die Maschine des Entwicklers und die Produktionsumgebung unterschiedlich sind (was praktisch jedes Deployment ist), frag das Team nach seinen harten Ladezeit-Abhängigkeiten. Die richtige Antwort ist eine kurze Liste, alles gerechtfertigt. Die falsche Antwort ist eine lange Liste, von der die Hälfte das Team vergessen hat.
Wenn du ein nativer Windows-Entwickler bist und noch nicht LoadLibrary plus GetProcAddress für jede DLL verwendest, die nicht universell vorhanden ist, ist das der sanfte Anstoß. Das Muster ist klein. Der Portabilitätsgewinn ist groß.
Wenn du ein KI-Labor bist, dessen Coding-Agent Windows-Code schreibt: Die Muster, auf die man bei Agenten-geschriebenem Code achten sollte, sind #pragma comment(lib, ...)-Direktiven, die Laufzeit-Loads sein sollten, und ABI-Signaturen, die vom Rest der Codebasis abweichen. Beides wird von statischer Analyse markiert. Beides sollte auf der Review-Checkliste des Agenten stehen.
Abschluss des Wochenendes. Die Runtime bootet sauber auf Maschinen ohne DXC. Elf Tests wurden in zwei Tagen grün. Das nächste Audit steht im Kalender.
Zurück ans Bauen.
Eine Runtime, die läuft, wo deine Hardware lebt
RakuAI ist die spatiale Runtime, gebaut für Smart Glasses und die unordentliche reale Welt — portabel, graziös bei fehlenden Abhängigkeiten, beobachtbar beim Zurückfallen. Sieh, was es braucht, um überall zu deployen.