Programando como se fosse 1982. Parte VI – Hardware na Memória
Nos posts anteriores desta série construímos o modelo completo. Registradores, memória, instruções, decisões e loops. Você já sabe como um programa se move pela memória e como o processador executa cada byte que encontra pela frente. O que falta é a parte mais surpreendente da história do C64, e provavelmente a ideia mais elegante de toda a série.
Escrever na memória era acionar hardware
No post sobre o mapa de memória vimos que certas regiões não eram RAM comum. A faixa de $D000 a $DFFF pertencia aos chips de hardware. O VIC-II, o SID e o CIA. Mas o que isso significa na prática só fica claro quando você vê o código.
Para mudar a cor da borda da tela no C64, você escrevia:
LDA #$02 ; 2 = vermelho na paleta do C64
STA $D020 ; escreve no registrador de cor da borda
Duas instruções. Nenhuma chamada de sistema. Nenhum driver. Nenhuma API. Você escreveu um byte num endereço e a borda ficou vermelha imediatamente, no próximo ciclo de clock. Isso é memory-mapped I/O: os chips de hardware viviam no mesmo espaço de endereçamento que a RAM, e escrever nesses endereços não guardava dados. Comandava o hardware diretamente.
Não havia camada entre o código e o efeito. Nenhuma.
VIC-II, vídeo sem placa de vídeo
O VIC-II expunha seus 47 registradores na região $D000 a $D02E. Cada um controlava um aspecto do que aparecia na tela. Cores, posição de sprites, modo gráfico, scroll, sincronização com o feixe de vídeo.
A paleta do C64 era fixa em 16 cores, numeradas de 0 a 15. Cor 0 era preto, 1 branco, 2 vermelho e assim por diante. Para mudar o fundo:
LDA #$06 ; 6 = azul
STA $D021 ; registrador de cor do fundo
Sprites seguiam o mesmo padrão. Cada sprite tinha dois registradores para posição, um para X e outro para Y, mais bits de controle espalhados por outros registradores. Para habilitar o sprite 0 e posicioná-lo:
LDA #$01
STA $D015 ; bit 0 = sprite 0 habilitado
LDA #$60 ; posição X = 96
STA $D000 ; registrador X do sprite 0
LDA #$80 ; posição Y = 128
STA $D001 ; registrador Y do sprite 0
Um detalhe que mostra bem o nível de controle disponível: o VIC-II desenhava a tela linha por linha, de cima para baixo, 50 vezes por segundo no padrão PAL. O registrador $D012 informava em qual linha o feixe estava naquele exato momento. Programas que queriam efeitos visuais sincronizados com o raster, como mudar cores no meio da tela, precisavam esperar o feixe chegar na linha certa antes de escrever no registrador. Programadores experientes faziam isso em loops que contavam ciclos de clock, sabendo exatamente quantas instruções cabiam entre uma linha e outra.
SID, um sintetizador dentro de um chip
O SID tinha três vozes independentes. Cada uma podia tocar uma frequência diferente com sua própria forma de onda e envelope de amplitude. Para tocar uma nota, você escrevia a frequência nos registradores da voz, escolhia uma forma de onda e ativava o gate:
LDA #$0F
STA $D418 ; volume master = 15 (máximo)
LDA #$25 ; frequência byte baixo (nota Lá aproximada)
STA $D400
LDA #$1D ; frequência byte alto
STA $D401
LDA #$A0 ; attack rápido, decay médio
STA $D405
LDA #$F0 ; sustain alto, release longo
STA $D406
LDA #$11 ; forma de onda triangular + gate ON
STA $D404 ; a nota começa a tocar aqui
Oito escritas na memória e você tinha um oscilador triangular tocando uma nota com envelope completo. Para silenciar, desligue o gate. LDA #$10 / STA $D404. Para tocar uma nota diferente, basta mudar os registradores de frequência enquanto o gate está ativo.
O envelope ADSR, Attack, Decay, Sustain, Release, é um conceito vindo da síntese analógica. Define como o volume do som se comporta ao longo do tempo. O tempo que leva para atingir o volume máximo quando uma nota começa, como ele decai depois, qual volume sustenta enquanto a nota está pressionada e quanto tempo leva para silenciar após soltar. Os quatro parâmetros eram divididos entre os dois bytes em $D405 e $D406, quatro bits cada.
CIA, lendo o joystick
O CIA lidava com entradas, e ler o joystick era uma das operações mais comuns em jogos. O registrador $DC00 continha o estado das duas portas de controle:
LDA $DC00 ; lê a porta do joystick
AND #$10 ; isola o bit do botão de fogo
BNE nao_atirou ; se bit = 1, botão NÃO está pressionado
; chegou aqui: botão está pressionado
nao_atirou:
O detalhe contraintuitivo é a lógica invertida. Bit 0 significa que o botão está pressionado, bit 1 significa solto. Isso se chama active-low, comum em hardware da época onde o pino físico era puxado para baixo quando ativo. Cada direção do joystick correspondia a um bit:
Bit 0 → cima
Bit 1 → baixo
Bit 2 → esquerda
Bit 3 → direita
Bit 4 → botão de fogo
Verificar se o jogador estava indo para cima e para a direita ao mesmo tempo era apenas uma questão de checar os dois bits com operações lógicas. Não havia abstração de “evento de input”. Era leitura de hardware, crua.
O loop principal de um jogo
Com tudo isso em mãos, a estrutura de um jogo no C64 era direta:
game_loop:
JSR ler_joystick
JSR atualizar_jogador
JSR verificar_colisoes
JSR atualizar_sprites
JSR esperar_raster ; sincroniza com $D012
JMP game_loop
A sub-rotina esperar_raster era o coração da sincronização. Ela ficava num loop lendo $D012 até o feixe chegar a uma linha específica, geralmente abaixo da área visível da tela. Só então o loop avançava para o próximo frame. Isso garantia que todas as atualizações de sprites e cores acontecessem quando o feixe não estava desenhando, evitando o rasgo visual que ocorre quando você atualiza um sprite no meio do feixe passando por ele.
Esse padrão de sincronização manual com o hardware de vídeo é o ancestral direto do VSync que você conhece em engines de jogo modernas. A diferença é que no C64 você implementava à mão, sabendo exatamente o que estava fazendo e por quê.
Neste ponto você conhece os três chips e sabe o que cada endereço faz. O VIC-II muda o que aparece na tela. O SID gera som. O CIA lê o mundo externo. Cada um vive no mapa de memória e responde a escritas como se fossem comandos.
O que falta é ver os três funcionando juntos num programa real, não em exemplos isolados, mas coordenados por um loop que lê uma tecla, toca uma nota e muda uma cor ao mesmo tempo. É exatamente isso que vamos fazer no próximo post.