1 points par GN⁺ 2025-07-02 | 1 commentaires | Partager sur WhatsApp
  • Le bug des barils rotatifs de Donkey Kong Country 2 se produit sur l’émulateur ZSNES
  • ZSNES n’émule pas correctement le comportement de l’open bus, ce qui fait que les barils tournent en permanence
  • Contrairement au matériel réel, ZSNES renvoie toujours 0 lors d’un accès mémoire invalide, ce qui provoque le bug
  • Dans le fonctionnement correct, la logique veut que le baril s’arrête de tourner dans la bonne direction (8 directions)
  • On suppose que ce problème vient d’une petite erreur de code, à savoir l’utilisation de l’adressage absolu au lieu de l’adressage immédiat

Donkey Kong Country 2 et le bug des barils dans l’émulateur ZSNES

Donkey Kong Country 2 présente un bug bien connu sur un ancien émulateur SNES appelé ZSNES, où les barils rotatifs de certains niveaux ne fonctionnent pas correctement.

Quand on entre dans un baril, celui-ci devrait normalement tourner uniquement tant que l’on maintient la direction gauche/droite, mais dans ZSNES, une simple pression brève sur gauche/droite suffit pour que le baril continue à tourner indéfiniment dans cette direction.

À cause de ce bug, les séquences de barils rotatifs qui apparaissent au-dessus de ronces ou d’obstacles, surtout dans les niveaux plus avancés, deviennent bien plus difficiles que ce que les développeurs avaient prévu.

Le problème avait été partiellement documenté autrefois sur les forums ZSNES, mais ces forums ont disparu et il est aujourd’hui difficile de retrouver les ressources associées.

Cause du bug - Émulation de l’open bus

La cause fondamentale de ce bug est que ZSNES n’émule pas le comportement de l’open bus.

  • l’open bus est un comportement observé sur d’anciennes plateformes comme la SNES lors de la lecture d’une adresse mémoire invalide
  • sur le matériel réel, la dernière valeur placée sur le bus est renvoyée
  • le CPU principal de la SNES est le 65C816 (65816)
  • le 65816 est une version 16 bits du 6502, avec un bus d’adresses 24 bits et un mécanisme de bank switching

Dans le code des barils rotatifs de DKC2, lorsqu’un accès est effectué à des adresses invalides (Bank $B3, $2000 et $2001), le matériel renvoie la valeur 0x2020 via l’open bus.

Comme ZSNES ne prend pas en charge ce comportement, il renvoie toujours 0, ce qui déclenche le bug.

Fonctionnement du code du jeu

La routine du jeu liée aux barils rotatifs suit le flux suivant :

  • elle additionne la direction actuelle du baril et sa rotation (vitesse), puis stocke le résultat dans une variable temporaire
  • elle mesure le changement de direction avec une opération XOR, puis applique un AND entre ce résultat et la valeur lue sur l’open bus
  • si le résultat du AND vaut 0, la rotation continue ; sinon, elle s’arrête et la direction est arrondie puis alignée sur l’une des 8 directions

Sur le matériel réel, la valeur de l’open bus est 0x2020, mais si 0 est renvoyé à la place, la rotation continue à l’infini.

On suppose que cette logique aurait dû faire l’opération AND avec une valeur immédiate (address #$2000), mais qu’elle utilise par erreur une adresse absolue (address $2000).

Cependant, à cause des caractéristiques de l’open bus sur le matériel réel, les deux méthodes fonctionnent en pratique correctement.

Correctif et conclusion

D’autres émulateurs SNES comme Snes9x ont corrigé ce bug via un correctif codé en dur, tandis que ZSNES, dont le développement est arrêté, n’a jamais été patché.

Si l’on modifie l’opcode de l’instruction AND dans cette routine de 0x2D à 0x29 (AND #$2000), les barils rotatifs fonctionnent normalement même sans comportement d’open bus.

Ce problème ne se produit pas sur le matériel réel ni sur les émulateurs modernes.

En fin de compte, ce bug est un exemple de problème né de la combinaison entre l’absence de prise en charge de l’émulation de l’open bus et une erreur de code.


Contexte supplémentaire : architecture du 65816 et mémoire de la SNES

Le CPU 65816 dispose d’un bus d’adresses 24 bits, mais il utilise principalement une combinaison de banque sur 8 bits et de décalage sur 16 bits.

  • le compteur ordinal (PC) est sur 16 bits, et l’adresse complète est construite avec le registre de banque programme (PBR, K)
  • la banque de données (DBR, B) sert à sélectionner la banque utilisée pour les opérations sur les données
  • la pile matérielle et la direct page se trouvent toujours dans la banque $00

La mémoire de la SNES est elle aussi conçue autour du 65816, et il est plus efficace de penser les adresses comme une combinaison banque 8 bits + décalage 16 bits.

Mot de la fin

Ce cas montre que les caractéristiques du matériel legacy (comme l’open bus) peuvent provoquer, en émulation, des bugs inattendus.

Les développeurs auraient dû utiliser l’adressage immédiat, mais c’est un cas où l’adressage absolu a malgré tout fonctionné par hasard.

Aujourd’hui, cela montre à quel point l’émulation du comportement de l’open bus est essentielle pour reproduire fidèlement les anciens logiciels.

1 commentaires

 
GN⁺ 2025-07-02
Avis Hacker News
  • En tant que programmeur en assembleur 6502, j’ai moi aussi perdu un nombre incalculable d’heures à cause d’erreurs où j’oubliais le symbole # et faisais un accès mémoire au lieu d’utiliser une valeur immédiate. C’est d’autant plus pénible que ce genre d’erreur fonctionne parfois par chance. Mais pire encore que le problème du floating bus dans l’exemple, il y a le code qui dépend d’une RAM non initialisée : comme la valeur initiale varie selon les DRAM, tout marche toujours bien sur sa propre machine ou dans son émulateur, mais ça échoue sur une autre machine utilisant une DRAM différente. En général, c’est le genre de problème qu’on découvre à une demo party quand il reste moins de 15 minutes avant de devoir faire tourner le code sur le matériel de quelqu’un d’autre

    • Je me demande s’il a réellement existé des architectures à base de 6502 utilisant de la mémoire dynamique. D’après mon expérience, ces plateformes utilisaient toujours de la SRAM

    • Le 6502 a été mon premier langage assembleur, et je voyais LDA #2 comme « charger le nombre 2 dans le registre A ». À l’inverse, LDA 2 donnait l’impression de « charger la valeur située à l’emplacement mémoire 2 », donc cette différence m’a aidé à éviter l’erreur dès le départ

    • Dans ce genre de situation, faire passer le code à un LLM peut au contraire être utile. Les LLM sont plutôt bons pour repérer ce type de fautes de frappe ou points d’erreur à fort impact

  • En voyant le terme Open Bus écrit avec des majuscules, j’ai d’abord cru qu’il s’agissait d’un ancien protocole ou standard de bus. J’ai fini par comprendre qu’il signifiait simplement qu’aucun périphérique n’était connecté sur le bus à cet endroit, parce qu’aucune puce mémoire n’était activée à l’adresse spécifiée par le décodeur d’adresses ($2000). Autrement dit, l’oubli du mode immédiat (#) faisait que rien n’était réellement lu en mémoire, et c’est le fait qu’un vieil émulateur se comporte différemment du matériel réel qui a permis de le découvrir. La correction consiste à passer l’instruction en adressage immédiat, ce qui supprime la lecture mémoire et rend le code environ 2 us plus rapide. Mais un tel écart de performance n’a sans doute pas beaucoup d’importance hors du matériel réel, surtout avec des émulateurs dont le timing n’est pas parfaitement fidèle

    • Il est expliqué que certains émulateurs SNES ont aujourd’hui atteint une fidélité temporelle presque parfaite. Cela dit, un écart de 2 us ne produit pratiquement jamais de différence perceptible, sauf cas vraiment exceptionnel. Article lié : How SNES emulators got a few pixels from complete perfection

    • Il existe plusieurs cas de jeux, comme ceux de Rare, dans lesquels des bugs ne sont apparus que très longtemps après la sortie grâce à de nouvelles architectures. Dans Donkey Kong 64, une fuite mémoire fatale se produit après 8 à 9 heures de jeu continu, mais les fonctions de sauvegarde d’état des émulateurs permettent d’accumuler ce temps instantanément, ce qui expose facilement le bug. On raconte souvent que le Memory Pak fourni à la sortie servait à masquer ce bug, mais des recherches récentes indiquent que ni Rare ni Nintendo n’en avaient connaissance à l’époque

  • J’ai déjà rencontré un phénomène d’open bus du PPU dans la version SNES de Puyo Puyo. C’était en cherchant pourquoi les save states ne correspondaient pas lors du développement de la fonction RunAhead dans RetroArch, et c’était un cas particulier où la valeur lue sur l’open bus du PPU changeait après chargement de l’état, ce qui faisait diverger les logs de trace d’exécution du CPU

  • En 6502 ou dans du code similaire, je confonds souvent adresses mémoire et valeurs immédiates. Je pense que des notations comme #$1234 favorisent ce type d’erreur, et j’ai même entendu dire que Chuck Peddle lui-même regrettait profondément cette syntaxe. Dans un IDE, le fait de colorer # en rouge aide un peu à l’éviter. Même les développeurs de Rare n’ont pas échappé à ce genre de faute

    • Il y a assez longtemps, j’ai rencontré un problème similaire avec le GNU assembler en mode intel_syntax noprefix, où il existe une ambiguïté syntaxique lorsque l’on référence par avance une constante immédiate nommée, qui peut être interprétée comme une adresse mémoire ou un symbole. Résultat, au lieu de ce que j’attendais, cela créait une adresse mémoire temporaire en attendant même l’étape d’édition de liens du symbole, ce qui rendait le débogage vraiment pénible

    • Les jeux d’instructions comme ARM, qui nécessitent des instructions séparées pour manipuler la mémoire, évitent à la racine ce genre de confusion

  • À ma connaissance, le phénomène d’open bus n’apparaît que dans les premiers systèmes à bus synchrone simples. La plupart des autres systèmes renvoient une valeur constante comme tout à 0 ou tout à 1 quand on accède à une adresse inexistante, et les protocoles de bus gèrent cela avec un handshake permettant au maître de détecter l’absence de réponse, comme le master abort sur PCI

  • En programmant la puce Parallax Propeller, j’ai vécu plusieurs fois une erreur similaire. Je confonds souvent JMP #address et JMP address, probablement à cause de ma mémoire musculaire héritée de l’assembleur 6502. Sur le Propeller, JMP #address saute vers l’adresse indiquée, tandis que JMP address saute vers la valeur lue à l’adresse donnée. Le problème, c’est que ce bug fonctionne parfois quand même, et on peut alors perdre des heures à chercher pourquoi ça finit par se bloquer

  • Un open bus signifie que les lignes du bus de données sont réellement laissées ouvertes au niveau électrique. Quand le CPU place sur le bus une adresse non mappée ou en écriture seule, aucun matériel ne répond, si bien que les lignes du bus restent flottantes — autrement dit, un undefined behavior au niveau matériel. Pour comprendre ce qui se passe réellement, il faut regarder la structure physique du bus. Le bus est un long conducteur qui transporte les signaux entre la carte mère et la cartouche, séparé du plan de masse par une fine couche isolante. Cette structure se comporte comme un condensateur, et finit donc par « retenir » pendant un certain temps la tension du dernier signal. C’est pourquoi, sur un open bus, on relit en pratique souvent la dernière valeur transmise. Des jeux comme DKC2 dépendent sans le vouloir de cette propriété, et sur la NES, le port série de la manette ne fournit des signaux que sur les bits de poids faible tandis que les bits de poids fort restent en open bus, ce qui fait que certains jeux attendent $40 ou $41 avec l’instruction LDA $4016. Le phénomène d’open bus est même exploité dans des stratégies de speedrun comme le credits warp de Super Mario World, via corruption mémoire ou exécution de code arbitraire. Il existe cependant des exceptions, avec des cartouches non standard, l’usage de résistances de pull-up/pull-down, ou des interactions exotiques avec le DMA comme le Horizontal DMA. Par exemple, si un transfert HDMA se produit au milieu d’une instruction sur SNES, cela peut affecter le timing d’une lecture d’open bus et insérer une valeur anormale entre des blocs mémoire que l’on cherche à dupliquer, ce qui casse l’exploit dans un speedrun de Super Metroid. Ainsi, sur le matériel d’origine ou avec un émulateur très précis, cela peut provoquer un crash, tandis que la plupart des émulateurs et des rééditions officielles n’implémentent pas parfaitement ce comportement de niche, ce qui permet à la stratégie de fonctionner normalement. Le record du monde TAS sur Super Metroid dépend lui aussi de ce comportement HDMA. En manipulant la position des ennemis pour changer le timing CPU, on peut faire en sorte que le HDMA place la valeur voulue sur l’open bus, puis exécuter finalement les entrées manette comme du code, jusqu’à permettre l’exécution de code arbitraire vidéo du credits warp de Super Mario World, vidéo sur l’usage du HDMA, vidéo sur l’exploit DMA de Super Metroid, record TAS de Super Metroid

    • La série de vidéos de Ben Eater sur son ordinateur 6502 sur breadboard m’a beaucoup aidé à comprendre ce type de comportement matériel. Cela permet de bien saisir comment ces mécanismes de bus s’étendent dans les appareils commerciaux site de Ben Eater
  • J’aime ce genre de contenu d’analyse de bugs. Je ne suis l’assembleur qu’à environ 60 %, mais les explications en prose qui accompagnent le tout aident énormément à comprendre. Et j’adore particulièrement ces histoires où un bug inconnu pendant des années est finalement découvert dans un logiciel culte

    • Ces systèmes de l’époque sont d’autant plus fascinants qu’ils n’avaient pratiquement aucun des mécanismes de vérification considérés aujourd’hui comme indispensables, notamment dans l’embarqué, qu’il y ait ou non une connexion réseau. À l’époque de la NES, quantité de lectures/écritures se résumaient à basculer la tension sur des lignes, et on ne savait vraiment ce qui se produirait qu’au moment précis où cela arrivait. On obtenait les effets voulus en basculant les tensions avec un timing exactement synchronisé sur le signal de blanking du CRT, et dans Super Mario Bros. 3, on s’amusait par exemple à commuter le multiplexeur RAM pour changer de banque de sprites à chaque cycle de rafraîchissement de l’écran. Comme les différences NTSC/PAL de la télévision selon les régions servaient effectivement d’horloge à la logique de rendu, il fallait sortir des versions logicielles distinctes adaptées à chaque téléviseur. C’était vraiment une époque sauvage
  • Quand je suis bloqué dans un jeu sur émulateur, j’ai toujours le doute : « et si c’était un bug de l’émulateur ? ». Dans ce cas précis, j’aurais simplement pensé que le jeu avait été conçu pour être aussi difficile. Et quand la difficulté est vraiment élevée, je me suis aussi demandé si ça ne venait pas de la latence de l’émulateur, au point de finir par me fabriquer un mister FPGA pour l’utiliser

    • Je me souviens d’un passage dans Chrono Trigger où il faut appuyer sur quatre touches simultanément, mais l’entrée USB ne transmettait que trois touches à la fois, si bien qu’une tentative sur quatre seulement était reconnue, ce qui rendait la chose très difficile et frustrante

    • Comme je n’avais joué à DKC que sur ZSNES, je n’avais absolument pas compris avant de lire l’article qu’il s’agissait d’un bug d’émulateur. Je croyais simplement que le game design était volontairement aussi difficile, et découvrir que c’était en fait un bug m’a vraiment choqué

    • J’ai beaucoup joué à Bionic Commando quand j’étais enfant, et en y rejouant sur émulateur, je l’ai trouvé bien plus difficile. J’ai appris plus tard qu’à cause d’un bug de l’émulateur, les ennemis ne disparaissaient pas, ce qui doublait en pratique le nombre de vies nécessaires. J’ai quand même réussi à le finir une fois dans ces conditions, mais je ne recommencerais pas

  • Les graphismes 3D pré-rendus de DKC 1, produits à partir de SGI, étaient de la technologie de pointe à l’époque. Vector Man sur Mega Drive utilisait une technique similaire, mais n’a jamais autant marqué que DKC

    • En 1995, j’étais exactement dans la tranche d’âge cible de DKC (11 ans), et les graphismes du jeu m’avaient vraiment sidéré. J’avais même reçu une cassette promotionnelle à sa sortie, contenant des images des coulisses, et je l’ai regardée plusieurs fois. Je ne possédais pas le jeu moi-même, mais j’avais l’occasion d’y jouer chez un ami

    • Enfant, j’avais l’impression que les graphismes de DKC avaient quelque chose de « faux ». À l’époque, les magazines présentaient souvent cela de manière artificielle, comme si la SNES rendait vraiment des personnages 3D en temps réel, alors qu’au fond je pressentais qu’il s’agissait plutôt d’une forme d’animation image par image, comme un flipbook