Donkey Kong Country 2 e Open Bus
(jsgroth.dev)- 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
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 pessoaFico 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 #2como “carregar o número 2 no registrador A”. JáLDA 2parecia “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çaNuma 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 exatoExplicaçã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
#$1234induzem 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 erroHá 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 dolorosoConjuntos 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 #addresscomJMP address, por causa da memória muscular do assembly 6502. No Propeller,JMP #addresssalta para o endereço especificado, enquantoJMP addresssalta 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 vezOpen 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
$40ou$41ao executarLDA $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 MetroidGosto 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
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