1 pontos por GN⁺ 2025-07-02 | 1 comentários | Compartilhar no WhatsApp
  • O bug dos barris giratórios de Donkey Kong Country 2 ocorre no emulador ZSNES
  • O ZSNES não emula corretamente o comportamento de open bus, fazendo com que os barris girem permanentemente
  • Diferente do hardware real, no ZSNES um acesso incorreto à memória sempre retorna 0, causando o bug
  • No funcionamento correto, a lógica faz o barril parar de girar na direção exata entre 8 direções
  • Acredita-se que o problema venha de um pequeno erro de programação: uso de endereçamento absoluto em vez de endereçamento imediato

Donkey Kong Country 2 e o bug dos barris no emulador ZSNES

Donkey Kong Country 2 tem um bug famoso em que os barris giratórios de algumas fases não funcionam corretamente no antigo emulador de SNES chamado ZSNES

Ao entrar no barril, o normal seria ele girar apenas enquanto o direcional esquerdo/direito estivesse pressionado, mas no ZSNES um toque curto para a esquerda ou direita já faz o barril continuar girando para sempre naquela direção

Por causa desse bug, especialmente nas fases mais avançadas, as sequências com barris giratórios sobre espinhos ou obstáculos ficam muito mais difíceis do que os desenvolvedores pretendiam

Esse problema chegou a ser parcialmente documentado nos antigos fóruns do ZSNES, mas como eles desapareceram, hoje é difícil encontrar material relacionado

A causa do bug - emulação de Open Bus

A causa fundamental desse bug é que o ZSNES não emula o comportamento de open bus

  • open bus é um comportamento que ocorre em plataformas antigas como o SNES ao ler endereços de memória inválidos
  • No hardware real, o valor mais recente colocado no barramento é retornado
  • A principal CPU do SNES é a 65C816 (65816)
  • A 65816 é uma versão de 16 bits da 6502, tem barramento de endereços de 24 bits e usa um esquema de bancos de memória

No código dos barris giratórios de DKC2, ao acessar endereços inválidos (Bank $B3 em $2000 e $2001), o hardware retorna 0x2020 via open bus

No ZSNES, como essa função não existe, 0 é sempre retornado, e isso causa o bug

Como o código do jogo funciona

A rotina do jogo relacionada aos barris giratórios segue o fluxo abaixo

  • Soma a direção atual do barril com a quantidade de rotação (velocidade) e salva isso em uma variável temporária
  • Mede a mudança de direção com uma operação XOR e faz uma operação AND do resultado com o valor lido via open bus
  • Se o resultado do AND for 0, a rotação continua; se não for 0, ela para e a direção é arredondada e alinhada para uma das 8 direções

No hardware real, o valor de open bus é 0x2020, mas se 0 for retornado, a rotação continua infinitamente

A hipótese é que essa lógica originalmente deveria fazer a operação AND com um valor imediato (address #$2000), mas por engano usou um endereço absoluto (address $2000)

Por causa da característica de open bus no hardware, porém, na prática os dois modos acabam funcionando normalmente

Correção e conclusão

Outros emuladores de SNES, como o Snes9x, corrigiram esse bug com uma solução hardcoded, enquanto o ZSNES, por estar descontinuado, nunca recebeu patch

Se o opcode da instrução AND nessa rotina for alterado de 0x2D para 0x29 (AND #$2000), o barril giratório passa a funcionar corretamente mesmo sem o comportamento de open bus

Esse problema não acontece no hardware real nem em emuladores modernos

No fim, esse bug é um exemplo de falha causada pela combinação de falta de suporte a open bus na emulação com um erro de programação


Contexto adicional: arquitetura da 65816 e mapa de memória do SNES

A CPU 65816 tem barramento de endereços de 24 bits, mas normalmente usa uma combinação de banco de 8 bits com deslocamento de 16 bits

  • O contador de programa (PC) tem 16 bits, e o registrador de banco do programa (PBR, K) compõe o endereço completo
  • O banco de dados (DBR, B) é usado para selecionar o banco em operações de dados
  • A pilha de hardware e a direct page sempre existem no banco $00

O mapa de memória do SNES também foi projetado com base na 65816, então costuma ser mais eficiente pensar os endereços como banco de 8 bits + deslocamento de 16 bits

Encerramento

Esse caso mostra que características de hardware legado (como open bus) podem virar bugs inesperados em emulação

Os desenvolvedores deveriam ter usado endereçamento imediato, mas foi um caso em que, por acaso, o endereço absoluto também funcionava normalmente

Hoje, isso sugere que emular até mesmo o comportamento de open bus é muito importante para reproduzir software antigo com precisão

1 comentários

 
GN⁺ 2025-07-02
Comentários do Hacker News
  • Como programador de assembly 6502, já perdi incontáveis horas por esquecer o símbolo # e acabar fazendo acesso à memória em vez de usar um valor imediato; isso é ainda mais traiçoeiro porque às vezes o erro “funciona” por sorte. Mas um caso ainda pior do que o problema de barramento flutuante do exemplo é código que depende de RAM não inicializada: como o valor inicial varia de um DRAM para outro, tudo pode funcionar sempre no seu computador ou emulador, mas falhar em outra máquina com DRAM diferente. Normalmente você descobre esse tipo de problema faltando menos de 15 minutos para apresentar numa demoparty, quando o código não roda no hardware de outra pessoa

    • Fico curioso se realmente existiu alguma arquitetura com CPU 6502 usando memória dinâmica. Pela minha experiência, essa plataforma sempre usava apenas RAM estática

    • 6502 foi minha primeira linguagem assembly, e eu pensava em LDA #2 como “carregar o número 2 no registrador A”. Já LDA 2 parecia “carregar o valor da posição de memória 2”, então eu tentava evitar esse erro desde o começo por causa dessa diferença

    • Numa situação dessas, talvez até valha a pena passar o código por um LLM. Eles costumam ser bons em encontrar esse tipo de typo ou ponto de erro com grande impacto

  • Quando vi a expressão Open Bus escrita com maiúsculas, achei que fosse algum protocolo ou padrão antigo de barramento. Depois entendi que significava apenas o barramento não estar conectado a lugar nenhum, porque nenhum dispositivo de memória era ativado no endereço indicado pelo decodificador ($2000). Um emulador antigo, que se comportava diferente do hardware real, acabou revelando o problema causado por esquecer o modo imediato (#) e assim não ler nada da memória. A solução foi mudar a instrução para endereçamento imediato, o que também deixou o código cerca de 2 us mais rápido, já que ele deixa de fazer a leitura de memória. Ainda assim, parece uma diferença sem muita relevância fora do hardware real, especialmente em emuladores sem timing totalmente exato

    • Explicação de que (alguns) emuladores de SNES hoje praticamente já alcançaram perfeição em termos de timing. Ainda assim, uma diferença de 2 us praticamente não produz efeito perceptível, salvo em casos muito excepcionais. Artigo relacionado: How SNES emulators got a few pixels from complete perfection

    • Há vários casos de jogos com bugs ocultos que só vieram à tona muito tempo depois, graças a novas arquiteturas. A Rare, por exemplo, lançou vários jogos assim. Em Donkey Kong 64 existe um vazamento de memória fatal depois de 8 a 9 horas seguidas de jogo, mas emuladores com save state acumulam esse tempo rapidamente e expõem o bug com facilidade. Havia uma teoria de que o Memory Pak incluído no lançamento servia para esconder esse bug, mas pesquisas recentes indicam que nem a Rare nem a Nintendo sabiam dele na época

  • Já encontrei o fenômeno de open bus da PPU em Puyo Puyo de SNES. Foi durante o trabalho no recurso RunAhead do RetroArch, ao investigar por que os save states não batiam: o valor lido do open bus da PPU mudava depois de carregar o estado, então o log de trace de execução da CPU deixava de coincidir

  • Em 6502 e códigos parecidos, eu frequentemente confundo endereços de memória com valores imediatos. Acho que notações como #$1234 induzem ao erro, e já ouvi dizer que até Chuck Peddle se arrependeu profundamente dessa sintaxe. No IDE, destacar o # em vermelho ajudava um pouco a evitar isso. Até desenvolvedores da Rare cometeram esse erro

    • Há bastante tempo, tive um problema parecido no GNU assembler em modo intel_syntax noprefix: ali existe uma ambiguidade sintática ao referenciar na frente uma constante nominal de valor imediato, que pode ser interpretada como endereço de memória ou símbolo. O resultado foi a criação de um endereço temporário de memória, esperando o símbolo ser resolvido na etapa de link, e encontrar esse bug foi realmente doloroso

    • Conjuntos de instruções como o ARM, que exigem instruções separadas para lidar com memória, evitam esse tipo de confusão na raiz

  • Pelo que sei, open bus só aparece em sistemas de barramento síncrono simples mais antigos. A maioria dos outros sistemas retorna um valor fixo, como tudo zero ou tudo um, ao acessar um endereço inexistente; ou então isso é tratado por handshaking do protocolo do barramento, em que o mestre pode detectar a ausência de resposta, como no master abort do PCI

  • Ao programar o chip Parallax Propeller, já passei repetidamente por um erro parecido. Costumo confundir JMP #address com JMP address, por causa da memória muscular do assembly 6502. No Propeller, JMP #address salta para o endereço especificado, enquanto JMP address salta para o valor lido naquele endereço. O problema é que esse bug às vezes também funciona, então eu perco horas tentando descobrir a causa até tudo parar de vez

  • Open bus significa que as linhas do barramento de dados estão literalmente abertas, deixando o circuito em estado aberto. Quando a CPU coloca no barramento um endereço não mapeado ou somente leitura, nenhum hardware responde e as linhas ficam flutuando — ou seja, undefined behavior em nível de hardware. Para entender o que realmente acontece, é preciso olhar para a estrutura física do barramento. Ele é um condutor longo que transmite sinais entre placa-mãe e cartucho, separado do plano de terra por uma placa fina isolante. Essa estrutura funciona como uma espécie de capacitor, então acaba “segurando” por algum tempo a tensão do último sinal transmitido. Por isso, em open bus, você obtém o efeito de reler o último valor que passou ali. Jogos como DKC2 acabam dependendo disso sem querer, e a porta serial do controle do NES também, já que só os bits baixos recebem sinal e os bits altos ficam em open bus, de modo que certos jogos esperam $40 ou $41 ao executar LDA $4016. O fenômeno de open bus também é aproveitado em estratégias de speedrun como o credits warp de Super Mario World, envolvendo corrupção de memória ou execução arbitrária de código. Há exceções, porém, causadas por cartuchos fora do padrão, resistores pull-up/pull-down ou interações incomuns com DMA, como Horizontal DMA. Por exemplo, se uma transferência HDMA do SNES ocorre no meio de uma instrução, isso pode afetar o timing da leitura de open bus; em exploits de speedrun de Super Metroid, isso pode inserir valores anormais entre blocos de memória que se queria copiar, quebrando o exploit. Como resultado, no hardware original ou em emuladores extremamente precisos isso pode causar crash, enquanto a maioria dos emuladores e relançamentos oficiais não implementa perfeitamente esse comportamento de nicho, e a estratégia acaba funcionando normalmente. A conclusão do recorde mundial TAS de Super Metroid também depende desse comportamento de HDMA. Manipulando a posição dos inimigos para alterar o timing da CPU, faz-se o HDMA colocar o valor desejado no open bus e, por fim, executar a entrada do controle como código, permitindo execução arbitrária de código. vídeo do credits warp de Super Mario World, vídeo sobre uso de HDMA, vídeo do exploit de DMA em Super Metroid, recorde TAS de Super Metroid

    • A série de vídeos do computador 6502 em protoboard do Ben Eater me ajudou bastante a entender como esse comportamento de hardware funciona. Dá para sentir como esse comportamento de barramento se amplia em equipamentos comerciais site do Ben Eater
  • Gosto desse tipo de conteúdo de análise de bugs interessantes. Só consigo acompanhar algo como 60% do assembly, mas as explicações em texto junto ajudam bastante na compreensão. E essas histórias de bugs em softwares clássicos que passaram despercebidos por tanto tempo são especialmente divertidas

    • Esses sistemas daquela época são ainda mais interessantes porque não tinham a maior parte das verificações que hoje seriam essenciais em sistemas embarcados, houvesse ou não possibilidade de conexão em rede. Na era do NES, muitas leituras e escritas eram pouco mais do que alternar a tensão nas linhas, e o que ia acontecer de fato só se sabia naquele momento. Os efeitos desejados eram obtidos alternando tensões em sincronia exata com o sinal de blanking do CRT, e em Super Mario Bros. 3 havia truques como alternar o multiplexador de RAM para mudar o banco de sprites no momento de atualização da tela. Como a diferença regional entre TVs NTSC e PAL fazia a taxa de varredura atuar como clock da lógica de renderização, era preciso lançar software separado para cada tipo de TV. Era uma época realmente selvagem
  • Quando fico travado em algum ponto jogando em emulador, sempre suspeito: “será que é bug do emulador?”. Nesse caso, eu provavelmente teria achado que o jogo era simplesmente assim mesmo. E quando a dificuldade é muito alta, também costumo pensar “será que é por causa da latência do emulador?”, a ponto de eu mesmo ter montado um mister FPGA para usar

    • Lembro de uma parte em Chrono Trigger em que era preciso apertar quatro teclas ao mesmo tempo, mas a entrada USB só conseguia transmitir três de uma vez, então apenas uma em cada quatro tentativas era registrada, o que deixava tudo extremamente difícil e frustrante

    • Eu só tinha jogado DKC no ZSNES, então até ler o artigo eu não fazia ideia de que isso era bug de emulador. Sempre achei que a dificuldade fazia parte do design do jogo, e descobrir que era bug foi realmente chocante

    • Eu jogava muito Bionic Commando quando era criança, mas ao revisitá-lo em emulador ele pareceu muito mais difícil. Depois descobri que era por causa de um bug do emulador: os inimigos não desapareciam, então eu precisava do dobro de vidas. Ainda consegui zerar uma vez assim, mas não faria isso de novo

  • Os gráficos 3D pré-renderizados do DKC 1, feitos com base em SGI, eram tecnologia de ponta na época. Vector Man, do Mega Drive, usava uma técnica semelhante, mas nunca recebeu tanta atenção quanto DKC

    • Em 1995 eu estava exatamente na faixa etária-alvo de DKC, 11 anos, e os gráficos desse jogo foram realmente chocantes. Perto do lançamento, eu até recebi um vídeo promocional e assisti várias vezes àquela fita com cenas de bastidores. Eu não tinha o jogo, mas às vezes conseguia jogar na casa de amigos

    • Quando criança, eu tinha a sensação de que os gráficos de DKC eram meio “falsos”. As revistas da época às vezes davam a entender, de forma meio artificial, que o SNES renderizava personagens 3D em tempo real, mas eu já suspeitava vagamente que na verdade era algo mais parecido com animação em flipbook