O que aprendi sobre a máquina de fliperama Bomb Jack no processo de criação de seu emulador
Recentemente, escrevi um pequeno emulador para uma máquina Bomb Jack, principalmente para descobrir como essas primeiras máquinas arcade de 8 bits diferiam no design dos computadores domésticos de 8 bits.
Como aprendi muito mais tarde, uma reunião em uma feira de verão em minha cidade natal com máquinas de fliperama como Bomb Jack foi um daqueles momentos que mudaram meu destino. Em um dia normal de verão, depois de gastar todo o meu suprimento de moedas em máquinas de fliperama, voltei para casa e minha cabeça estava cheia de flores e efeitos sonoros. Eu tentei entender como esses jogos funcionavam. E até o final do ano, passei todo o meu tempo depois da escola criando cópias bastante desbotadas desses jogos de arcade no meu computador doméstico. Eu era como um fã de cultos de carga das ilhas do Oceano Pacífico, que queria criar uma estação de rádio militar americana a partir de gravetos.
No começo, pensei na idéia de criar um emulador do
Pengo , porque meu cérebro adolescente ficou muito mais impressionado com este jogo do que Bomb Jack (a propósito, aqui está minha
versão do Pengo para cultos de carga ). Mas o equipamento de fliperama Pengo exigiria a criação de novos emuladores de chip para áudio e vídeo, e para o Bomb Jack já havia partes suficientes (Z80 como CPU e AY-3-8910 para som), então fui o primeiro a enfrentar o Bomb Jack.
Além disso, o Bomb Jack foi uma ótima oportunidade para finalmente adicionar suporte a NMI (interrupção não mascarável) ao meu emulador Z80. Nenhuma das máquinas baseadas em Z80 que eu emulei usava NMI e, portanto, não havia muito sentido em recriar essa função - eu ainda não conseguia verificar seu funcionamento.
Se você não sabe o que é o Bomb Jack, esse jogo fica assim (não tenho certeza se eu escolhi a proporção correta):
A versão do emulador no WebAssembly pode ser encontrada aqui:
https://floooh.imtqy.com/tiny8bit/bombjack.htmlApós a conclusão do procedimento de carregamento e a tabela de pontuação mais alta, pressione
1 para soltar uma moeda e, em seguida, pressione
Enter (ou qualquer outra tecla, exceto as setas e a barra de espaço) para iniciar o jogo.
Dentro do jogo, use as
setas do teclado para mudar de direção e a
barra de
espaço para pular. Enquanto estiver no ar, pressione a
barra de
espaço para diminuir a queda.
O código fonte está aqui:
https://github.com/floooh/chips-test/blob/master/examples/sokol/bombjack.cEle usa
cabeçalhos de chip para fornecer emulação do Z80 e AY-3-8910, além de
cabeçalhos sokol como um invólucro de plataforma cruzada (para inserir o aplicativo, renderização, entrada e som).
Etapa 1: Pesquisa
"Pesquisa" é uma palavra muito grande: acabei de acessar as "especificações de hardware de arcade Bombjack do Google".
Comparado com os populares computadores domésticos dos anos 80 (ou mesmo os misteriosos computadores da Europa Oriental, que geralmente ainda têm comunidades ativas), há muito pouca informação sobre o Bomb Jack na Internet.
Encontrei duas informações muito importantes: o
diagrama de circuito da máquina e, claro, o
código fonte do emulador MAME .
Há também um projeto que implementa o
Bomb Jack no FGPA , a partir das fontes VHDL das quais consegui descobrir detalhes que não estão no diagrama de circuito.
Compreender o código fonte do MAME seria complicado, porque os emuladores de máquinas arcade geralmente são apenas um monte de macros que descrevem como várias partes do equipamento interagem, mas não
há muito
código fonte .
No entanto, as descrições macro do equipamento, e especialmente os comentários, acabaram sendo muito úteis para entender a operação do hardware e onde se tornaram muito enigmáticas (por exemplo, a
parte sobre decodificação de vídeo ), tentativa e erro foram suficientes, estudo detalhado do conceito.
Visão geral do hardware
O mais interessante sobre o hardware Bomb Jack é que na verdade são
dois computadores conectados entre si por uma fita isolante: existe uma
placa principal com CPU Z80 e equipamento de decodificação de vídeo e uma
placa de som separada com sua própria CPU Z80 e três (sim, três!) chips de som AY-3-8910.
O equipamento de decodificação de vídeo não é implementado como um circuito integrado - são apenas muitos chips pequenos de uso geral (o circuito deles ocupa 6 em 10 páginas do diagrama de circuitos do dispositivo). Ao criar um emulador, decidi percorrer um caminho curto: em vez de emular certas partes do equipamento de decodificação de vídeo, emulei apenas seu comportamento, criando a saída correspondente a partir dos dados de entrada e não me preocupando com o funcionamento do próprio equipamento no meio.
Essa solução simplificada é bastante adequada para uma máquina de arcade separada, projetada para executar apenas um programa. Se o jogo iniciar e funcionar corretamente, a emulação poderá ser considerada "boa o suficiente".
Além disso, essa abordagem simplificada é uma diferença importante da emulação da maioria dos computadores domésticos: alguns jogos exigem emulação mais precisa do que outros, por exemplo, máquinas como C64 ou Amstrad CPC precisam de emulação muito precisa até ciclos de clock, para que os sistemas de vídeo de alguns jogos e gráficos demos funcionou corretamente.
Isso também significa que meus emuladores de CPU e chip de som prontos são realmente um trabalho supérfluo para o Bomb Jack, por exemplo, trabalhar com CPUs Z80 com a implementação da fracionalidade do ciclo da máquina é um exagero, uma fragmentação mais simples e rápida no nível da instrução seria suficiente.
Placa principal
Normalmente, a primeira coisa que tento descobrir ao escrever um novo emulador é o esquema de alocação de memória (onde estão as áreas de ROM e RAM, memória de vídeo e endereços especiais ou portas de entrada / saída).
Existe apenas um chip "interessante" na placa principal do Bomb Jack - o CPU Z80 operando a 4 MHz. Todo o espaço restante na placa principal é ocupado por equipamentos de decodificação de vídeo (com exceção de um par de chips de RAM e ROM).
O espaço de endereço de 16 bits é o seguinte:
- 0000..7FFF : ROM de 32 KB
- 8000..8FFF : 4 KB de RAM de uso geral
- 9000..93FF : 1 KB de memória de vídeo
- 9400..97FF : 1 KB de RAM colorida
- 9820..987F : 96 bytes de RAM do sprite
- 9C00..9CFF : 256 bytes de paleta de cores RAM
- 9E00, B000..B005, B800 : portas de entrada e saída
- C000..DFFF : 8KB ROM
A área da porta de E / S é a seguinte. Algumas portas são somente para gravação, outras são somente leitura e algumas têm funções diferentes ao ler e gravar nelas:
- 9E00 : write: número da imagem de fundo atual, leia: -
- B000 : leitura: estado do joystick do jogador 1, gravação: ativar / desativar a máscara NMI
- B001 : leia: estado do joystick do jogador 2, escreva: -
- B002 : leia: moedas e botões Iniciar, escreva: -
- B003 : leia: watchdog da CPU, escreva: ???
- B004 : leitura: interruptores dip 1, gravação: comutador de tela
- B005 : leia: dip switches 2, escreva: -
- B800 : write: command placa de som, leia: -
Vale a pena mencionar aqui:
- O dispositivo possui MUITA ROM (40 Kbytes) e muito pouca RAM (cerca de 7 Kbytes, e apenas 4 Kbytes deles são "RAM de uso geral")
- Apenas 2 Kbytes são alocados para a RAM da tela, divididos em dois fragmentos de 1 Kbyte, o que parece muito pequeno para uma tela colorida de 256x256, na qual, ao que parece, as cores são definidas pixel por pixel
- Este é um sistema de E / S em um esquema de alocação de memória!
A E / S no esquema de alocação de memória é um pouco incomum para uma máquina Z80, porque uma das características do Z80 é o espaço de endereço de 16 bits separado para E / S. Isso é feito para economizar espaço valioso no endereço de memória. A E / S em um esquema de alocação de memória é normalmente encontrada em computadores com um processador 6502.
Uma olhada no diagrama de circuitos confirma isso: o pino IORQ não é detectado na CPU da placa principal, apenas o pino MREQ está conectado (que é usado para inicializar a leitura ou gravação na memória):
Isso significa que não precisamos nos preocupar com solicitações de E / S para a função de timer da CPU da placa principal no emulador, mas apenas com solicitações de memória.
Tendo estudado o diagrama de circuitos, encontrei outro detalhe interessante sobre a CPU da placa principal:
Somente o pino NMI está conectado, enquanto o pino INT sempre mantém um alto nível de sinal de relógio / permanece inativo (isso significa que as interrupções mascaradas "usuais" não são executadas e ocorrem apenas interrupções não mascaradas):
Isso também é bastante incomum para um carro com o Z80. Em todos os computadores domésticos baseados em Z80 com os quais eu costumava lidar, o oposto era verdadeiro - eles usavam apenas interrupções mascaráveis e nunca usavam não-mascaráveis. A interrupção mascarada Z80 é uma melhoria muito flexível e séria em comparação com o sistema de interrupção primitivo de seu "pai ilegítimo" - Intel 8080, ou seu concorrente - MOS 6502. Mas esse aumento da flexibilidade também é mais difícil de implementar em equipamentos (a menos que seja uma fonte de interrupções) outros chips da família Z80 são usados, nos quais já existe um protocolo de interrupção complexo incorporado quando conectado por barramento).
Bem, detalhes suficientes sobre o equipamento, vamos para o emulador!
Procedimento de inicialização
O próximo passo após determinar a configuração da memória é conectar a CPU emulada ao esquema de alocação de memória emulada, registrar algum tipo de visualização do conteúdo da memória de vídeo e iniciar os ciclos da CPU.
Surpreendentemente, uma abordagem tão áspera é muitas vezes suficiente para passar pelo procedimento de carregamento e exibir
algo na tela. Ao projetar o emulador de Bomb Jack, peguei o conteúdo da memória de vídeo de 1 KB no intervalo de 0x9000 a 0x93FF como uma matriz de 32x32 bytes. Quando o byte era 0, renderizei um bloco de pixels pretos 8x8 e, caso contrário, um bloco de pixels brancos.
Então eu apenas executei a CPU emulada e esperava o melhor. Eis! Algum tipo de imagem legível apareceu:
A imagem superior se parece com uma tela de teste de hardware na inicialização e a parte inferior se parece com uma tela de registro de pontuação que aparece após a conclusão do procedimento de inicialização:
... mas girou 90 graus (o que é lógico, porque a tela das máquinas de fliperama costumava estar na orientação "retrato" vertical).
Ótimo, o começo é promissor!
O próximo passo é descobrir como transformar esses blocos brancos em pixels coloridos ... (e este é um passo enorme, os detalhes são descritos abaixo na seção sobre decodificação de vídeo).
No início, tudo foi muito rápido, na tela de teste, pixels e cores foram exibidos durante o carregamento (mais tarde, notei que a decodificação de cores estava completamente errada e, no entanto ...):
Mas quando a tela de gravação deveria aparecer, eu tinha uma tela preta. Cortando a cor do plano de fundo para que “não seja preto”, descobri que os pixels são renderizados, mas toda a paleta de cores é preta. Hmm ...
Depois de olhar para essa tela por alguns minutos, lembrei-me de que algumas cores na tela de recordes são animadas e, quando há animação, deve haver algum tipo de temporizador. A fonte lógica de tempo nesta configuração de equipamento será o sinal VSYNC do monitor e o VSYNC conectado ao pino NMI da CPU (ou melhor, não o VSYNC, mas o VBLANK, que é o breve momento entre o sinal VSYNC e o feixe de raios catódicos que se move para o canto superior esquerdo).
E ainda não implementei tudo isso ...
Na noite seguinte, quando adicionei a primeira versão do processamento NMI à emulação Z80 e a conectei ao primeiro contador vsync / vblank na função de timer da CPU da placa principal, muitas coisas de repente começaram a acontecer!
Em primeiro lugar, as cores apareceram na tela de registros e algumas foram animadas:
Depois de alguns segundos, algo ainda mais emocionante começou! O recorde desapareceu e uma visualização estranha do primeiro mapa foi exibida. Ficou claro que este é um modo de demonstração de uma máquina de fliperama para atrair atenção - vi várias bombas com animações coloridas que desapareceram quando um imaginário Bomb Jack pulou em um mapa coletando essas bombas:
As cores ainda estavam completamente erradas e, no entanto, é PROGRESS!
É o momento certo para fazer o resto da decodificação de vídeo:
Ferro de vídeo
À primeira vista, o equipamento de processamento de vídeo em Bomb Jack parecia muito poderoso para uma máquina de 8 bits desde 1984: apesar da resolução de apenas 256x256 pixels, podia exibir simultaneamente 128 (de 4096) cores e renderizar até 24 sprites de hardware (tamanho 16x16) ou 32 x 32) com pixel por pixel.
Os computadores domésticos de 8 bits da época tinham aproximadamente a mesma resolução de tela, mas tinham muitas restrições de cores. Essas restrições são claramente visíveis ao comparar as versões Bomb Jack para o ZX Spectrum e Amstrad CPC com a versão para a máquina de arcade:
A
versão para o ZX Spectrum tinha uma resolução de pixel bastante boa (256x192), mas muito poucas cores, e sofria com o efeito típico de "conflito de cores" do Spectrum (embora os desenvolvedores tenham se esforçado para não ser perceptível):
A versão do Amstrad CPC é mais colorida, mas, para obter mais cores, os desenvolvedores tiveram que mudar para o modo de exibição de baixa resolução (160x200). Como resultado disso, Jack e os monstros se transformaram em um monte ilegível de pixels:
Compare isso com a versão da máquina de fliperama, que tinha a mesma resolução de pixel que o ZX Spectrum, mas com muito mais cores
e maior resolução de cores pixel por pixel:
O interessante aqui é que a versão arcade possui gráficos melhores, não porque funciona em hardware mais poderoso (possui mais ROMs para armazenar mais dados gráficos, mas o "poder de computação" é o mesmo), mas porque os desenvolvedores do dispositivo podem se concentrar na fabricação de uma máquina especializada para um tipo específico de jogo e eles não precisavam criar um computador doméstico universal para uso geral.
Veja como o hardware da tela funciona (pelo menos na minha interpretação de alto nível):
Três camadas de exibição
O sinal de vídeo final do Bomb Jack é combinado de três camadas: uma camada de fundo, uma camada frontal e uma camada de sprite.
Esse sistema de camadas tem duas vantagens principais:
- Ele implementa uma compactação de imagem de hardware bastante complicada para gerar uma imagem colorida de "alta resolução" a partir de uma quantidade muito pequena de dados.
- Reduz significativamente a quantidade de trabalho da CPU necessária para atualizar os elementos dinâmicos da tela (mesmo a uma frequência de 4 MHz, uma CPU de 8 bits não tem energia suficiente para mover um número tão grande de objetos em uma tela de 256x256 com uma frequência de 60 Hz)
O ferro de vídeo é bem diferente do que vi nos computadores domésticos de 8 bits, mas o MAME implementa classes auxiliares generalizadas para esse tipo de equipamento, por isso posso assumir que é bastante comum em máquinas de fliperama.
Camada de fundo
A camada de fundo pode renderizar 1 entre 5 imagens de fundo incorporadas na ROM. A imagem de fundo é selecionada escrevendo um valor de 1 a 5 no endereço 0x9E00 (parece que o valor 0 é especial e renderiza um fundo completamente preto).
De fato, parece que o equipamento é capaz de renderizar 7 imagens diferentes, mas apenas 5. são usadas no jogo. Secretamente, eu esperava encontrar dados de imagem anteriormente não detectados na ROM. Mas, infelizmente, eles não estão lá (sim, provavelmente não sou o primeiro a procurá-los lá).
Aqui está a aparência da camada de segundo plano do primeiro mapa sem as outras duas camadas:
A camada de fundo é montada a partir de blocos de
16x16 pixels.
A vantagem de criar imagens de plano de fundo a partir de blocos é que os mesmos blocos podem ser usados várias vezes, para que menos dados possam ser armazenados na ROM. Observe que o céu azul, partes da pirâmide e areia sob a pirâmide usam os mesmos azulejos:
Para economizar memória, o equipamento da camada de segundo plano implementa outro truque - os ladrilhos podem ser girados horizontalmente. Quase perdi isso na minha implementação porque presumi que o software não usasse essa função de hardware, mas notei um pequeno bug no fundo da terceira placa:
Usei o mesmo truque no quinto mapa, mas aqui é um pouco mais difícil de perceber se você não sabe o que procurar:
Camada frontal:
Acima da camada de fundo está a “camada frontal”, que renderiza todas as partes fixas da tela, que, no entanto, devem ser atualizadas pela CPU (principalmente texto, plataformas e bombas). O layout é lido da RAM (de fragmentos de 1 KB de RAM e 1 KB de cor).
Aqui está a aparência da camada frontal isolada do primeiro mapa:
A camada frontal também consiste em blocos (assim como no plano de fundo), mas usa blocos menores de 8x8:
A principal vantagem de dividir o fundo e a frente em camadas separadas é que a CPU não precisa se preocupar em armazenar e restaurar pixels de fundo ao criar ou excluir elementos da frente.
Camada de Sprite
Finalmente, sprites de hardware são renderizados sobre a camada frontal. Tudo o que se move pela tela é implementado em sprites. O equipamento Bomb Jack pode renderizar até 24 sprites, e cada sprite pode ter um tamanho de 16x16 ou 32x32 pixels. Nesse caso, os sprites podem ser posicionados com precisão pixel por pixel:
Decodificador 8x8 lado a lado
No coração do equipamento de decodificação de vídeo está uma paleta de cores com 128 elementos e um decodificador de blocos de 8x8 pixels. A tarefa do decodificador de bloco é gerar um índice de paleta de cores de 7 bits para cada um dos 64 pixels do bloco.
Esses blocos 8x8 são os blocos de construção de tudo na tela - blocos 16x16 em segundo plano, blocos 8x8 na camada frontal e sprites de hardware 16x16 ou 32x32.Aqui está um diagrama de blocos deste decodificador de blocos 8x8 para renderizar a camada frontal (como eu a entendi):Explicação do diagrama de blocos de cima para baixo:- O processo de decodificação começa na parte superior, lendo o byte do “código de bloco” da memória de vídeo (organizado como uma matriz de códigos de bloco 32x32) e um byte separado da RAM colorida (também uma matriz 32x32). A obtenção dos códigos de ladrilhos e cores da memória de vídeo ocorre apenas na camada frontal, mas eu a adicionei para tornar a imagem como um todo mais compreensível. O decodificador de bloco 8x8 requer apenas um código de bloco e cor na entrada.
- . ( ). , , ( ).
- 8 , 8 ( ). , , 8x8 24 (3 ).
- 64 7- . 3 , 4 — . , , 16 «», 8 . 8 .
- 7- , , 12- RGB- (4 ). ( , , ; , ).
Este é um esquema geral de decodificação de blocos que é usado por cada uma das três camadas de exibição, mas a decodificação de cada camada é um pouco diferente:- A camada frontal pode render 512 blocos diferentes de 8x8. Isso requer códigos de bloco de 9 bits, mas a memória de vídeo fornece apenas 8 bits por bloco. O nono bit é "emprestado" do quinto bit do valor da cor (como apenas 4 bits do valor da cor são usados para construir o índice da paleta de cores, restam mais 4 bits para outros fins). Se todos os 3 bits das camadas de 8x8 lado a lado forem iguais a zero, o pixel frontal será considerado transparente e o pixel de segundo plano "brilhará" nele.
- 16x16, 16x16=256 256 (512 ). , 16x16 8x8, . , ; «» : 7 , .
- 16x16 32x32 , 4 16 8x8 . , 16x16 96 , 32x32 — 384 . , 3 , .
Para entender melhor a aparência das camadas de bloco, escrevi um pequeno programa C que converte blocos de ROM em arquivos PNG (3 bits por pixel convertido em 8 níveis de escala de cinza).A seguir, são mostrados os blocos de ROM da camada frontal. Vemos números e dados de fontes de texto, blocos de plataforma, bombas (divididas ao meio), partes do logotipo do protetor de tela Bomb Jack e o número de multiplicadores de pontuação que aparecem na parte superior da tela (a propósito, tudo gira 90 graus, porque a tela inteira também é girada ):Em seguida, considere os blocos de ROM do plano de fundo. Não parece muito claro, porque o que observamos é na verdade decodificando blocos 16x16 para blocos 8x8. Cada bloco 16x16 é criado a partir de quatro blocos 8x8 adjacentes. Mas você pode reconhecer partes do templo grego no mapa 2, o castelo no mapa 3 e os arranha-céus no mapa 4.E, finalmente, ROM sprite tiles. Sprites de 16x16 ocupam a metade superior e sprites de 32x32 ocupam a metade inferior.Um truque interessante do protetor de tela de Bomb Jack é que o logotipo é montado a partir dos mosaicos e sprites da frente. Acho que os desenvolvedores estavam ficando sem ROMs do bloco frontal, mas havia pouco espaço na ROM do sprite:Equipamento de Sprite
O equipamento de sprite Bomb Jack é muito poderoso comparado ao usado em computadores domésticos da época:- Pode renderizar até 24 sprites de hardware. Parece que não havia restrições no número de sprites por linha de varredura.
- Os sprites podem ter um tamanho de 16x16 pixels ou 32x32 pixels
- Cada sprite pode escolher um dos 16 slots de 8 cores em uma paleta de cores comum
- Sprites tinham resolução de cor pixel por pixel.
- Cada sprite pode ser invertido verticalmente ou horizontalmente
- Cada sprite pode escolher uma das 128 imagens de sprite exibidas na ROM.
Ao decodificar pixels e sprites de um sistema de sprites, o mesmo bloco base 8x8 é usado como nas camadas frontal e de fundo.Os atributos do Sprite estão localizados no intervalo de endereços de 0x9820 a 0x987F - 96 bytes, 4 bytes por sprite. Até onde eu vi, essa área é apenas para gravação; pelo menos a CPU não realiza acesso de leitura a esse intervalo de memória.Cada sprite é descrito por 4 bytes:- Byte 0 :
- Bit 7 : se definido, este é um sprite de 32x32, caso contrário, 16x16
- Bits 6..0 : 7 bits para definir o código do bloco de sprites usado para procurar camadas de bits da imagem do sprite nos blocos de ROM.
- Byte 1 :
- Bit 7 : se definido, o sprite é invertido horizontalmente
- Bit 6 : se definido, o sprite é invertido verticalmente
- Bits 3..0 : 4 bits para definir o valor da cor para o decodificador de blocos
- Byte 2 : posição do sprite do eixo X na tela
- Byte 3 : posição do sprite na tela ao longo do eixo Y
Não está claro o que os bits 4 e 5 do byte 1 fazem, o comentário em MAME diz o seguinte:
e ? (, )
f ? (, (B)?)
Portas de E / S de memória
Algumas notas nas portas de entrada / saída da placa principal. Como afirmado acima, as portas de E / S são assim:
- 9E00 : write: número da imagem de fundo atual, leia: -
- B000 : leitura: estado do joystick do jogador 1, gravação: ativar / desativar a máscara NMI
- B001 : leia: estado do joystick do jogador 2, escreva: -
- B002 : leia: moedas e botões Iniciar, escreva: -
- B003 : leia: watchdog da CPU, escreva: ???
- B004 : leitura: interruptores dip 1, gravação: comutador de tela
- B005 : leia: dip switches 2, escreva: -
- B800 : write: command placa de som, leia: -
O endereço 0x9E00 (seleção da imagem de plano de fundo) que já consideramos acima e o endereço 0xB800 (placa de som de comando) que consideraremos na próxima seção. Permanece os endereços de 0xB000 a 0xB005:
A leitura dos endereços 0xB000 e 0xB001 retorna o estado atual dos dois joysticks. Os bytes definidos indicam os interruptores fechados do joystick:
- bit 0 : direção certa
- bit 1 : direção esquerda
- bit 2 : direção ascendente
- bit 3 : direção descendente
- bit 4 : botão de salto pressionado
Os 3 bits restantes são ignorados.
A leitura de 0xB002 retorna o status do aceitador de moedas e os botões Iniciar:
- bit 0 : a moeda do jogador 1 é lançada
- bit 1 : a moeda do jogador 2 é lançada
- bit 2 : botão iniciar do jogador 1
- bit 3 : botão iniciar do jogador 2
A leitura dos endereços 0xB004 e 0xB005 retorna o estado dos comutadores DIP usados para configurar o comportamento da máquina arcade:
- B004 :
- bits 0,1 : quantos "jogos" são dados por uma moeda (1, 2, 3 ou 5)
- bits 2,3 : o mesmo para o jogador 2
- bits 4,5 : quantas vidas por jogo (3, 4, 5 ou 2)
- bit 6 : a localização da máquina de arcade: “mesa de coquetel” ou “vertical”.
- bit 7 : se é para reproduzir som no modo de espera
- B005 :
- bits 3.4 : dificuldade 1 (velocidade das aves)
- bits 5,6 : dificuldade 2 (número e velocidade dos inimigos)
- bit 7 : frequência de ocorrência de uma moeda específica
Finalmente, a leitura do endereço
B003 implementa um watchdog de software. A CPU deve frequentemente ler este endereço, caso contrário, a máquina arcade executará uma redefinição de hardware. Se, por algum motivo, o jogo travar, o equipamento será reiniciado automaticamente.
Você pode gravar em alguns endereços de porta de E / S:
- B000 : se deve gerar NMI durante o vblank; parece estar desativado apenas durante o procedimento de inicialização
- B004 : vira a tela inteira; Nunca conheci o uso dessa função, mas tenho uma teoria sobre isso (veja abaixo)
A funcionalidade de inversão de tela é um pouco confusa, porque ao jogar um jogo, nunca vi seu uso. No entanto, tenho um palpite sobre o que ele está fazendo, mas para confirmar, você precisa escrever um código. Quando a máquina de fliperama está na configuração de "mesa de coquetel", dois jogadores se sentam um em frente ao outro. Portanto, sugeri que, quando um jogo muda do jogador 1 para o jogador 2, essa função vira a tela. No entanto, ainda não implementei o modo para dois jogadores no emulador.
Placa de som
A placa de som em si é um computador completo com uma CPU Z80 (operando na frequência de 3 MHz), três chips de som (AY-38910 operando na frequência de 1,5 MHz), além de RAM e ROM. O esquema de alocação de memória da placa de som parece bastante simples:
- 0000..2000 : 8 Kbytes de ROM
- 4000..4400 : 1 KB de RAM
- 6000 : comando de som da placa principal
Como não há nada interessante no esquema de alocação de memória acima do endereço 0x8000, o contato de endereço mais alto da CPU nem está conectado:
O endereço especial 0x6000 é a porta de E / S (trava de 8 bits) localizada na memória, que não corresponde à RAM real. Essa é a mesma porta que está localizada na placa principal em 0xB800. É um canal de comunicação entre as placas principal e de som.
Os três chips de som são controlados por estas instruções de saída do Z80, não pelas portas de memória. O AY-3-8910 possui apenas duas portas de E / S abertas, a primeira é usada para armazenar o número do registro e a segunda é usada para gravar ou ler o conteúdo do registro especificado pela primeira porta.
O circuito de E / S é o seguinte:
- 0x00 : primeiro chip de som: seleção de registro
- 0x01 : primeiro chip de som: acesso ao registro selecionado
- 0x10 : segundo chip de som: seleção de registro
- 0x11 : segundo chip de som: acesso ao registro selecionado
- 0x80 : terceiro chip de som: seleção de registro
- 0x81 : terceiro chip de som: acesso ao registro selecionado
Algumas palavras sobre o chip de som AY-3-8910:
Este é um dispositivo bastante padrão, muito popular nos computadores domésticos da época (por exemplo, no Amstrad CPC, no ZX Spectrum 128, nos computadores MSX e em muitos outros). O AY-3-8910 gerou muitas variações e clones (por exemplo, a Yamaha YM2149, que por si só se tornou a base de toda uma família de chips de som mais poderosos).
O AY-3-8910 possui 3 canais de sinais retangulares, um gerador de ruído que pode ser misturado com três canais e um gerador de envelope. Como havia apenas um gerador de envelope para os três canais, não era particularmente útil, e a maioria dos jogos usava uma CPU para modular o tom e o volume.
Isso significa que o chip AY-3-8910 requer mais intervenção da CPU para criar som de alta qualidade (diferente de outros chips SID independentes, por exemplo, em um computador C64).
É incrível ver o que pode ser feito em três chips de som bastante simples e na CPU que os controla. A música e os efeitos sonoros de Bomb Jack são muito mais ricos do que eu ouvi na maioria dos jogos de computador doméstico.
A única coisa realmente interessante nesta placa de som é a maneira como ela recebe seus comandos da placa principal.
Trava de comando de som
A “trava de som” é um armazenamento de byte único (trava de 8 bits) comum às placas principal e de som. A trava está vinculada ao endereço 0xB800 na placa principal e ao endereço 0x6000 na placa de som.
Quando a interrupção da NMI é ativada usando o VSYNC, a placa de som executa uma rotina de serviço de interrupção muito simples, que lê a trava do hardware, grava no endereço de memória normal e define o "bit de sinal", que informa ao "loop principal" que um novo comando de som foi recebido:
ex af,af' ;0066 exx ;0067 ld hl,04390h ;0068 set 0,(hl) ;006b ld a,(06000h) ;006d ld (04391h),a ;0070 exx ;0073 ex af,af' ;0074 retn ;0075
O método de ativação de contato da NMI é um pouco diferente do método da placa principal:
Na placa principal, o pino NMI se torna ativo durante a execução do VBLANK.
No entanto, na placa de som, a NMI é ativada quando o VSYNC é acionado e permanece ativo não durante o VBLANK, mas até o procedimento de serviço de interrupção ler os dados da trava em 0x6000.
Quando o equipamento reconhece a leitura do endereço 0x6000, ele executa duas operações codificadas:
- o conteúdo do clipe de som é redefinido para 0
- O contato com a MNI se torna inativo
De fato, esta é uma simples eliminação do ressalto de contato, que não permite que um comando de som seja executado duas vezes.
A única questão permanece: com que freqüência a placa principal escreve um novo comando (porque a maneira de implementar a emulação de duas placas depende disso).
Após a depuração com printf, descobri que a placa principal registra no máximo um comando de som por quadro de 60 Hz. Isso simplificou bastante a estrutura do "ciclo principal" do emulador.
O problema do trabalho conjunto de dois computadores emulados separados que precisam trocar dados entre si é que a emulação de um computador é efetiva apenas se puder executar muitos ciclos de cada vez sem interferência.
Por exemplo, o pior caso seria:
- nós executamos uma instrução no computador 1
- nós executamos uma instrução no computador 2
- repita ...
Meu emulador Z80 não é otimizado para sair e inserir emulação para cada instrução, pois nesse caso ele deve liberar na memória e carregar da memória o estado da CPU no início e no final de cada instrução. Se a CPU puder processar muitas instruções sem interferência, você poderá armazenar (a maior parte) o estado da CPU nos registradores e redefinir o estado para a memória na última instrução.
Ou seja, uma situação ideal seria a seguinte: executamos um sistema emulado sem interferência em todo o quadro do sistema host (para uma CPU com uma frequência de 4 MHz e 60 Hz, isso significa cerca de 67 mil ciclos por quadro, ou algo entre 3 mil e 16 mil instruções Z80).
Ao trabalhar com Bomb Jack, eu precisava garantir que a placa principal não registrasse um novo comando antes que a placa de som pudesse ler o último comando. Antes de descobrir que a placa principal registra não mais de um comando por quadro, considerei a necessidade de criar uma fila complexa de comandos que interceptasse gravações na trava de som da placa principal e armazenasse o número do ciclo e o byte de comando na fila.
Então, no momento em que a placa de som estava executando seu quadro, seria necessário um novo comando da fila de comandos quando o número do ciclo de comando era atingido.
Esse sistema funcionaria e seria "correto", mas aumentaria bastante a complexidade do código.
No final, decidi usar uma solução muito mais simples, sem filas. Como a placa principal registra apenas um comando por quadro, alternei a execução em dois computadores para que cada um deles executasse duas faixas de tempo por quadro:
- execute a primeira metade do quadro na placa principal
- execute a primeira metade do quadro na placa de som
- execute a segunda metade do quadro na placa principal
- execute a segunda metade do quadro na placa de som
Isso garante que a placa de som veja corretamente todos os comandos gravados pela placa principal e, ao mesmo tempo, execute cada emulação por milhares de ciclos.
Obviamente, o fato de o sistema host operar a uma taxa de quadros de 60 Hz é uma suposição muito ousada :)
E o ultimo ...
O último fato interessante sobre a versão do emulador no WebAssembly:
Tamanho compactado de todos os arquivos baixados ao executar o emulador no WebAssembly
aproximadamente igual a 113 Kbytes:
- cerca de 2,5 Kbytes para HTML, CSS e JS "manuscrito"
- 26,8 kb por arquivo JS de tempo de execução emscripten
- 83,7 KB por arquivo .wasm
O arquivo WASM contém as ROMs internas da máquina de fliperama.
Descompactadas, essas ROMs ocupam 112 Kbytes.
Ou seja,
todo o emulador compactado com ROMs integradas ocupa quase o mesmo volume que as ROMs não compactadas :)
ROMs de 112 kilobytes são compactadas para aproximadamente 57 KB, ou seja, o tamanho real do código compactado no WASM sem dados de ROM é menor que 30 KB (84-57).
Parece-me bastante bom para um emulador completo de um sistema de 8 bits;)