ALAN JAHNEL / Engenharia de software

Artigos

GBA em Assembly — 4. Pixels, cores e retângulos

Descubra como coordenadas viram endereços na VRAM e como cores RGB de cinco bits formam um retângulo em Mode 3.

Na primeira ROM, escrevemos diretamente a cor de um pixel. Agora vamos desenhar um retângulo verde sobre uma tela preta. Ainda não há jogador, inimigo ou regra de vitória: o único problema é converter uma área da tela em escritas na memória.

Use o arquivo completo retangulo.s, disponível também no pacote da série. Ele usa os mesmos gba.ld e rom.py do capítulo anterior. Os blocos abaixo destacam a rotina que vamos estudar; não substitua o arquivo inteiro por um desses trechos.

Roteiro deste capítulo

Antes de começar: construa e abra a ROM de tela vermelha do capítulo 3. Guarde o código que funcionou.

  1. Salve retangulo.s completo ao lado de gba.ld e rom.py. Abra no editor de texto; mantenha o .s anterior separado.
  2. Leia os exemplos de coordenadas e cores abaixo e localize fill_rect e sua chamada no arquivo completo. A chamada fornece os valores; a rotina realiza o desenho.
  3. Execute os quatro comandos de construção deste capítulo no terminal aberto nessa pasta e abra retangulo.gba no mGBA.
  4. Depois que o resultado original funcionar, mude apenas a largura da chamada do retângulo de 32 para 64. Mantenha a altura em 32, salve e repita os quatro comandos. Não altere a outra chamada, que limpa a tela inteira.

Ponto de conferência: primeiro aparece um quadrado verde de 32 por 32 sobre preto; depois da edição, ele tem 64 pixels de largura e 32 de altura. Se o fundo inteiro mudar, confira qual chamada você editou. Restaure a largura original antes de continuar.

A tela como uma sequência de números

No Mode 3, uma imagem tem 240 colunas e 160 linhas. Cada pixel ocupa uma meia palavra, ou dois bytes. A memória armazena uma linha depois da outra:

Texto simples
pixel (0,0)       -> 0x06000000
pixel (1,0)       -> 0x06000002
pixel (239,0)     -> 0x060001DE
pixel (0,1)       -> 0x060001E0

Uma linha ocupa 240 × 2 = 480 bytes. Ir para a direita significa avançar dois bytes. Ir para a mesma coluna da linha seguinte significa avançar 480 bytes. A fórmula geral é:

Texto simples
endereço(x,y) = 0x06000000 + 2 × (y × 240 + x)

Essa relação depende do modo de vídeo. Quando passarmos a tiles e sprites, escrever nessa mesma região terá outro significado. O circuito interpreta os dados segundo a configuração escolhida em DISPCNT.

Como três intensidades cabem em dois bytes

Cada componente de cor tem cinco bits, com intensidade de 0 a 31. Os bits de 0 a 4 guardam vermelho; de 5 a 9, verde; de 10 a 14, azul. O bit 15 não é um canal de transparência neste modo.

Texto simples
cor = vermelho | (verde << 5) | (azul << 10)
CorVermelhoVerdeAzulValor
Preto0000x0000
Vermelho31000x001F
Verde03100x03E0
Azul00310x7C00
Branco3131310x7FFF

Não copie uma cor de CSS como #FF0000 diretamente para a VRAM: o formato e a quantidade de bits são diferentes. Essa diferença não é uma particularidade do Assembly; qualquer linguagem que controle esse modo precisa respeitar o formato.

Uma função de desenho feita por nós

Consulte BL e BX para chamadas e retornos, e PUSH e POP para os valores preservados pela rotina.

Queremos chamar uma rotina passando posição, tamanho e cor. Neste programa, a convenção é:

RegistradorParâmetro
r0, r1X e Y do canto superior esquerdo
r2, r3Largura e altura, em pixels
r4Cor de 16 bits

É um acordo entre as rotinas deste exemplo, não uma promessa de compatibilidade com qualquer função C. Chamamos a rotina com bl fill_rect. bl guarda um endereço de retorno em lr, e bx lr retorna para o chamador.

A implementação completa da rotina é:

<!-- source: retangulo.s#pixels -->
Assembly ARM
@ r0=x, r1=y, r2=largura, r3=altura, r4=cor.
@ Exige largura/altura positivas e retangulo inteiramente dentro da tela.
fill_rect:
    push {r5-r8, lr}
    mov r5, #240
    mul r6, r1, r5
    add r6, r6, r0
    ldr r5, =VRAM
    add r5, r5, r6, lsl #1
    rsb r7, r2, #240
    mov r7, r7, lsl #1
    mov r6, r3
row:
    mov r8, r2
pixel:
    strh r4, [r5], #2
    subs r8, r8, #1
    bne pixel
    add r5, r5, r7
    subs r6, r6, #1
    bne row
    pop {r5-r8, lr}
    bx lr

push {r5-r8, lr} guarda na pilha os valores que a rotina quer preservar; pop os recupera no final. O ponteiro sp foi configurado no boot. Como a pilha ocupa RAM, salvar e restaurar também são operações de memória.

mul r6, r1, r5 calcula Y × 240; somamos X e multiplicamos por dois com lsl #1. Somar isso ao início da VRAM produz o endereço do primeiro pixel do retângulo.

Por que precisamos de dois laços?

A rotina combina MUL, ADD, SUB e RSB, deslocamentos e endereçamento pós-indexado.

Um retângulo de 32 por 32 tem 32 pixels por linha e 32 linhas. O laço pixel escreve uma linha; o laço row repete o processo para as demais linhas.

Depois de escrever os 32 pixels, o ponteiro já avançou 64 bytes. Para chegar ao início da linha seguinte do retângulo, faltam (240 - 32) × 2 = 416 bytes. Esse salto é guardado em r7.

Se esquecêssemos esse ajuste, escreveríamos os pixels seguintes ao lado dos anteriores, como uma faixa comprida. A CPU não conhece a ideia de retângulo: nós a implementamos com uma sequência de endereços e dois contadores.

A rotina exige largura e altura positivas e uma área completamente dentro da tela. Ela não recorta automaticamente coordenadas inválidas. Nos exemplos, vamos respeitar 0 ≤ X, 0 ≤ Y, X + largura ≤ 240 e Y + altura ≤ 160. Uma largura zero também seria um erro, pois o laço escreve antes de diminuir o contador.

Usar a rotina

No arquivo completo, o boot limpa a tela com um retângulo preto de 240 por 160 e depois prepara esta chamada:

Assembly ARM
mov r0, #104
mov r1, #64
mov r2, #32
mov r3, #32
ldr r4, =0x03e0
bl fill_rect

O retângulo ocupa X de 104 a 135 e Y de 64 a 95. Seu centro coincide com o centro da imagem. O vídeo fica em forced blank durante a preparação e é habilitado depois. Por ser uma imagem estática, podemos parar no laço final.

Monte o exemplo com:

Shell
arm-none-eabi-as -mcpu=arm7tdmi -g -o retangulo.o retangulo.s
arm-none-eabi-ld -T gba.ld -o retangulo.elf retangulo.o
arm-none-eabi-objcopy -O binary retangulo.elf retangulo.gba
python3 rom.py retangulo.gba

Abra retangulo.gba no mGBA. O resultado esperado é um quadrado verde de 32 por 32 sobre fundo preto.

Captura do exemplo no mGBA: quadrado verde de 32 por 32 pixels sobre uma tela preta

Da forma simples ao cenário de um jogo

Uma estrada pode ser um retângulo comprido. Uma barra de vida pode ser um retângulo cuja largura depende de uma variável. Um tabuleiro pode converter coluna e linha em X e Y antes de desenhar. Esses elementos são interpretações do nosso programa; para o hardware, continuam sendo cores na VRAM.

Vamos continuar com o quadrado simples nos próximos capítulos. Ele permite estudar tempo e controles sem acrescentar o problema de organizar um mapa inteiro.

Como exercício, mude apenas a largura para 64. O objeto deve se tornar um retângulo horizontal. Depois, mude a cor para azul. Por último, desenhe dois retângulos sobrepostos, chamando a rotina duas vezes antes de habilitar a tela: a última escrita vence na área compartilhada. Não existe uma lista de objetos preservando automaticamente o retângulo de baixo no Mode 3.

A referência de modos bitmap do Tonc descreve o formato da imagem e as diferenças entre os modos. A relação entre cor, memória e configuração de vídeo é o ponto que precisamos levar daqui.

Anterior: primeira ROM. Próximo: fazer o quadrado piscar no ritmo da tela. Índice da série.

Voltar ao início