O relatório pode ser dividido em três partes:
- como o processador 6502 funciona e como emulá-lo usando JavaScript,
- como o dispositivo de saída gráfica funciona e como os jogos armazenam seus recursos,
- como o som é sintetizado usando áudio da web e como ele é paralelizado em dois fluxos usando um orlet de áudio.
Tentei dar dicas de otimização. Ainda assim, a emulação é o que importa, a 60 FPS resta pouco tempo para a execução do código.
- Olá a todos, meu nome é Zhenya. Agora haverá uma pequena conversa inusitada, sábado, sobre o projeto por muitos sábados. Vamos falar sobre emulação de sistemas de computador, que podem ser implementados no topo das tecnologias web existentes. Na verdade, a web já é muito rica em ferramentas e você pode fazer coisas absolutamente incríveis. Mais especificamente, falaremos do emulador para todos, provavelmente, o famoso console Dandy da década de 90, que na verdade é chamado de Nintendo Entertainment System.
Vamos lembrar um pouco de história. Tudo começou em 1983 quando a Famicom foi lançada no Japão. Foi lançado pela Nintendo. Em 1985, a versão americana foi lançada, que foi chamada de Nintendo Entertainment System. Nos anos 90, tínhamos a mesma região taiwanesa chamada Dandy, mas secretamente, este é um prefixo não oficial. E o último presente de ferro da Nintendo foi em 2016, quando o NES mini foi lançado. Infelizmente, não tenho um mini NES. Existe o SNES mini, o Super Nintendo. Veja como ele é pequeno e, neste slide, você pode ver a Lei de Moore em toda a sua glória.
Se olharmos para 1985 e a proporção do console para o joystick, e em 2016, podemos ver como tudo se tornou menor, porque as mãos das pessoas não mudam, o joystick não pode ficar menor, mas o próprio console ficou minúsculo.
Como já observamos, existem muitos emuladores. Nós não dissemos, mas pelo menos um funcionário notou. Essa coisa - SNES mini ou NES mini - não é realmente um decodificador de sinais de verdade. Esta é uma peça de hardware que emula o console. Este é, na verdade, um emulador oficial, mas que vem em uma forma de ferro muito engraçada.
Mas, como sabemos, desde os anos 2000, existem programas que emulam o NES, graças ao qual ainda podemos desfrutar de jogos dessa época. E existem muitos emuladores. Por que outro, especialmente em JavaScript, você me pergunta? Quando fiz isso, encontrei três respostas para essa pergunta para mim mesmo.
- , . - , . . , - , - . . . , , . , -.
- , , . , , , , NES — , , NTSC, 60 . 16 , . .
- . , . , , . , , — , . . , , .
Também assisti à apresentação de Matt Godbold, que também falou sobre emular o processador que roda o NES. Ele disse que é engraçado estarmos emulando uma coisa de nível tão baixo em uma linguagem de nível tão alto. Não temos acesso a hardware, trabalhamos indiretamente.
Vamos prosseguir para a consideração do que vamos emular, como vamos emular, etc. Começaremos com o processador. O próprio NES é icônico. Para a Rússia, é compreensível, este é um fenômeno cultural. Mas no Ocidente, e no Oriente, no Japão, também foi um fenômeno cultural, porque o console, de fato, salvou toda a indústria de videogames domésticos.
O processador também é instalado no icônico MOS6502. Qual é o seu significado? Na época em que foi lançado, seus concorrentes custavam US $ 180 e o MOS6502 custava US $ 25. Ou seja, esse processador lançou a revolução do computador pessoal. E aqui eu tenho dois computadores. O primeiro é o Apple II, todos sabemos e imaginamos o quão significativo este evento foi para o mundo dos computadores pessoais.
Há também um computador BBC Micro. Ele era mais popular na Grã-Bretanha, a BBC é uma empresa de televisão britânica. Ou seja, esse processador trouxe os computadores para as massas, graças a ele agora somos programadores, desenvolvedores front-end.
Vamos dar uma olhada no programa mínimo. O que precisamos para fazer um sistema de computação?
A própria CPU é um dispositivo bastante inútil. Como sabemos, a CPU executa o programa. Mas pelo menos para que este programa seja armazenado em algum lugar, é necessária memória. E, claro, está incluído no programa mínimo. E nossa memória consiste em células de oito bits, que são chamadas de bytes.
Em JavaScript, podemos usar arrays Uint8Array digitados para emular essa memória, ou seja, podemos alocar um array.
Para que a memória faça interface com o processador, existe um barramento. O barramento permite que o processador direcione a memória por meio de endereços. Os endereços não consistem mais em oito bits, como dados, mas em 16, o que nos permite endereçar 64 kilobytes de memória.
Existe um certo estado no processador, existem três registros - A, X, Y. Um registro é como um armazenamento para valores intermediários. O tamanho do registro é de um byte ou oito bits. Isso nos diz que o processador é de oito bits, ele opera com dados de oito bits.
Um exemplo de uso do registro. Queremos somar dois números, mas há apenas um barramento na memória. Acontece que você precisa armazenar o primeiro número em algum lugar no meio. Podemos salvá-lo no registro A, podemos pegar o segundo valor da memória, adicioná-lo e o resultado é novamente colocado no registro A.
Funcionalmente, esses registros são bastante independentes - podem ser usados como gerais. Mas eles têm um significado, como adição, o resultado é obtido no registro A e o valor do primeiro operando é obtido.
Ou, por exemplo, tratamos de dados. Falaremos sobre isso um pouco mais tarde. Podemos especificar o modo de endereçamento de deslocamento e usar o registrador X para obter o valor final.
O que mais está incluído no estado do processador? Existe um registro no PC que aponta para o endereço do comando atual, pois o endereço é de dois bytes.
Também temos o registro de Status, que indica os sinalizadores de status. Por exemplo, se subtrairmos dois valores e obtivermos negativos, um certo bit no registrador do sinalizador acende.
Finalmente, existe o SP, um ponteiro para a pilha. A pilha é apenas memória comum, não está separada de tudo o mais, de todos os outros programas. Existe simplesmente uma instrução do processador que controla este ponteiro SP. É assim que a pilha é implementada. Em seguida, examinamos uma grande ideia de computador que leva a essas soluções interessantes.
Agora sabemos que existe um processador, memória, estado no processador. Vamos ver qual é o nosso programa. Esta é uma sequência de bytes. Nem precisa ser consistente. O próprio programa pode estar localizado em diferentes partes da memória.
Podemos imaginar um programa, eu tenho um trecho de código aqui - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10. Este é um programa real para 6502. Cada byte deste programa, cada dígito neste array é entidade como opcode. Opcode - código de operação. “Então, novamente, um número comum.
Por exemplo, existe um opcode 169. Ele codifica duas coisas em si mesmo - primeiro, uma instrução. Quando executada, a instrução altera o estado do processador, da memória e assim por diante, ou seja, o estado do sistema. Por exemplo, adicionamos dois números, o resultado aparece no registro A. Esta é uma instrução de exemplo. Também temos uma instrução LDA, que consideraremos com mais detalhes. Ele carrega um valor da memória no registro A.
A segunda coisa que o opcode codifica é o modo de endereçamento. Ele dá instruções sobre onde obter seus dados. Por exemplo, se este for o modo de endereçamento IMM, ele diz: pegue os dados que estão na célula ao lado do contador do programa atual. Também veremos como esse modo funciona e como pode ser implementado em JavaScript.
Esse é o programa. Além desses bytes, tudo é muito semelhante ao JavaScript, apenas em um nível inferior.
Se você se lembra do que eu estava falando, pode haver um paradoxo engraçado. Nós, ao que parece, armazenamos o programa na memória e os dados também. Pode-se fazer a seguinte pergunta: um programa pode atuar como dados? A resposta é sim. Podemos mudar do próprio programa no momento da execução deste programa.
Ou outra pergunta: os dados podem ser um programa? Sim também. O processador não importa. Ele simplesmente, como um moinho, mói os bytes que lhe são fornecidos e segue as instruções. Uma coisa paradoxal. Se você pensar bem, é super inseguro. Você pode começar a executar um programa que consiste apenas de dados em uma pilha, etc. Mas a vantagem é que é super fácil. Não há necessidade de fazer circuitos complicados.
Esta é a primeira grande ideia que encontramos hoje. É chamada de arquitetura de von Neumann. Mas, na verdade, havia muitos co-autores lá.
Aqui está ilustrado. Existe o programa 1, opcode 169, seguido por 10, alguns dados. OK. Este programa também pode ser visto assim: 169 são dados e 10 é um opcode. Este seria um programa legal para 6502. Todo este programa, novamente, pode ser considerado como dados.
Se tivermos um compilador, podemos construir algo, colocá-lo neste pedaço de memória, e será uma coisa muito engraçada.
Vamos dar uma olhada na primeira parte do nosso programa - instruções.
6502 fornece acesso a 73 instruções, incluindo aritmética: adição, subtração. Sem multiplicação e divisão, desculpe. Existem operações de bits, tratam da manipulação de bits em palavras de oito bits.
Existem saltos que são proibidos em nosso frontend: a instrução jump, que simplesmente transfere o contador do programa para alguma parte do código. Isso é proibido na programação, mas se você estiver lidando com nível baixo, esta é a única maneira de fazer ramificações. Existem operações para a pilha, etc. Elas são agrupadas. Sim, temos 73 instruções, mas se você olhar para os grupos e o que eles fazem, não há realmente muitos deles e são todos muito semelhantes.
Voltemos à instrução LDA. Como dissemos, isso é "carregar o valor da memória no registrador A". É assim que pode ser super simples em JavaScript. Na entrada está o endereço que o modo de endereçamento nos fornece. Mudamos o estado interno, dizemos que this._a é igual ao valor lido da memória.
Ainda precisamos definir esses dois campos de bits no registrador de status - um sinalizador zero e um sinalizador negativo. Há muitas coisas bitwise aqui. Mas se você fizer um emulador, torna-se uma segunda natureza fazer esses ORs, negativos, etc. A única coisa engraçada aqui é que existe tal% 256 no segundo ramo. Isso nos remete, novamente, à natureza de nossa amada linguagem JavaScript, ao fato de que ela não possui valores digitados. O valor que colocamos em Status pode ir além de 256, que cabe em um byte. Temos que lidar com esses truques.
Agora vamos dar uma olhada na parte final de nosso opcode, o modo de endereçamento.
Temos 12 modos de endereçamento. Como dissemos antes, eles nos permitem obter e indicar para a instrução de onde obter os dados.
Vamos dar uma olhada em três coisas. O último é o ABS, modo de endereçamento absoluto, vamos começar com ele, peço desculpas pelo pequeno constrangimento. Ele faz algo assim. Fornecemos a ele o endereço completo, 16 bits, como entrada. Ele nos obtém o valor dessa célula de memória. Em assembler, na segunda coluna, você pode ver o que parece: LDA $ ccbb. ccbb é um número hexadecimal, um número comum, simplesmente escrito em uma notação diferente. Se você se sentir desconfortável aqui, lembre-se de que este é apenas um número.
Na terceira coluna, você pode ver como fica em código de máquina. À frente está o opcode - 173, destacado em azul. E 187 e 204 já são dados de endereço. Mas como estamos operando com valores de oito bits, precisamos de dois locais de memória para escrever o endereço.
Também esqueci de dizer que o opcode é executado há algum tempo na CPU, tem um certo custo. LDA com endereçamento absoluto leva quatro ciclos de CPU.
Aqui você já pode entender porque tantos modos de endereçamento são necessários. Considere o próximo modo de endereçamento, ZP0. Este é o modo de endereçamento de página zero. E a página zero são os primeiros 256 bytes alocados na memória. Esses são endereços de zero a 255.
Em assembler, novamente, LDA * 10. O que esse modo de endereçamento faz? Ele diz: vá para a página zero, aqui nestes primeiros 256 bytes, com tal e tal deslocamento. neste caso, 10 e obtenha o valor a partir daí. Aqui já notamos uma diferença significativa entre os modos de endereçamento.
No caso de endereçamento absoluto, precisamos, em primeiro lugar, de três bytes para escrever tal programa. Em segundo lugar, precisamos de quatro ciclos de CPU. E no modo de endereçamento ZP0, levou apenas três ciclos de CPU e dois bytes. Mas sim, perdemos flexibilidade. Ou seja, só podemos colocar nossos dados na primeira página, esta aqui.
O modo de endereçamento final do IMM diz: pegue dados da célula ao lado do opcode. Este LDA # 10 em assembler faz isso. E acontece que o programa se parece com [169, 10]. Já requer dois ciclos de CPU. Mas aqui está claro que também perdemos flexibilidade e precisamos que o opcode esteja próximo aos dados.
Implementar isso em JavaScript é fácil. Aqui está um exemplo de código. Existe um endereço. Este é o endereçamento IMM, que obtém dados do contador do programa. Simplesmente dizemos que nosso endereço é um contador de programa e o incrementamos em um para que da próxima vez que o programa for executado, ele pule para a próxima instrução.
Aqui está uma coisa engraçada. Agora podemos ler código de máquina como desenvolvedores front-end. E a gente até sabe ver o que está escrito aí no montador.
Já sabemos tudo de que precisamos em princípio. Existe um programa que consiste em bytes. Cada byte é um opcode, cada opcode é uma instrução e assim por diante.Vamos ver como nosso programa é executado. E é executado apenas nesses ciclos de CPU.
Como esse código pode ser feito? Exemplo. Precisamos ler o opcode do contador do programa e, em seguida, aumentá-lo em um. Agora precisamos decodificar esse opcode em uma instrução e no modo de endereçamento. Venha para pensar sobre isso, o opcode é um número primo, 169. E em um byte temos apenas 256 números. Podemos fazer um array com 256 valores. Cada elemento desse array simplesmente nos referirá a qual instrução usar, que modo de endereçamento é necessário e quantos ciclos serão necessários. Ou seja, é super simples. E a matriz que tenho está apenas no estado do processador.
Em seguida, apenas executamos a função do modo de endereçamento na linha 36, que nos fornece o endereço, e as instruções de alimentação.
A última coisa que precisamos fazer é lidar com os loops. opcodeResolver retorna o número de ciclos, nós os escrevemos na variável restanteCycles. Olhamos para cada ciclo do processador: se houver zero ciclos restantes, podemos executar o próximo comando, se for maior que zero, simplesmente diminuímos em um. E isso é tudo, super simples. É assim que o programa roda no 6502.
Mas como já dissemos, o programa pode estar localizado em diferentes partes da memória, em diferentes proporções, etc. Como um processador pode saber por onde começar a executar este programa? Precisamos de um int main como este do mundo C.
Na verdade, tudo é simples. O processador tem um procedimento para redefinir seu estado. Neste procedimento, pegamos o endereço do comando inicial do endereço 0xfffxc. 0xfffxc é novamente um número hexadecimal. Se você se sentir desconfortável, marque, este é o número normal. É assim que eles são escritos em JavaScript, por meio de 0x.
Precisamos ler dois bytes do endereço, o endereço é de 16 bits. Lemos os bytes baixos desse endereço, os bytes altos do próximo endereço. E então adicionamos este caso com a mágica das operações de bits. Além disso, redefinir o estado do processador também redefine o valor nos registradores - registrador A, X, Y, ponteiro para a pilha, status. A reinicialização leva oito ciclos. Essa é a coisa.
Já sabemos tudo agora. Para ser sincero, foi meio difícil para mim escrever tudo isso, porque não entendi nada como testar. Estamos escrevendo um computador inteiro que pode executar qualquer programa já criado para ele. Como entender que estamos nos movendo corretamente?
Existe uma maneira excelente e maravilhosa! Pegamos duas CPUs. O primeiro é o que fazemos, o segundo é o CPU de referência, sabemos com certeza que funciona bem. Por exemplo, existe um emulador para o NES, o nintendulator, que é considerado uma CPU de referência.
Pegamos um determinado programa de teste, o executamos na CPU de referência e gravamos o estado do processador no log de estado de cada comando. Então pegamos este programa e o executamos em nossa CPU. E cada estado após cada comando é comparado com este log. Super ideia!
Claro, não precisamos de uma referência de CPU. Precisamos apenas de um log de execução do programa. Este log pode ser encontrado no Nesdev. Na verdade, um emulador de processador pode ser escrito, não sei, em alguns dias no fim de semana - é simplesmente fantástico!
E isso é tudo. Pegamos o log, comparamos o estado e temos um teste interativo. Executamos o primeiro comando, ele não está implementado no processador que estamos desenvolvendo. Nós o implementamos, vamos para a próxima linha do log e o implementamos novamente. Super rápido! Permite que você se mova rapidamente.
Arquitetura NES
Agora temos uma CPU, que é essencialmente o coração do nosso computador. E podemos ver do que é feita a arquitetura do próprio NES e como esses complexos sistemas de computador compostos são feitos. Porque se você pensar bem, bem, existe uma CPU, existe uma memória. Podemos receber valores, gravar, etc.
Mas no NES, em qualquer set-top box, também existe uma tela, aparelhos de som, etc. Precisamos aprender a trabalhar com periféricos. Você nem precisa aprender nada de novo para isso, basta o conceito do nosso ônibus. Esta é provavelmente a segunda ideia brilhante, uma descoberta brilhante que fiz para mim mesma no processo de escrever um emulador.
Vamos imaginar que pegamos nossa memória, que era de 64 kilobytes, e a dividimos em dois intervalos de 32 kilobytes. Na faixa inferior haverá um determinado dispositivo, que é um arranjo de lâmpadas, como na foto com esta placa.
Digamos que, ao gravar nesta faixa júnior de 32 kilobytes, a luz acenderá ou apagará. Se escrevermos aí o valor 1, acende-se a luz, se for 0 - apaga-se. Ao mesmo tempo, podemos ler o valor e entender o estado do sistema, entender qual imagem é exibida nesta tela.
Novamente, na faixa de endereço alta colocamos a memória comum na qual o programa está localizado, porque precisamos de um endereço na faixa alta durante o procedimento de reinicialização.
Esta é realmente uma ideia super genial. Para interagir com os periféricos, nenhum comando adicional é necessário, etc. Apenas escrevemos na boa e velha memória, como antes. Mas, ao mesmo tempo, a memória já pode ser dispositivos adicionais.
Agora estamos totalmente preparados para dar uma olhada na arquitetura NES. Temos uma CPU e seu barramento, como de costume. Existem dois kilobytes adicionais de memória. Existe um APU - um dispositivo de saída de som. Infelizmente, agora não vamos considerar isso, mas está tudo super legal aí também. E há um cartucho. Ele é colocado na faixa alta e fornece dados do programa. Ele também fornece esses gráficos, agora vamos considerar. A última coisa no barramento da CPU é uma PPU, uma unidade de processamento de imagem, como uma placa de proto-vídeo. Se você queria aprender a trabalhar com placas de vídeo, agora vamos até aprender como implementar uma.
O PPU também possui seu próprio barramento, no qual as tabelas de nomes, paletas e dados gráficos são deslocados. Mas os dados gráficos vêm do cartucho. E então há a memória do objeto. Esta é a arquitetura.
Vamos ver o que é um cartucho. Essa é uma ideia muito mais legal do que o CD, se você considerar que é do passado.
Por que ela é legal? À esquerda podemos ver o cartucho da região americana, o famoso jogo Zelda, se alguém ainda não jogou - jogue, super. E se desmontarmos este cartucho, encontraremos microcircuitos nele. Não há disco laser, etc. Normalmente esses chips contêm apenas alguns dados. Além disso, o cartucho corta diretamente em nosso sistema de computador, no barramento da CPU e PPU. Ele permite que você faça coisas incríveis e aprimore a experiência do usuário.
Há um mapeador a bordo do cartucho, ele se preenche com a tradução dos endereços. Digamos que temos um grande jogo. Mas o NES tem apenas 32 kilobytes de memória que pode endereçar para o programa. Um jogo, digamos, tem 128 kilobytes. O mapeador pode, instantaneamente, durante a execução do programa, substituir um determinado intervalo de memória por dados completamente novos. Podemos dizer no programa: carregue-nos no nível 2, e a memória será substituída diretamente, quase instantaneamente.
Além disso, há coisas engraçadas. Por exemplo, o mapeador pode fornecer chips que expandem a trilha sonora, adicionam novos, etc. Se você jogou Castlevania, ouça como soa o Castlevania da região japonesa. Há um som adicional, parece completamente diferente. Nesse caso, tudo é executado no mesmo hardware. Ou seja, essa ideia é mais parecida com a de quando você comprou uma placa de vídeo, conectou-a a um computador e tem funcionalidades adicionais. É o mesmo aqui. Isso é ótimo. Mas estamos presos aos CDs.
Vamos para a parte final - vamos ver como funciona esse dispositivo de saída de imagem. Porque se você quer fazer um emulador, o programa mínimo é fazer um processador e essa coisa para ver como ficam as fotos e os videogames.
Vamos começar com a entidade de nível superior - a própria imagem. Tem dois planos. Há um primeiro plano onde entidades mais dinâmicas são colocadas e um segundo plano onde entidades mais estáticas, como uma cena, são colocadas.
Você pode ver a divisão aqui. À esquerda está o mesmo famoso jogo Castlevania, então toda a nossa jornada para PPU acontecerá com Simon Belmont. Junto com ele, vamos considerar como tudo funciona.
Há um fundo, colunas, etc. Vemos que eles são desenhados no fundo, mas ao mesmo tempo todos os personagens - o próprio Simon (esquerda, marrom) e fantasmas - já estão desenhados no primeiro plano. Ou seja, o primeiro plano existe para entidades mais dinâmicas e o segundo plano existe para as mais estáticas.
Uma imagem em uma exibição de bitmap consiste em pixels. Pixels são apenas pontos coloridos. No mínimo, precisamos de cores. O NES possui uma paleta de sistema. É composto por 64 cores, que infelizmente são todas as cores que o NES pode reproduzir. Mas não podemos tirar nenhuma cor da paleta. Para paletas personalizadas, existe um intervalo específico na memória, que, por sua vez, também é dividido em dois subfaixas.
Há uma variedade de plano de fundo e primeiro plano. Cada faixa é dividida em quatro paletas de quatro cores. Por exemplo, fundo, paleta zero consiste em branco, azul, vermelho. E a quarta cor em cada paleta sempre se refere a uma cor transparente, o que nos permite fazer um pixel transparente.
Esta faixa com paletas não está mais localizada no barramento da CPU, mas no barramento PPU. Vamos ver como podemos escrever dados lá, porque não temos acesso ao barramento PPU através do barramento da CPU.
Aqui, voltamos novamente à ideia de E / S mapeada em memória. Existem endereços 0x2006 e 0x2007, são endereços hexadecimais, mas são apenas números. E nós escrevemos assim. Como temos um endereço de 16 bits, escrevemos o endereço no registrador de endereço ox2006 em duas abordagens de oito bits e, então, podemos escrever nossos dados por meio do endereço 0x2007. Que coisa engraçada. Ou seja, na verdade, precisamos realizar três operações para, pelo menos, escrever algo na paleta.
Excelente. Temos uma paleta, mas precisamos de estruturas. As cores são sempre boas, mas os bitmaps têm uma certa estrutura.
Para gráficos, existem duas tabelas de quatro kilobytes cada, contendo blocos. E toda essa memória é uma espécie de atlas. Anteriormente, quando todos usavam uma imagem raster, faziam um grande atlas, a partir do qual selecionavam as imagens necessárias através da imagem de fundo por coordenadas. Aqui está a mesma ideia.
Cada mesa tem 256 peças. Novamente, numerologia engraçada: exatamente 256 permite que você especifique um byte, 256 valores diferentes. Ou seja, em um byte podemos especificar qualquer bloco de que precisamos. Acontece duas tabelas. Uma mesa para fundos, outra para primeiro plano.
Vamos ver como esses blocos são armazenados. É uma coisa engraçada aqui também. Vamos lembrar que temos quatro cores em nossa paleta. Numerologia novamente: um byte tem oito bits e um bloco tem oito por oito. Acontece que com um byte podemos representar uma faixa de um ladrilho, onde cada bit será responsável por alguma cor. E com oito bytes, podemos representar um bloco de oito por oito completo.
Mas há um problema aqui. Como dissemos, um bit é responsável pela cor, mas pode representar apenas dois valores. As telhas são armazenadas em dois planos. Existe um plano do bit mais significativo e menos significativo. Para obter a cor final, combinamos dados de ambos os planos.
Você pode considerar - aqui, por exemplo, a letra "I", a parte inferior, há o número "3", que resulta assim: pegamos o plano dos bits menos significativos e mais significativos e obtemos o número binário 11, que será igual ao decimal 3. Uma estrutura de dados engraçada.
fundo
Agora podemos finalmente renderizar o fundo!
Existe uma tabela de nomes para isso. Temos dois deles, cada um com 960 bytes, cada byte nos remete a um bloco específico. Ou seja, o identificador do ladrilho é indicado na tabela anterior. Se representarmos esses 960 bytes como uma matriz, teremos uma tela de 32 por 30 blocos. A resolução NES será de 256 pixels por 240 pixels.
Excelente. Podemos escrever ladrilhos lá. Mas, como você deve ter notado, os ladrilhos não indicam a paleta com a qual devem ser exibidos. Podemos exibir blocos diferentes com paletas diferentes e também precisamos armazenar essas informações em algum lugar. Infelizmente, temos apenas 64 bytes por tabela de nomes para armazenar informações da paleta.
E é aí que surge o problema. Se dividirmos a tabela ainda mais de forma que haja apenas 64 valores, obteremos quadrados de quatro por quatro peças que se parecem com esse quadrado vermelho. Esta é apenas uma grande parte da tela. Ela seria subordinada a uma paleta, se não por uma, mas.
Como lembramos, existem quatro paletas na subpaleta e precisamos apenas de dois bits para indicar o que precisamos. Cada um desses 64 bytes copia as informações da paleta para uma grade quatro por quatro. Mas esta grade ainda está dividida em tais subgrades duas a duas. Claro, há uma limitação aqui: uma grade de dois por dois está ligada a uma paleta. Essas são as limitações no mundo da exibição de planos de fundo na Nintendo. Curiosidade, mas em geral não interfere muito nos jogos.
Também há rolagem. Se nos lembrarmos, por exemplo, de "Mario" ou Castlevania, então sabemos: se nesses jogos o herói se move para a direita, o mundo parece se desenrolar ao longo da tela. Isso é feito rolando.
Lembre-se de que temos duas tabelas de nomes que já codificam duas telas. E quando nosso herói se move, nós meio que adicionamos dados à tabela de nomes a seguir. Na hora, quando nosso herói se move, preenchemos a tabela de nomes. Acontece que podemos indicar a partir de qual bloco na tabela de nomes precisamos começar a exibir os dados e vamos expandi-los em faixas. Todo o truque da rolagem está na leitura das duas tabelas de nomes.
Ou seja, se formos além de uma tabela de nomes horizontalmente, começamos a ler automaticamente de outra, etc. E não se esqueça, novamente, de preencher os dados.
A propósito, a rolagem era algo muito importante na época. As primeiras realizações de John Carmack foram no campo da rolagem. Confira esta história, é muito engraçada.
Primeiro plano
E o primeiro plano. Em primeiro plano, como dissemos, existem entidades dinâmicas, e elas estão armazenadas na memória de objetos e atributos.
Existem 256 bytes lá nos quais podemos gravar 64 objetos, quatro bytes por objeto. Cada objeto codifica X e Y, que é o deslocamento de pixel na tela. Além do endereço e dos atributos do bloco. Podemos priorizar o background, vê a imagem abaixo? Podemos especificar a paleta. Prioridade sobre o fundo diz ao PPU que o fundo deve ser desenhado na parte superior do sprite. Isso nos permite colocar Simon atrás da estátua.
Também podemos fazer a orientação, girá-la além de qualquer eixo, por exemplo, horizontal, vertical, como a letra “I” da imagem. Escrevemos aproximadamente da mesma maneira que a paleta: através dos endereços 0x2003, 0x2004.
Finalmente, a final. Como renderizamos objetos em primeiro plano?
A imagem se desdobra em linhas chamadas linhas de varredura, este é um termo da televisão. Antes de cada linha de varredura, simplesmente pegamos oito sprites do objeto e atribuímos a memória. Não mais que oito, apenas oito são suportados. Também existe essa limitação. Apenas os exibimos linha por linha, como aqui, por exemplo. Na linha de varredura atual, em amarelo, exibimos uma nuvem, um sol e um coração em uma faixa. E o smiley não é exibido. Mas ele ainda está feliz.
Confira o super canal do One Lone Coder . Há o próprio processo de programação, em particular - a programação do emulador NES. E o Nesdev contém todas as informações sobre emulação - em que consiste, etc. O link final é o código do meu emulador . Dê uma olhada se estiver interessado. Escrito em TypeScript.
Obrigado. Espero que você tenha aproveitado.