Le test qui n'arrivait pas à trouver dxcompiler.dll
Un runtime qui plante quand une DLL optionnelle est manquante s'est silencieusement confiné à un seul type de machine. La vraie portabilité signifie démarrer partout et se dégrader bruyamment — et c'est ce sur quoi les partenaires déploient.
Samedi matin, tableau de bord CI, huit tests au rouge. Les huit ont échoué avec le même code d’erreur Windows : 0xc0000135. Quiconque a passé du temps sur le développement natif Windows le reconnaît immédiatement. STATUS_DLL_NOT_FOUND. Le processus a essayé de charger une DLL dont il avait besoin et la DLL n’était pas sur le système.
La DLL en question était dxcompiler.dll, le runtime du compilateur de shaders DirectX de Microsoft. La machine CI ne l’avait pas. Les machines utilisateur de production pourraient ne pas l’avoir. Partout en dehors d’une station de travail de développeur bien équipée, le runtime avait silencieusement supposé que dxcompiler.dll était présent et plantait durement quand ce n’était pas le cas.
À la fin du samedi, trois problèmes étaient corrigés et la suite de tests était à 89 % de réussite, en hausse depuis 71 %. Voici le billet sur chacun d’eux.
Pourquoi la dépendance était dure au départ
Le chemin de cross-compilation de shaders utilise le compilateur DXC de Microsoft pour prendre une source de shader HLSL et produire du bytecode SPIR-V ou DXIL. La plupart des shaders livrés du moteur sont précompilés. Quelques chemins de code dans le runtime peuvent compiler de nouveaux shaders au runtime : le rechargement à chaud de permutations de shaders pendant le développement, l’édition dynamique de matériaux dans l’éditeur, et quelques chemins de débogage.
L’implémentation originale se liait contre dxcompiler.lib avec une directive #pragma comment(lib, "dxcompiler.lib") dans la source de cross-shader. Cela produisait une dépendance dure au chargement. Le chargeur Windows résout les dépendances au chargement quand le processus démarre ; si la DLL est manquante, le processus n’atteint jamais main(). L’exécutable du runtime ne pouvait pas démarrer sans la DLL sur le système.
C’est la bonne réponse pour un développeur qui a le SDK DXC complet installé. C’est la mauvaise réponse pour un runner CI, une machine utilisateur, ou tout autre endroit où DXC n’est pas présent. Le runtime devrait démarrer. La fonctionnalité de rechargement à chaud devrait rapporter « non disponible ». Tout le reste devrait continuer.
À quoi ressemblait le correctif
Trois changements, chacun petit, chacun précis.
La source de cross-shader est passée d’un chargement de DLL au démarrage à un chargement au runtime. Remplacement de la directive #pragma comment(lib, ...) par des appels explicites à LoadLibrary et GetProcAddress. La DLL est recherchée à la première utilisation, pas au démarrage du processus. Si LoadLibrary échoue, le chemin de code retourne une erreur claire.
Des chemins de repli ont été ajoutés pour quand DXC est manquant. Quand dxcompiler.dll n’est pas présent, le sous-système de cross-shader retombe sur l’un des deux chemins. Si l’ancien D3DCompiler est disponible, il y retombe avec une fonctionnalité réduite (pas de sortie SPIR-V, seulement DXIL). Si même D3DCompiler est manquant, il retombe sur un blob SPIR-V placeholder qui produit un shader de fragment bruyamment magenta uniquement. Le placeholder garantit que le runtime continue de fonctionner de bout en bout dans les environnements où aucun compilateur de shaders n’est disponible ; le signal visuel rend évident que vous fonctionnez en mode placeholder.
Le chemin d’échec élégant journalise et émet de la télémétrie. Chaque repli est journalisé au niveau WARNING avec le sous-système spécifique qui a basculé et la raison. La télémétrie enregistre le repli pour la visibilité des opérations. Un développeur qui exécute localement sans DXC voit l’avertissement dans sa console et sait qu’il doit installer DXC s’il a besoin d’une vraie compilation de shaders. Un utilisateur qui exécute le runtime en supposant que tout est précompilé ne voit aucun avertissement du tout car les shaders précompilés fonctionnent bien sans DXC.
C’est la bonne forme pour la dépendance. Le runtime est portable. La capacité est optionnelle selon ce qui est installé. Le mode d’échec est observable.
Deux bugs adjacents qui sont ressortis du même audit
Pendant que j’étais dans le code de chargement de shaders avec le couvercle levé, deux autres tests échouaient de façons adjacentes et je les ai corrigés dans la même passe.
Incohérence ABI des effets de bus audio. Les fonctions d’API C raku_audio_bus_add_effect et raku_audio_bus_remove_effect avaient été écrites avec une signature (handle, struct*), mais la suite de tests les appelait avec (handle, handle) parce que c’est ce que le reste de l’API audio utilisait. Les tests plantaient par segfault parce que le déréférencement du pointeur de struct lisait tout ce qui se trouvait à l’adresse que les bits du deuxième handle ressemblaient. Incohérence d’ABI.
Le correctif a été de changer l’API C pour correspondre au reste du module audio : (handle, handle). C’est la bonne forme pour l’API parce que les chaînes d’effets dans le bus audio sont des objets de première classe que le runtime suit. La version pointeur de struct était un reliquat d’une conception d’API antérieure qui n’a pas survécu au reste du refactor. La suite de tests avait raison ; l’implémentation était périmée.
Des stubs de test de fuite mémoire écrasant les vraies implémentations. Le sous-système de streaming d’assets a un test de fuite mémoire qui exerce le comptage de références sur les assets en streaming. Le test échouait parce qu’une implémentation stub de AssetStreamingManager avait été laissée dans le fixture de test et écrasait la vraie implémentation depuis la DLL du runtime. Le test exerçait le stub, pas le vrai code. Le stub avait une fuite mémoire. La vraie implémentation n’en avait pas. Le test avait raison qu’il y avait une fuite ; il avait tort sur de qui était la fuite.
Le correctif a été de retirer le stub du fixture de test et d’ajouter des exports RAKU_STREAMING_API sur la vraie implémentation afin que le test puisse se lier contre elle proprement. Une fois lié correctement, le test réussissait contre le vrai code.
Ce que les trois correctifs ont débloqué ensemble
Huit tests sont passés au vert sur le correctif dxcompiler. Deux sur le correctif de bus audio. Trois sur le correctif de fuite mémoire. Le total de tests réussis a grimpé de 39/55 à 49/55. Le chiffre de 89 % est le bon jalon à célébrer mais le jalon plus profond est que le runtime fonctionne maintenant sans DXC, ce qui signifie qu’il fonctionnera dans des environnements que je n’ai pas encore anticipés.
Ce que j’ai appris
Trois choses.
Les dépendances dures au chargement sont un défaut de portabilité. Partout où votre runtime a une dépendance au chargement sur une DLL ou un objet partagé qui n’est pas universellement présent, le runtime s’est confiné aux environnements qui incluent cette DLL. Cette contrainte est acceptable si faite délibérément. C’est mauvais si fait accidentellement. L’audit de « ce dont notre runtime a besoin au chargement » vaut la peine d’être exécuté.
0xc0000135 est le code d’erreur DLL Windows le plus courant, et le message ne révèle rien. L’erreur vous dit qu’une DLL est manquante. Elle ne vous dit pas laquelle. Les outils de diagnostic pour le découvrir (Process Monitor, Dependencies.exe, le nouveau traçage ETW de Windows) fonctionnent tous, mais ils exigent tous que vous sachiez qu’ils existent et que vous les configuriez avant que l’échec ne se produise. Le runtime émet maintenant un message d’erreur clair quand un repli élégant se déclenche, nommant la DLL qui était manquante. Mon futur moi sera reconnaissant.
Les incohérences d’ABI entre les tests et les implémentations sont généralement correctes du côté du test. Les tests avaient été écrits contre l’API que le reste de la base de code exposait. Quand les tests étaient en désaccord avec l’implémentation, les tests avaient raison. C’est l’inverse de l’instinct habituel, qui est de « corriger le test ». Le bon instinct est de regarder la surface d’API plus large et de demander quel côté est l’anomalie.
Ce que partenaires et bâtisseurs devraient en retenir
Si vous évaluez un moteur pour un partenariat et que vous déployez sur des environnements où la machine du développeur et l’environnement de production sont différents (ce qui est presque tous les déploiements), demandez à l’équipe leurs dépendances dures au chargement. La bonne réponse est une courte liste, toutes justifiées. La mauvaise réponse est une longue liste, dont la moitié a été oubliée par l’équipe.
Si vous êtes un développeur natif Windows et que vous n’utilisez pas encore LoadLibrary plus GetProcAddress pour toute DLL qui n’est pas universellement présente, voici le petit coup de pouce. Le motif est petit. Le gain de portabilité est grand.
Si vous êtes un labo IA dont l’agent de codage écrit du code Windows, les motifs à surveiller dans le code écrit par agent sont les directives #pragma comment(lib, ...) qui devraient être des chargements au runtime et les signatures d’ABI qui dérivent du reste de la base de code. Les deux sont signalés par l’analyse statique. Les deux devraient être dans la checklist de relecture de l’agent.
Bilan de fin de week-end. Le runtime démarre proprement sur des machines qui n’ont pas DXC. Onze tests sont passés au vert en deux jours. Le prochain audit est au calendrier.
Retour à la construction.
Un runtime qui fonctionne là où vit votre matériel
RakuAI est le runtime spatial construit pour les lunettes intelligentes et le monde réel désordonné — portable, élégant face aux dépendances manquantes, observable quand il bascule en repli. Voyez ce qu'il faut pour déployer partout.