PPU / 02

Del tile al primer píxel de fondo en la NES

Cómo la PPU convierte los 16 bytes de un tile en una imagen de 8 por 8 píxeles.

Tener el CPU funcionando está bueno, pero en algún momento querés que dibuje algo. Acá la NES se pone interesante: su fondo no funciona escribiendo un color en una posición cualquiera de la pantalla.

La PPU arma la imagen con tiles, un mapa de números de tile y paletas. Vamos a seguir un píxel por ese recorrido. No necesitás una ROM de un juego ni un editor gráfico; el ejemplo genera un cartucho chico con sus propios gráficos.

Cambiá un bit y ejecutá la ROM

Cada tile mide 8 por 8 píxeles. El editor de abajo cambia dos bytes de una fila del tile. Tocá los bits, ejecutalo y mirá qué dibuja la PPU. La imagen ampliada es un recorte del framebuffer del emulador WebAssembly.

Con la fila 3 seleccionada, los bytes iniciales son $58 y $38. Sus índices de píxel combinados son 0 1 2 3 3 0 0 0, de izquierda a derecha. Cambiá solamente el bit de más a la izquierda del plano bajo. Cambia el primer píxel de esa fila.

Ejecutá un programa de NES

El resultado lo dibuja el mismo emulador WebAssembly de la página principal.

Plano bajo
Plano alto

Índices calculados de píxel, de izquierda a derecha0 1 2 3 3 0 0 0

; CHR tile 32, row 3
; low plane:  $58
; high plane: $38
; pixel = (highBit << 1) | lowBit
Ejecutá la ROM para ver su salida.Framebuffer de NES · WASM

Listo. Cambiá los valores o ejecutá el ejemplo inicial.

La ROM incluye código de arranque y dibujo alrededor del ejemplo. Código de la ROM

Cada ejecución vuelve a armar el cartucho con tus bytes CHR y arranca el emulador. Estamos editando gráficos guardados en CHR-ROM, no haciendo que el programa escriba en ROM mientras corre. Podés descargar el archivo .nes generado y probarlo también en el emulador completo.

La nametable elige un tile

Una nametable de fondo contiene una grilla de 32 por 30 números de tile. A 8 píxeles por tile, cubre la pantalla de 256 por 240. El número de tile no es un color. Elige un patrón de una pattern table.

Nuestro programa pone el tile 32 en la esquina superior izquierda de la nametable. Selecciona la pattern table que empieza en la dirección PPU $0000, así que el primer byte de ese tile está en 32 * 16, o $0200 en memoria de la PPU.

Esa dirección pertenece al espacio de la PPU. No es la RAM $0200 del CPU que usamos en la guía anterior. Los dos chips tienen mapas de memoria distintos. Conviene acordarse de eso antes de pasarse una hora mirando el array equivocado.

Dos bits, guardados por separado

Un tile ocupa 16 bytes. Los primeros ocho guardan un bit por píxel para cada una de sus ocho filas. Los ocho siguientes guardan el segundo bit de esos mismos píxeles. Son los bitplanes, o planos de bits, bajo y alto.

Para la fila 3, el byte bajo está en tileBase + 3 y el alto en tileBase + 3 + 8. Para la columna x, leemos el bit 7 - x de cada byte. El bit 7 es el píxel de más a la izquierda.

En Crystal, la operación es así, siguiendo el enfoque del libro:

bit = 7 - x
low_bit = (low_plane >> bit) & 1
high_bit = (high_plane >> bit) & 1
pixel = (high_bit << 1) | low_bit

Para $58 y $38, las dos filas de bits quedan así:

bajo:    0 1 0 1 1 0 0 0
alto:    0 0 1 1 1 0 0 0
índice:  0 1 2 3 3 0 0 0

En la columna 2, el bit bajo es 0 y el alto es 1. Eso da 10 en binario, o índice 2. En la columna 3 los dos bits son 1, así que da índice 3. Sin multiplicar por un color RGB ni llamar a una API de dibujo. Estamos combinando dos bits en un número chico.

El índice todavía no es un color

El fondo tiene cuatro paletas. Una attribute table elige la paleta para cada región de 16 por 16 píxeles, o un bloque de dos por dos tiles. Nuestro programa deja los atributos en cero, así que usa la paleta de fondo 0.

Para los índices de píxel distintos de cero, la paleta y el índice de dos bits seleccionan una entrada de la RAM de paletas. Esa entrada guarda uno de los 64 códigos de color de la PPU. En este ejemplo, los índices 1, 2 y 3 usan $21, $16 y $30.

El índice 0 tiene un papel especial: un píxel de fondo con índice 0 deja ver el color de fondo universal. Como este ejemplo no tiene sprites, se ve el color de $3F00, que pusimos en $0F.

El RGB exacto de un color NES depende de la paleta que use el emulador o la pantalla. Lo que estamos probando acá es qué entrada de la paleta se selecciona.

Siguiendo el recorrido completo

Para un píxel de fondo, el recorrido simplificado es:

  1. Buscar su posición de tile en la nametable.
  2. Leer el número de ese tile.
  3. Leer los bytes bajo y alto de esa fila en la pattern table.
  4. Sacar un bit de cada uno y combinarlos en un índice de 0 a 3.
  5. Usar la paleta elegida por el atributo, o el fondo universal si el índice es 0, para obtener el color.

La PPU real busca y desplaza datos siguiendo sus ciclos, en vez de ejecutar esta lista como una función prolija por cada píxel. El scroll también cambia las direcciones que usa. Este es el recorrido de los datos que conviene entender antes de agregar esos detalles.

Algo para probar

Elegí una fila y poné todos sus bits en cero, en los dos planos. Después activá solamente el bit 7 del plano alto. Qué píxel debería cambiar y qué índice debería usar?

Ver el resultado

El píxel de más a la izquierda cambia al índice 2. Los otros siete quedan en el índice 0. Si también activás el bit 7 del plano bajo, ese primer píxel pasa al índice 3.

Dónde sigue en el libro

El capítulo 5 presenta tiles, nametables, atributos y paletas de la PPU. El 5.3 los conecta al renderer de fondo en Crystal y agrega el scroll que necesitan los juegos. Este ejemplo se queda con un tile y una paleta para que se vea bien la parte de los bitplanes.

Referencias: pattern tables de la PPU, mapa de memoria de la PPU, renderizado de la PPU, código del libro.

Capítulos relacionados del libro: 5 y 5.3. Crystal.