Graphismes roses, score qui s'emballe, crash XR. Quatre bugs en un seul samedi.
Quatre bugs dans quatre sous-systèmes qui passent tous leurs tests unitaires - c'est le mode de défaillance qui livre des démos cassées. L'attraper est la discipline qui fait un exemple que vous pouvez réellement faire tourner sur RakuAI.
Il y a un genre de samedi spécifique que connaît quiconque a livré une démo. La démo fonctionnait à la fin du week-end dernier. Vous la reprenez ce samedi matin. Elle est maintenant cassée de quatre façons différentes et aucune de ces façons n’a de rapport avec les autres.
C’était aujourd’hui. La démo était le jeu Shooter que nous livrons comme l’un des exemples canoniques. Les bugs attendaient comme des invités de fête que personne n’avait invités.
Ce qui était cassé
La démo démarrait. C’était la bonne nouvelle. Après ça :
Les graphismes étaient roses. Quiconque a travaillé dans un moteur de rendu temps réel connaît le rose de la mort. C’est la couleur d’emplacement réservé « shader manquant », rose vif, qui crie qu’un élément du pipeline de rendu n’a pas trouvé le shader qu’il attendait. Chaque objet généré procéduralement dans la démo se rendait dans cette couleur. Vaisseaux. Astéroïdes. Particules. Tout en rose. Du rose partout.
Le score s’est emballé. Chaque élimination s’enregistrait deux fois. Le compteur d’éliminations montait par paires. Vingt ennemis abattus, « score : quarante ». Quarante ennemis abattus, « score : quatre-vingts ». Sur le papier, on dirait que le jeu était généreux, mais en pratique le tableau des scores était gonflé et la logique de meilleur score se déclenchait sur des seuils complètement faux.
Le démarrage XR s’est crashé. La démo sur un écran plat démarrait proprement. Au moment où j’ai branché un casque et basculé en mode XR, sortie brutale. Aucune trace de pile ne remontait à travers le lanceur. Le thread de rendu avait disparu avant que le lanceur ne puisse attraper un log.
La caméra ne se verrouillait pas sur le vaisseau. Caméra d’orbite troisième personne standard, censée garder le vaisseau dans le cadre. Au démarrage, la caméra apparaissait à l’origine du monde et y restait. Le vaisseau était visible à travers le vide comme un petit point au loin.
Quatre bugs. Zéro chevauchement de sous-systèmes. Le genre de samedi matin qui décide si vous avez le flux de travail ou non.
Ce qui a causé chacun
Je veux passer en revue chacun parce que chacun enseigne une leçon différente sur comment une base de code grandit et casse dans un flux de travail piloté par agents.
Les graphismes roses
Cause racine : les handles de shader étaient recherchés par nom de chaîne à chaque appel de rendu, et une refactorisation pendant les fêtes avait déplacé l’initialisation du cache de shaders vers un point différent de la séquence de démarrage. Les générateurs procéduraux rendaient maintenant avant que le cache de shaders ne se soit réchauffé. Ils recevaient en retour le repli de handle nul, que le moteur de rendu rendait consciencieusement en rose vif.
La solution a été de créer un utilitaire ShaderCache explicite qui met en cache les références de shader résolues à la première utilisation et expose une API GetOrCreate synchrone que les générateurs procéduraux peuvent appeler sans se soucier de l’ordre de démarrage. Chaque générateur procédural a été mis à jour pour l’utiliser. Le rose a disparu.
La leçon : une hypothèse d’ordre d’initialisation qui vivait implicitement dans la séquence de démarrage a été cassée par une refactorisation qui n’a pas signalé l’hypothèse. L’ordonnancement implicite est fragile. Le nouveau cache rend l’ordonnancement explicite et auto-réparant.
Le score qui s’emballe
Cause racine : le GameplayHUD comptait les éliminations côté interface, et ScoreManager comptait aussi les éliminations côté simulation. Les deux écoutaient le même signal d’événement d’élimination. Les deux ajoutaient à un champ de score partagé. Le score était incrémenté deux fois par élimination.
La solution a été de choisir le propriétaire canonique du score. ScoreManager détient la vérité. Le GameplayHUD lit depuis ScoreManager et rend. L’incrément dupliqué dans le HUD a été retiré.
La leçon : dans un moteur piloté par événements, deux abonnés au même événement s’exécuteront tous les deux. Si les deux écrivent dans le même état, l’état sera faux proportionnellement au nombre d’abonnés qui ont écrit. La solution est de tracer une ligne claire sur qui possède quel état. Une fois tracée, le bug devient impossible.
Le crash au démarrage XR
Cause racine : le sous-système XR essayait de s’initialiser avant que l’appareil graphique sous-jacent ne soit prêt. Au démarrage écran plat, l’ordre se trouvait fonctionner parce que l’appareil graphique était toujours prêt au moment où l’utilisateur appuyait sur « Démarrer ». Au démarrage XR, la détection de casque déclenchait le chemin d’initialisation XR avant que l’appareil graphique ne soit confirmé prêt. Le sous-système XR déréférençait un handle d’appareil nul. Crash.
La solution a été un XRBootstrap qui détecte le XR au point le plus tôt possible du démarrage, diffère l’initialisation XR réelle jusqu’à ce que l’appareil graphique confirme être prêt, et s’arrête avec grâce si quoi que ce soit dans la chaîne signale une défaillance. Le crash devient maintenant un message d’erreur propre et l’utilisateur se replie sur écran plat.
La leçon : les crashs brutaux sont le pire type d’erreur parce qu’ils tuent le logging qui vous aurait dit ce qui s’est passé. Détecter la défaillance plus tôt et la rapporter proprement vaut le coût d’ingénierie.
La caméra qui ne se verrouillait pas
Cause racine : la caméra d’orbite essaie de cibler le vaisseau du joueur au démarrage. Le vaisseau est engendré par le générateur procédural, de façon asynchrone, quelques frames après l’initialisation de la caméra. La caméra cherchait le vaisseau dès la toute première frame, ne trouvait rien, et n’essayait jamais plus.
La solution a été de donner à la caméra d’orbite un mécanisme de nouvelle tentative. Elle cherche le vaisseau pendant un nombre borné de frames avant d’abandonner, et une fois verrouillée, elle reste verrouillée. La caméra apparaît maintenant brièvement à l’origine, puis se claque sur le vaisseau dans la première seconde de jeu.
La leçon : les conditions de course entre systèmes qui s’initialisent sur des calendriers légèrement différents vous mordront. La solution est une petite nouvelle tentative, bornée pour ne pas boucler indéfiniment, avec un délai d’attente explicite qui produit une erreur utile si la nouvelle tentative s’épuise.
Ce que j’ai appris spécifiquement sur la démo Shooter
Trois choses, toutes inconfortables.
Chaque sous-système fonctionnait isolément. L’intégration était cassée. Le moteur de rendu fonctionnait. Le système de score fonctionnait. Le sous-système XR fonctionnait. La caméra fonctionnait. La démo Shooter comme expérience intégrée ne fonctionnait pas. C’est le genre d’échec qui échappe aux tests unitaires à chaque fois, parce que les tests unitaires par nature exercent les choses isolément.
La refactorisation des fêtes pendant la pause de fin décembre était la cause immédiate d’au moins deux de ces bugs. La réorganisation du cache de shaders et la division de l’abonnement à l’événement d’élimination ont toutes deux atterri dans la fenêtre du sprint des fêtes. Les deux étaient des PR correctes isolément. Les deux ont cassé la démo de façons non évidentes. La leçon est de faire tourner les démos intégrées dans le cadre de la CI, pas juste les tests unitaires. Ce travail a atterri ce matin comme une PR séparée.
Le chemin de démarrage XR avait besoin de son propre gardien de démarrage. Les crashs brutaux sont inacceptables parce qu’ils tuent le diagnostic. Chaque chemin de démarrage qui touche du matériel non trivial a besoin d’un gardien. Nous l’avons maintenant pour le XR. Nous ne l’avons pas pour l’audio spatial ni pour la liaison Wi-Fi 7. Ajouter les deux est dans la file pour les deux prochains samedis.
Ce que les partenaires et bâtisseurs devraient retenir de ceci
Si vous construisez quoi que ce soit qui intègre plusieurs sous-systèmes avec des dépendances d’ordre d’initialisation, la leçon est la même que celle que la démo Shooter vient d’apprendre. Rendez l’ordre d’initialisation explicite. Rendez les modes de défaillance gracieux. Faites tourner la démo intégrée en CI, pas juste les tests unitaires.
Si vous êtes une équipe indépendante qui réfléchit à adopter Raku, la démo Shooter est un véritable exemple que nous testons à chaque sortie. Si elle casse, vous le voyez casser. C’est le genre de signal de discipline publique qui compte plus qu’une liste de fonctionnalités.
Si vous êtes un laboratoire IA qui réfléchit à brancher votre modèle dans une expérience AR, le chemin de démarrage XR a maintenant un gardien défendable. Votre modèle ne verra pas de crash brutal en entrant. Vous obtiendrez un rapport d’erreur propre si quoi que ce soit dans la chaîne échoue. L’infrastructure est en place.
Quatre bugs. Un samedi. La démo se construit proprement maintenant. Les invités de la fête ont été raccompagnés.
Retour à la construction.
Déployez-vous sur un runtime qui fait tourner ses propres exemples
RakuAI teste ses démos à chaque sortie avec une CI d'intégration et des gardiens de démarrage XR gracieux. C'est le signal de discipline publique qui compte plus qu'une liste de fonctionnalités. Commencez à construire dessus.