Projeto WideNES - vá além dos limites da tela do NES

imagem

Em meados dos anos 80, o Nintendo Entertainment System (NES) era um console obrigatório. O melhor som, os melhores gráficos e os melhores jogos entre todos os consoles da época - o console expandiu os limites do possível. Até agora, projetos como Super Mario Bros. , The Legend of Zelda e Metroid são considerados alguns dos melhores jogos de todos os tempos.

Mais de 30 anos após o lançamento do NES, os jogos clássicos são ótimos, o que não pode ser dito sobre o hardware no qual eles trabalharam. Com uma resolução de apenas 256x240, o console do NES não podia fornecer jogos com espaço suficiente. No entanto, os destemidos desenvolvedores conseguiram se encaixar nos mundos incríveis e inesquecíveis dos jogos da NES: as masmorras labirínticas de The Legend of Zelda , vastos espaços do planeta em Metroid , níveis brilhantes de Super Mario Bros. . No entanto, devido às limitações de hardware do NES, os jogadores nunca poderiam ir além de 256x240 ...

Até recentemente.

Apresento a vocês o projeto wideNES - uma nova maneira de jogar clássicos do NES!



O wideNES é uma nova tecnologia para marcar de maneira automática e interativa os jogos da NES em tempo real .

Quando os jogadores se movimentam pelo nível, wideNES registra a tela, construindo gradualmente um mapa da parte explorada do mundo. Nos níveis subsequentes, o wideNES sincroniza a jogabilidade na tela com o mapa gerado, essencialmente permitindo que os jogadores vejam mais “olhando” além dos limites da tela do NES! O melhor de tudo é que a maneira como você marca jogos wideNES é totalmente universal , o que permite que uma ampla variedade de jogos NES funcione com wideNES sem nenhuma configuração!

Mas como tudo isso funciona?



Se você quiser verificar a largura do NES antes de ler o artigo, por favor! ANESE é o emulador de NES que escrevi e atualmente é o único emulador que implementa wideNES. No entanto, vale ressaltar que a ANESE não é o melhor emulador de NES do mundo, em termos de precisão da UI e da emulação. A maioria dos recursos (incluindo a inclusão do wideNES) está disponível apenas na linha de comando e, embora muitos jogos populares funcionem bem, outros podem se comportar de maneiras inesperadas.



Como o wideNES funciona


Antes de se aprofundar nos detalhes, é importante explicar brevemente como o NES renderiza gráficos.

Transferência de pixels usando PPU


O coração do NES é o venerável processador MOS 6502. No final dos anos 70 e início dos anos 80, 6502 eram usados ​​em todos os lugares e trabalhavam em máquinas lendárias como o Commodore 64, o Apple II e muitos outros. Era barato, fácil de programar e poderoso o suficiente para ser perigoso.

Como complemento do 6502 no console do NES, havia um poderoso coprocessador gráfico chamado Picture Processing Unit (PPU). Comparado aos coprocessadores de vídeo simples usados ​​em sistemas mais antigos, o PPU é uma grande melhoria em termos de usabilidade. Por exemplo, cinco anos antes do lançamento do NES, o processador Atari 2600 6502 foi usado para transmitir instruções gráficas ao coprocessador para cada linha raster , o que deixou pouco tempo para o processador executar a lógica do jogo. Para comparação: o PPU precisava de apenas alguns comandos por quadro , e isso deu a 6502 tempo suficiente para criar uma jogabilidade interessante e inovadora.

A PPU é um chip incrível, sua maneira de renderizar gráficos é quase como o trabalho das GPUs modernas, e uma série completa de artigos será necessária para explicar completamente suas funções. Como o wideNES usa apenas um pequeno subconjunto de funções da PPU, é suficiente considerá-las apenas brevemente:

  • Resolução: 256x240 pixels, 60 Hz
  • Funciona independentemente da CPU
    • Comunica-se com a CPU usando E / S com mapeamento de memória (intervalo de endereços 0x2000 - 0x2007)
  • 2 camadas de renderização: camada sprite e camada de fundo
    • Camada de Sprite
      • Cada sprite individual pode ser colocado em qualquer lugar da tela.
      • Ótimo para objetos em movimento: jogador, inimigos, conchas
      • Até 64 sprites de 8x8 pixels
    • Camada de fundo
      • Amarrado a uma grade
      • Ótimo para elementos estáticos: plataformas, grandes obstáculos, decorações
      • Memória de vídeo é suficiente para armazenar 64x30 blocos de tamanho 8x8 pixels
        • Verdadeira resolução interna de 512x240, com uma janela de visualização de 256x240
        • Suporta rolagem de hardware para alterar a janela de visualização de 256x240
          • O registro PPUSCROLL (endereço 0x2005) controla o deslocamento da viewport em X / Y

Depois de lidar com essa breve visão geral, vamos para o mais interessante: como o wideNES funciona?

Ideia principal


No final de cada quadro, a CPU envia as informações de alteração para a PPU. Isso inclui novas posições de sprite, novos dados de nível e, o que é crítico para wideNES, novas compensações de viewport . Como o wideNES funciona no emulador, é muito fácil acompanhar os valores gravados no registro PPUSCROLL, o que significa que é incrivelmente fácil calcular o quanto a tela se moveu entre dois quadros!

Hmm, o que acontecerá se, em vez de desenhar cada novo quadro diretamente em cima do quadro antigo, novos quadros serão desenhados sobrepostos no quadro anterior, mas mudados para o valor atual de rolagem? Então, com o tempo, uma parte cada vez maior do nível permanecerá na tela, construindo gradualmente uma imagem completa do nível!

Para verificar se essa ideia teve algum valor, rapidamente esbocei a primeira implementação.

Compilando ...
A iniciar ...
Baixar Super Mario Bros. ...

Voila!


Funcionou!

Parece ser ...



Outra abordagem: por que não extrair níveis diretamente dos arquivos ROM?


Mesmo sem considerar os detalhes da implementação, torna-se óbvio que essa técnica tem uma séria limitação: um mapa do jogo completo pode ser coletado apenas quando o jogador explora independentemente o jogo inteiro.

E se houvesse alguma maneira de extrair níveis de ROMs NES cruas ?!

Essa técnica pode existir?

Bem, provavelmente não.

Se você jogar dois jogos para o NES, poderá garantir que eles tenham apenas uma coisa em comum: ambos trabalham para o NES. Tudo o resto pode ser completamente diferente! Essa incompatibilidade é um desastre real, porque os jogos NES têm essencialmente um número infinito de opções para armazenar dados de nível!

Algumas pessoas extraíram níveis completos por engenharia reversa, da maneira como armazenam os dados de níveis de alguns jogos (às vezes com a criação de editores de mapas com todos os recursos!), Mas essa é uma tarefa difícil, exigindo muito trabalho, perseverança e inteligência.

Para extrair dados de nível da ROM, é necessário determinar quais partes da ROM são código (não dados), e isso é difícil, porque encontrar todo o código em um arquivo binário é equivalente a um problema de parada !

O WideNES usa uma abordagem muito mais simples: em vez de adivinhar como o jogo empacotou os dados de nível na ROM, o wideNES apenas inicia o jogo e acompanha a saída!



Rolando além de 255


O NES é um sistema de 8 bits, ou seja, o registro PPUSCROLL pode receber apenas valores de 8 bits. Isso limita o deslocamento máximo de rolagem a 255 pixels, ou seja, o número máximo de 8 bits. Não é por acaso que a resolução da tela do NES é de 240x256 pixels, ou seja, a troca de 255 pixels é suficiente para rolar a tela inteira.

Mas o que acontece ao rolar além de 255?

Primeiramente, os jogos redefinem o registro PPUSCROLL para 0. Isso explica por que o SMB é transportado para o início quando Mario se move muito para a direita.

Em seguida, para compensar as restrições PPUSCROLL de 8 bits, os jogos atualizam outro registro PPU: PPUCTRL (endereço 0x2000). Os 2 bits inferiores do PPUCTRL definem o "ponto inicial" da cena atual em incrementos de tela cheia. Por exemplo, escrever um valor 1 desloca a janela de visualização para a direita em 256 pixels, enquanto um valor 2 desloca a janela de visualização em 240 pixels. O deslocamento PPUCTRL é empurrado para a pilha com o registro PPUSCROLL, que permite rolar a tela horizontalmente dentro de 512 pixels ou verticalmente dentro de 480 pixels.

Mas construa, existe apenas memória de vídeo suficiente para telas de dois níveis? O que acontece quando a janela de exibição rola muito para a direita e "vai além" da VRAM? Para lidar com esse caso, o PPU implementa a convolução: todas as partes da janela de exibição fora da memória de vídeo selecionada são simplesmente recolhidas na borda oposta da memória de vídeo.

Essa dobragem, combinada com a manipulação inteligente de registros PPUSCROLL e PPUCTRL, permite que os jogos NES criem a ilusão de mundos infinitamente altos / largos! Graças ao carregamento lento de parte do nível fora da janela de visualização e à rolagem gradual para dentro, os jogadores nunca percebem que dentro do VRAM eles realmente “correm em círculos”!

Uma excelente ilustração do wiki nesdev mostra como Super Mario Bros. usa essas propriedades para criar níveis maiores que duas telas:


Vamos voltar à pergunta que estamos discutindo: como o wideNES lida com a rolagem além de 256?

Bem, francamente, o wideNES ignora completamente o registro PPUCTRL e apenas acompanha a diferença do PPUSCROLL entre os quadros!

Se o PPUSCROLL saltar inesperadamente para cerca de 256, o que geralmente significa que o personagem do jogador foi movido para a esquerda / para cima na tela e se ele saltar inesperadamente para cerca de 0, isso geralmente significa que o jogador foi movido para a direita / para baixo na tela.

Embora essa heurística possa parecer simples - e é - de fato, ela funciona muito bem!

Depois de implementar essa heurística, Super Mario Bros. , Metroid e muitos outros jogos funcionaram quase perfeitamente!

Fiquei emocionado, então fui em frente e carreguei mais um clássico do NES - Super Mario Bros. 3 ...


Hmm ... Não é muito bonito.

Ignorando elementos estáticos da tela


Muitos jogos possuem elementos de interface do usuário estáticos nas bordas da tela. No caso do SMB3, esta é a coluna à esquerda e a barra de status está na parte inferior do status.

Por padrão, as amostras wideNES com incrementos de 16 pixels nas bordas da tela, ou seja, todos os elementos estáticos nas bordas são amostrados! Não é bom!

Para contornar esse problema, o wideNES implementa regras e heurísticas que tentam reconhecer e mascarar automaticamente elementos de tela estáticos.

Em geral, os jogos NES usam três tipos diferentes de elementos de tela estáticos: HUDs, máscaras e barras de status.

HUD - sem problemas


Se um jogo impõe um HUD no topo de um nível, é provável que o HUD consista em vários sprites. Exemplo: HUD no Metroid .

Felizmente, esses HUDs não causam problemas, porque o wideNES atualmente simplesmente ignora a camada de sprite. Ótimo!

Máscaras - em nenhum lugar mais fácil


A PPU possui um recurso que permite aos jogos mascarar os 8 pixels mais à esquerda da camada de plano de fundo. É ativado configurando o segundo bit do registrador (endereço 0x2001). Muitos jogos usam esse recurso, mas explicar por que o fazem está além do escopo deste artigo.

Reconhecer a máscara incluída é incrivelmente simples: wideNES apenas mantém o controle do valor PPUMASK e ignora os 8 pixels mais à esquerda quando o segundo bit é definido no registro!

Parece que a implementação desta regra simples resolveu o problema com o SMB3 :


... bem, ou quase eliminado.

As barras de status são as mais difíceis


Devido às limitações da PPU a qualquer momento na tela, não pode haver mais do que 64 sprites; além disso, a qualquer momento em cada linha de varredura, não pode haver mais do que 8 sprites. Essa restrição impede que os desenvolvedores criem HUDs complexos a partir de sprites e os força a usar partes da camada de segundo plano para exibir informações.

Além das máscaras, não há maneira fácil no PPU de separar a camada de segundo plano na área de jogo e na área de status. Portanto, os desenvolvedores fizeram truques, levando a várias maneiras pouco ortodoxas de criar painéis de status ...

O WideNES usa várias heurísticas para reconhecer diferentes tipos de painéis de status, mas para economizar tempo, considerarei apenas um dos mais interessantes: rastreamento de IRQ no meio do quadro.

Rastreamento IRQ de quadro intermediário


Diferentemente das GPUs modernas, com grandes buffers de quadro interno, as PPUs geralmente não têm um buffer de quadro! Para economizar espaço, o PPU armazena cenas como uma grade de blocos de 64x32 de 8x8 pixels. Em vez de pré-calcular os dados de pixel, os blocos são armazenados como ponteiros para a Memória CHR (Memória de Caracteres), que contém todos os dados de pixel.

Desde que o NES foi desenvolvido nos anos 80, o PPU foi criado sem levar em conta as modernas tecnologias de exibição. Em vez de renderizar o quadro inteiro ao mesmo tempo, o PPU emite o sinal de vídeo NTSC, que deve ser exibido em uma tela CRT que exibe vídeo pixel por pixel , linha por linha , de cima para baixo, de cima para baixo, de cima para baixo, da esquerda para a direita.

Por que tudo isso é importante?

Como o PPU renderiza os quadros de cima para baixo, linha por linha, você pode enviar instruções de PPU para o meio do quadro para criar efeitos de vídeo que são impossíveis com qualquer outra abordagem! Esses efeitos podem ser simples (por exemplo, alterar a paleta) ou bastante complexos (por exemplo, você adivinhou, criando barras de status!).

Para explicar como uma gravação PPU de quadro intermediário pode criar barras de status, gravei um despejo de fatia de vídeo PPU e CHR Memory bruto para um único quadro SMB3 :


Tudo parece bem, nada de especial ... mas basta olhar para a barra de status! Ela está completamente distorcida!

Agora olhe para o mesmo despejo bruto, mas feito após a linha 196 ...


Sim, o nível parece horrível, mas a barra de status está ótima!

O que está acontecendo aqui?

O SMB3 define um cronômetro para acionar o IRQ (interrupção) exatamente após renderizar a linha raster 195. Passa as seguintes instruções para o manipulador de IRQ:

  • Defina PPUSCROLL como (0,0) (para que a barra de status permaneça no lugar)
  • Substituímos o cartão lado a lado na Memória CHR (ordenamos os gráficos da barra de status)

Como o restante da camada já está renderizado, a PPU não "atualiza" novamente o quadro. Em vez disso, continuará a renderizar com essas opções, exibindo uma bela barra de status sem distorção!

Vamos voltar ao wideNES: observando todos os IRQs no meio do quadro e lembrando a linha de varredura na qual eles ocorreram, o wideNES pode ignorar todas as linhas de varredura subsequentes no registro! Se ocorrer IRQ na linha de varredura acima de 240/2, todas as linhas anteriores serão ignoradas, porque a interrupção antecipada da linha de varredura significa que a barra de status pode estar na parte superior da tela.

Depois de implementar essa heurística, Super Mario Bros. 3 ganhou perfeito!




Considerei brevemente a possibilidade de usar uma biblioteca de visão computacional, como o OpenCV, para reconhecer painéis de status (ou outras áreas principalmente estáticas da tela), mas, como resultado, decidi abandoná-la. Usar uma biblioteca de visão computacional enorme, complexa e opaca é contrária aos ideais do wideNES, no qual tento usar regras e heurísticas compactas, simples e transparentes para obter resultados.



Reconhecimento de cena


Com exceção de alguns exemplos importantes (por exemplo, Metroid ), os jogos para o NES geralmente não passam em um nível imenso e inextricável. Pelo contrário, a maioria dos jogos de NES é dividida em muitas pequenas “cenas” independentes com portas ou telas de transição entre elas.

Como wideNES não tem o conceito de “cenas”, coisas ruins acontecem ao mudar de cena ...

Por exemplo, aqui está a primeira transição da cena de Castlevania , onde Simon Belmont entra no castelo de Drácula:


Uau, tudo está ruim! wideNES reescreveu completamente a última parte do nível com a primeira tela de um novo nível!

Obviamente, o wideNES precisa de alguma maneira de reconhecer as mudanças de cena. Mas qual?

Hashing Perceptivo!

Diferentemente das funções hash criptográficas , que tendem a distribuir uniformemente dados de entrada semelhantes pelo espaço de informações de saída, as funções de hash perceptivas tentam manter dados de entrada semelhantes "próximos" um do outro no espaço de dados de saída. Portanto, os hashes perceptivos são ideais para reconhecer imagens semelhantes!

As funções perceptivas de hash podem ser incrivelmente complexas, algumas delas são capazes de reconhecer imagens semelhantes se uma delas foi girada, dimensionada, esticada e as cores alteradas nela. Felizmente, o wideNES não requer funções complexas de hash porque é garantido que cada quadro tenha o mesmo tamanho. Portanto, wideNES usa o mais simples dos hashes perceptivos existentes: somando todos os pixels na tela!

É simples, mas funciona muito bem!

Por exemplo, veja como as transições entre cenas se destacam se você traçar o hash perceptivo ao longo do tempo em The Legend of Zelda :


Atualmente, o wideNES usa um limite fixo entre valores de hash perceptivos para concluir a transição entre cenas, mas o resultado está longe de ser ideal. Jogos diferentes usam paletas diferentes, e há muitos casos em que o wideNES pensa que ocorreu uma transição, mas na verdade não foi. Idealmente, o wideNES deve usar um valor de limite dinâmico, mas até agora o valor fixo o fará.

Depois de implementar essa nova heurística, o wideNES reconhece com sucesso a entrada de Simon de Castlevania no castelo e, consequentemente, cria uma nova tela.


E com essa decisão, colocamos em prática a última peça importante do quebra-cabeça wideNES.

Tendo implementado a serialização mais simples, finalmente consegui rodar o jogo para o NES, jogar em vários níveis e gerar automaticamente mapas de níveis!

O que espera wideNES no futuro?


O wideNES consiste em duas partes separadas: o kernel wideNES, que são as próprias regras / heurísticas subjacentes à tecnologia, e a implementação específica do wideNES dentro do emulador ANESE.

Aprimoramento do núcleo WideNES


Em primeiro lugar, o wideNES é propenso a um reconhecimento muito agressivo das transições entre as cenas. O número de falsos positivos pode ser minimizado usando um algoritmo de hash perceptivo mais adequado ou alternando para valores de limiar dinâmico entre hashes perceptivos.

Também é necessário trabalho adicional para reconhecer elementos de tela estáticos.Por exemplo, Megaman IV tem um IRQ no meio do quadro, mas não há barra de status, e é por isso que o wideNES ignora por engano a parte sólida do campo de jogo. Embora esse caso específico possa ser corrigido pelo ajuste manual, é melhor usar heurísticas mais inteligentes.

Alguns jogos do NES rolam a tela de maneiras "únicas". Um dos exemplos mais notáveis ​​é The Legend of Zelda , que usa PPUSCROLL para rolagem horizontal, mas usa um registro completamente diferente para rolagem vertical - PPUADDR. Zelda é um jogo bastante popular, então o NES implementa heurísticas especificamente para Zelda. Existem outros jogos com modos de rolagem "únicos" semelhantes, que também exigem heurísticas individuais.

Seria útil encontrar uma maneira de “costurar” cenas idênticas. Por exemplo, se um usuário jogar Super Mario Bros. Nível 1, mas rasteja no cano para entrar na caverna subterrânea com moedas, o wideNES criará duas cenas separadas para o Nível 1: cena A, nível até Mario entrar na zona com moedas e cena B, nível, a partir do momento quando Mario sai do cano e sobe para o mastro. Se o jogo for reiniciado e o Nível 1 for repetido sem entrar no canal, o wideNES simplesmente atualizará a cena A, que conterá um mapa de nível completo, mas a cena B será interrompida.

Finalmente, o wideNES deve acompanhar as transições entre as cenas. Sem esses dados, não será possível construir um gráfico de transição entre as cenas para gerar mapas mundiais de jogos que não consistem em um único mundo grande.

Melhorando a implementação do wideNES na ANESE


Atualmente wideNES é implementado apenas no emulador NES que escrevi sob o nome ANESE. O ANESE é um emulador muito espartano: a maioria das opções está oculta por trás dos sinalizadores da CLI, e a única interface do usuário implementada é a sobreposição de seleção de arquivo mais simples! Ele ainda está muito longe do nível de "produção".

Além da falta de UI, ANESE e wideNES, melhorias na compatibilidade e velocidade não prejudicariam. ANESE é o primeiro emulador que escrevi, e é perceptível!

Existem alguns problemas de compatibilidade - muitos jogos não funcionam corretamente ou nem iniciam. Felizmente, a imperfeição da ANESE não significa que wideNES seja uma tecnologia ruim. O wideNES é baseado em princípios comprovados que serão fáceis de implementar em outros emuladores!

Em termos de velocidade, ANESE e wideNES não são perfeitos, e mesmo em PCs relativamente poderosos, o desempenho às vezes pode ficar abaixo de 60fps! A ANESE e a wideNES precisam implementar muitas otimizações. Além da melhoria geral do kernel ANESE, é necessário melhorar a gravação de quadros NES, a renderização de mapas e a amostragem de hash.

Conclusão


No artigo, falei sobre os principais aspectos do wideNES, mas não consegui descrever muitos recursos pequenos. Por exemplo, wideNES armazena um mapa dos valores de hash e rolagem verdadeiros de cada quadro, que são usados ​​para ativar cenas repetidas. Esse e muitos outros recursos são descritos no código-fonte amplamente comentado do wideNES, publicado na página do projeto wideNES .

Trabalhar no wideNES foi uma experiência verdadeiramente surpreendente, mas com a abordagem do novo semestre acadêmico da Universidade Waterloe, duvido que, no futuro próximo, seja capaz de continuar desenvolvendo o wideNES. No momento, as principais funções do wideNES estão funcionando e fico feliz por poder escrever este post descrevendo algumas de suas tecnologias!

Tente usar wideNES e compartilhe seus sentimentos! Faça o download da ANESE , inicie o Super Mario Bros. , The Legend of Zelda ou Metroid , e jogue-os de uma nova maneira!

Source: https://habr.com/ru/post/pt421555/


All Articles