1 puntos por GN⁺ 2025-07-02 | 1 comentarios | Compartir por WhatsApp
  • El bug de los barriles giratorios de Donkey Kong Country 2 ocurre en el emulador ZSNES
  • ZSNES no emula correctamente el comportamiento de open bus, lo que provoca que los barriles giren permanentemente
  • A diferencia del hardware real, en ZSNES un acceso incorrecto a memoria siempre devuelve 0, lo que desencadena el bug
  • En el comportamiento correcto, la lógica hace que el barril deje de girar en la dirección exacta (8 direcciones)
  • Se cree que este problema proviene de un pequeño error de programación (es decir, usar direccionamiento absoluto en lugar de direccionamiento inmediato)

El bug del barril en Donkey Kong Country 2 y el emulador ZSNES

Donkey Kong Country 2 tiene un bug famoso en el que los barriles giratorios de algunas fases no funcionan correctamente en un viejo emulador de SNES llamado ZSNES

Al entrar en un barril, normalmente este solo debería girar mientras mantienes presionado izquierda/derecha, pero en ZSNES basta con pulsar izquierda/derecha brevemente para que el barril siga girando eternamente en esa dirección

Debido a este bug, sobre todo en fases avanzadas, las secciones con barriles giratorios que aparecen sobre zarzas u obstáculos se vuelven mucho más difíciles de lo que los desarrolladores pretendían

Este problema estuvo documentado en cierta medida en los antiguos foros de ZSNES, pero como esos foros ya no existen, hoy es difícil encontrar material relacionado

La causa del bug - Emulación de open bus

La causa fundamental de este bug es que ZSNES no emula el comportamiento de open bus

  • open bus es un comportamiento que ocurre en plataformas antiguas como SNES cuando se lee una dirección de memoria inválida
  • En el hardware real, se devuelve el último valor puesto en el bus
  • La CPU principal de SNES es la 65C816 (65816)
  • La 65816 es una versión de 16 bits de la 6502, tiene un bus de direcciones de 24 bits y usa un esquema de memory banking

En el código del barril giratorio de DKC2, al acceder a direcciones inválidas (Bank $B3 en $2000, $2001), el hardware devuelve por open bus el valor 0x2020

Como ZSNES no tiene esta función, siempre devuelve 0, y ahí se produce el bug

Cómo funciona el código del juego

La rutina del juego relacionada con el barril giratorio sigue este flujo:

  • Suma la dirección actual del barril y la cantidad de rotación (velocidad), y lo guarda en una variable temporal
  • Mide el cambio de dirección con una operación XOR, y hace una operación AND al resultado con el valor leído desde open bus
  • Si ese resultado del AND es 0, la rotación continúa; si no es 0, se detiene y la dirección se redondea para alinearla a una de 8 direcciones

En el hardware real, el valor de open bus es 0x2020, pero si en su lugar se devuelve 0, la rotación continúa infinitamente

Se cree que esta lógica originalmente debía hacer la operación AND con un valor inmediato (address #$2000), pero por error se usó una dirección absoluta (address $2000)

Sin embargo, por las características del open bus del hardware, en la práctica ambos métodos funcionan correctamente

Solución y conclusión

Otros emuladores de SNES como Snes9x corrigieron este bug con un arreglo hardcodeado, mientras que ZSNES, al haber quedado descontinuado, nunca recibió un parche

Si en esa rutina se cambia el opcode de la instrucción AND de 0x2D a 0x29 (AND #$2000), el barril giratorio funciona correctamente incluso sin comportamiento de open bus

Este problema no ocurre en el hardware real ni en los emuladores modernos

En última instancia, este bug es un ejemplo de cómo coinciden la falta de soporte para emulación de open bus y un error de programación


Contexto adicional: estructura de la 65816 y mapa de memoria de SNES

La CPU 65816 tiene un bus de direcciones de 24 bits, pero normalmente usa una combinación de banco de 8 bits y offset de 16 bits

  • El contador de programa (PC) es de 16 bits, y la dirección completa se compone con el registro de banco de programa (PBR, K)
  • El banco de datos (DBR, B) se usa para seleccionar el banco en operaciones de datos
  • La pila de hardware y la direct page siempre existen en el banco $00

El mapa de memoria de SNES también está diseñado sobre la base de la 65816, así que es más eficiente pensar las direcciones como banco de 8 bits + offset de 16 bits

Cierre

Este caso muestra que las características del hardware legacy (como open bus) pueden convertirse en bugs inesperados dentro de la emulación

El desarrollador debía haber usado direccionamiento inmediato, pero por casualidad el direccionamiento absoluto también funcionaba correctamente

Hoy en día, esto sugiere que emular incluso el comportamiento de open bus es muy importante para reproducir con precisión el software antiguo

1 comentarios

 
GN⁺ 2025-07-02
Comentarios en Hacker News
  • Como programador de ensamblador 6502, he perdido incontables horas por el error de olvidarme del símbolo # y terminar haciendo un acceso a memoria en vez de usar un valor inmediato; además, a veces por suerte funciona bien, así que se vuelve todavía más fastidioso. Pero un caso aún peor que el problema del floating bus del ejemplo es el código que depende de RAM sin inicializar: como el valor inicial cambia según cada DRAM, en tu computadora o emulador siempre corre bien, pero falla en otra máquina con otra DRAM. Normalmente uno descubre este tipo de problema en una demoparty, cuando faltan menos de 15 minutos y el código no corre en el hardware de otra persona

    • Me da curiosidad si realmente existió alguna arquitectura con 6502 que usara memoria dinámica. En mi experiencia, esas plataformas siempre usaban RAM estática

    • El 6502 fue mi primer lenguaje ensamblador, y LDA #2 yo lo entendía como “cargar el número 2 en el registro A”. En cambio, LDA 2 se siente como “cargar el valor de la posición de memoria 2”, así que esa diferencia me ayudó desde el principio a evitar el error

    • En una situación así, pasar el código por un LLM podría incluso ser útil. Los LLM suelen ser buenos detectando errores tipográficos o puntos de fallo de alto impacto como este

  • Vi la expresión Open Bus escrita con mayúsculas y pensé que era algún protocolo o estándar viejo de bus. Resultó que solo significa que el bus no está conectado a nada, porque ningún dispositivo de memoria se activó en la dirección indicada por el decodificador de direcciones ($2000). O sea, por olvidar el modo inmediato (#), terminó intentando leer memoria y no obtuvo nada; eso se descubrió porque un emulador viejo se comportaba distinto al hardware real. La solución fue cambiar la instrucción al modo de direccionamiento inmediato, con lo cual ya no hace lectura de memoria y el código se vuelve unos 2us más rápido. Pero una diferencia de rendimiento así no parece tener mucha importancia salvo en hardware real, y especialmente no en emuladores cuya temporización no coincide por completo

    • Explican que (algunos) emuladores de SNES hoy están casi en la perfección a nivel de temporización. Aun así, una diferencia de 2us prácticamente no produce nada perceptible salvo en casos realmente excepcionales. Artículo relacionado: How SNES emulators got a few pixels from complete perfection

    • Hay varios casos como el de Rare, donde juegos llenos de bugs enterrados se descubren muchos años después gracias a nuevas arquitecturas. En Donkey Kong 64 hay una fuga de memoria fatal que aparece tras 8 o 9 horas continuas de juego, pero la función de save state de los emuladores hace que ese tiempo se acumule de golpe y el bug se vuelva fácil de exponer. Se decía que el Memory Pak incluido al lanzamiento era para ocultar ese bug, pero investigaciones recientes indican que ni Rare ni Nintendo sabían del problema en ese momento

  • Me topé con el fenómeno de open bus del PPU en SNES Puyo Puyo. Estaba investigando por qué los save states no coincidían al trabajar en la función RunAhead de RetroArch, y resultó ser un caso especial donde el valor leído desde el open bus del PPU cambiaba después de cargar el estado, así que el log de trazas de ejecución del CPU ya no coincidía

  • En código de 6502 o similar, a menudo cometo el error de confundir direcciones de memoria con valores inmediatos. Creo que una notación como #$1234 invita al error, e incluso escuché que Chuck Peddle lamentó mucho esa sintaxis. En el IDE pude prevenirlo un poco resaltando # en rojo. Hasta un desarrollador de Rare cayó en ese error

    • Hace bastante tiempo tuve un problema parecido con GNU assembler en modo intel_syntax noprefix; ahí hay una ambigüedad sintáctica cuando se referencia por adelantado una constante inmediata con nombre, porque puede interpretarse como dirección de memoria o como símbolo. Como resultado, inesperadamente se generaba una dirección de memoria temporal que esperaba hasta el momento del enlace del símbolo, y encontrar ese bug fue realmente doloroso

    • Los conjuntos de instrucciones como ARM, donde se requieren instrucciones separadas para tratar con memoria, evitan de raíz este tipo de confusión

  • Hasta donde sé, el fenómeno de open bus solo aparece en sistemas de bus síncrono simples y tempranos. La mayoría de los otros sistemas devuelven un valor fijo, como todo 0 o todo 1, cuando se accede a una dirección inexistente, y eso se maneja mediante handshaking en el protocolo del bus, donde el maestro puede detectar la falta de respuesta, como el master abort de PCI

  • Al programar el chip Parallax Propeller me pasó repetidamente un error parecido. A menudo confundo JMP #address con JMP address, por culpa de la memoria muscular del ensamblador 6502. En Propeller, JMP #address salta a la dirección indicada, mientras que JMP address salta al valor leído desde esa dirección. El problema es que a veces este bug también funciona, así que uno termina perdiendo horas tratando de averiguar por qué dejó de funcionar

  • Open bus significa que las líneas del bus de datos están literalmente abiertas, es decir, el circuito quedó sin cerrar. Cuando la CPU pone en el bus una dirección no mapeada o de solo escritura, ningún hardware responde y las líneas del bus quedan flotando: undefined behavior a nivel de hardware. Para entender qué pasa realmente, hay que mirar la estructura física del bus de datos. El bus es un conductor largo que transporta señales entre la tarjeta madre y el cartucho, separado del plano de tierra por una placa aislante delgada. Esa estructura actúa como una especie de capacitor, así que por un tiempo “retiene” el voltaje de la última señal. Por eso, en open bus se vuelve a leer el último valor transmitido. Juegos como DKC2 dependen sin querer de esta característica, y en el NES el puerto serial del control también solo entrega señal en los bits bajos mientras los bits altos quedan en open bus, por lo que ciertos juegos esperan $40 o $41 al ejecutar LDA $4016. El fenómeno de open bus incluso se aprovecha en estrategias de speedrun como el credits warp de Super Mario World, ya sea por corrupción de memoria o por ejecución arbitraria de código. Claro, los cartuchos no estándar, el uso de resistencias pull-up/pull-down, o interacciones extrañas con DMA como Horizontal DMA pueden producir resultados excepcionales. Por ejemplo, si una transferencia HDMA del SNES ocurre a mitad de una instrucción, puede afectar el timing de la lectura de open bus, y entonces en un exploit de speedrun de Super Metroid aparecen valores anómalos entre bloques de memoria que se querían duplicar, rompiendo el exploit. Por eso, en hardware original o en emuladores muy precisos puede haber crash, mientras que en la mayoría de emuladores o relanzamientos oficiales esta clase de comportamiento tan de nicho no está implementado por completo y la estrategia sí funciona. El récord mundial TAS completado de Super Metroid también depende de este comportamiento de HDMA. Manipulando la posición de los enemigos para alterar el timing del CPU, hacen que HDMA coloque el valor deseado en el open bus y finalmente ejecutan la entrada del control como código, habilitando ejecución arbitraria de código. Video del credits warp de Super Mario World, video del uso de HDMA, video del exploit DMA de Super Metroid, récord TAS de Super Metroid

    • La serie de videos de la computadora 6502 en protoboard de Ben Eater me ayudó muchísimo a entender cómo funciona este comportamiento a nivel de hardware. Da una buena idea de cómo este tipo de comportamiento del bus se escala en equipos comerciales sitio de Ben Eater
  • Me encantan este tipo de análisis de bugs tan interesantes. Solo logro seguir como el 60% del código ensamblador, pero las explicaciones escritas que lo acompañan ayudan mucho a entenderlo. Y estas historias donde se descubre, después de tanto tiempo, un bug en software clásico son especialmente entretenidas

    • Los sistemas de esa época son todavía más interesantes porque no tenían la mayoría de las comprobaciones que hoy son esenciales en sistemas embebidos, haya o no posibilidad de conexión en red. En la era del NES, muchísimas operaciones de lectura/escritura no eran más que alternar el voltaje de una línea, y solo en ese instante se sabía realmente qué iba a pasar. Se lograban efectos deseados alternando voltajes con una temporización sincronizada exactamente con la señal de blanking del CRT, y en Super Mario Bros. 3 incluso hacían trucos como alternar el multiplexor de RAM para cambiar el banco de sprites en cada actualización de pantalla. Como la diferencia regional entre televisores NTSC y PAL en la tasa de refresco actuaba como reloj para la lógica de renderizado, había que lanzar software distinto para cada tipo de TV. De verdad fue una época salvaje
  • Cuando me quedo atascado jugando algo en emulador, siempre sospecho: “¿y si es un bug del emulador?”. En este caso, yo habría pensado que el juego simplemente estaba diseñado para ser así de difícil. Y cuando la dificultad es muy alta también me he preguntado si será por la latencia del emulador, así que terminé armando mi propio mister FPGA para usarlo

    • Recuerdo que en Chrono Trigger hay una parte donde tienes que presionar cuatro teclas al mismo tiempo, pero la entrada USB solo podía transmitir tres a la vez, así que de cada cuatro intentos solo uno se registraba, y era frustrantemente difícil

    • Yo solo había jugado DKC en ZSNES, así que hasta leer el artículo no tenía idea de que esto era un bug del emulador. Siempre pensé que esa dificultad era parte del diseño del juego, y enterarme de que era un bug fue realmente impactante

    • Jugué mucho Bionic Commando cuando era niño, y al volver a jugarlo en emulador me pareció mucho más difícil. Después descubrí que era por un bug del emulador: los enemigos no desaparecían, así que necesitabas el doble de vidas. Aun así, una vez logré terminarlo así, pero no lo haría de nuevo

  • Los gráficos 3D prerenderizados de DKC 1 basados en SGI eran tecnología de punta para su época. Vector Man de Mega Drive usó una técnica parecida, pero no recibió tanta atención como DKC

    • En 1995 yo era exactamente el público objetivo de DKC, tenía 11 años, y los gráficos de este juego de verdad me impactaron. Incluso llegué a recibir un video promocional cerca del lanzamiento; esa cinta con escenas detrás de cámaras la vi muchas veces. Nunca tuve el juego, pero sí podía jugarlo en casa de amigos

    • De niño, los gráficos de DKC me daban una sensación rara de ser “falsos”. En esa época las revistas solían explicar de manera algo artificial que el SNES renderizaba personajes 3D en tiempo real, pero yo más o menos intuía que en realidad era algo más parecido a una animación tipo flipbook