Este proyecto es educativo y Open Source. No se copia código de otros emuladores. Implementación basada únicamente en documentación técnica y tests permitidas.
PPU Fase E - Renderizado de Sprites
Resumen
Este Step implementa el renderizado de Sprites (OBJ - Objects) en la PPU de C++. Hasta ahora, la PPU solo podía renderizar el Background (fondo), pero con la DMA funcionando (Step 0251), la memoria OAM (`0xFE00-0xFE9F`) ahora contiene datos válidos de los personajes y objetos del juego. Este Step completa el pipeline de renderizado permitiendo que los sprites se dibujen encima del fondo, respetando transparencia, prioridad y atributos (flip X/Y, paleta).
Concepto de Hardware
Los Sprites (OBJ - Objects) son objetos móviles que se dibujan encima del Background y la Window. En hardware real, la Game Boy puede mostrar hasta 40 sprites en total, pero solo 10 por línea de escaneo (scanline).
Estructura de OAM (Object Attribute Memory)
La memoria OAM (`0xFE00-0xFE9F`) contiene 160 bytes organizados en 40 entradas de 4 bytes cada una:
- Byte 0 (Y Position): Posición Y en pantalla + 16. Si Y = 0, el sprite está oculto.
- Byte 1 (X Position): Posición X en pantalla + 8. Si X = 0, el sprite está oculto.
- Byte 2 (Tile ID): Índice del tile en VRAM (0-255). Los sprites siempre usan direccionamiento unsigned desde `0x8000`.
- Byte 3 (Attributes): Atributos del sprite:
- Bit 7: Prioridad (0=encima del fondo, 1=detrás del fondo excepto color 0)
- Bit 6: Y-Flip (0=normal, 1=volteado verticalmente)
- Bit 5: X-Flip (0=normal, 1=volteado horizontalmente)
- Bit 4: Paleta (0=OBP0, 1=OBP1)
- Bits 0-3: No usados (reservados para CGB)
Tamaño de Sprites
El tamaño de los sprites está controlado por el bit 2 del registro LCDC:
- Bit 2 = 0: Sprites de 8x8 píxeles (un solo tile)
- Bit 2 = 1: Sprites de 8x16 píxeles (dos tiles verticales, tile_id y tile_id+1)
Prioridad y Transparencia
Los sprites tienen reglas especiales de renderizado:
- Color 0 = Transparente: El color 0 en un sprite siempre es transparente y no se dibuja.
- Prioridad (Bit 7 de Attributes):
- Si Prioridad = 0: El sprite se dibuja encima del fondo siempre.
- Si Prioridad = 1: El sprite se dibuja detrás del fondo, excepto si el fondo es color 0 (transparente).
Límite de Hardware
En hardware real, solo se pueden dibujar 10 sprites por línea de escaneo. Si hay más de 10 sprites que intersectan con una línea, solo los primeros 10 (en orden de OAM) se dibujan. Los demás se ignoran silenciosamente.
Fuente: Pan Docs - OAM, Sprite Attributes, Sprite Rendering
Implementación
Se completó la implementación del método `render_sprites()` en `PPU.cpp` y se integró en `render_scanline()` para que los sprites se dibujen después del Background.
Componentes modificados
- `src/core/cpp/PPU.cpp`:
- Completada la implementación de `render_sprites()` con toda la lógica de renderizado.
- Integrada la llamada a `render_sprites()` en `render_scanline()` después de renderizar el Background.
Lógica de Renderizado
El método `render_sprites()` implementa el siguiente algoritmo:
- Verificación de Habilitación: Verifica que los sprites estén habilitados (LCDC bit 1).
- Determinación de Altura: Lee el bit 2 de LCDC para determinar si los sprites son 8x8 o 8x16.
- Iteración sobre OAM: Itera sobre los 40 sprites en OAM (`0xFE00-0xFE9F`).
- Filtrado por Visibilidad:
- Verifica que Y y X no sean 0 (sprite oculto).
- Verifica que el sprite intersecte con la línea actual (LY).
- Decodificación de Atributos: Extrae prioridad, Y-Flip, X-Flip y paleta.
- Cálculo de Línea del Sprite: Calcula qué línea del sprite se está dibujando, aplicando Y-Flip si es necesario.
- Manejo de Sprites 8x16: Si el sprite es 8x16, determina qué tile usar (superior o inferior).
- Decodificación del Tile: Usa `decode_tile_line()` para decodificar la línea del tile desde VRAM.
- Renderizado de Píxeles: Para cada píxel del sprite:
- Aplica X-Flip si es necesario.
- Verifica que el píxel esté dentro de los límites de pantalla.
- Verifica transparencia (color 0 = no dibujar).
- Respetar prioridad: si prioridad = 1 y el fondo no es color 0, no dibujar.
- Escribe el índice de color en el framebuffer.
Decisiones de Diseño
- Límite de 10 Sprites por Línea: Se implementó un contador `sprites_drawn` que limita el renderizado a 10 sprites por línea, respetando el comportamiento del hardware real.
- Prioridad del Fondo: Se verifica el color del fondo en cada píxel antes de dibujar el sprite. Si el sprite tiene prioridad y el fondo no es transparente, el sprite no se dibuja.
- Transparencia: El color 0 del sprite siempre es transparente, independientemente de la prioridad.
- Paleta: Los índices de color (0-3) se guardan en el framebuffer. La aplicación de la paleta (OBP0/OBP1) se hace en Python al renderizar.
Archivos Afectados
src/core/cpp/PPU.cpp- Completada implementación de `render_sprites()` e integración en `render_scanline()`
Tests y Verificación
La implementación se validará ejecutando ROMs comerciales que usan sprites extensivamente:
- Pokémon Red: Debe mostrar el logo de "POKÉMON" y los sprites de Gengar y Jigglypuff (o Nidorino) en la pantalla de título.
- Super Mario Deluxe: Debe mostrar a Mario y otros sprites en pantalla.
- Tetris: Si logra arrancar, debe mostrar las piezas (tetrominos) como sprites.
Comando de prueba:
python main.py roms/pkmn.gb
Validación esperada: Los sprites deben aparecer encima del fondo, respetando transparencia y prioridad.
Fuentes Consultadas
- Pan Docs: OAM (Object Attribute Memory)
- Pan Docs: Sprite Attributes
- Pan Docs: Sprite Rendering
Integridad Educativa
Lo que Entiendo Ahora
- OAM y Sprites: La memoria OAM contiene los atributos de los 40 sprites posibles. Cada sprite tiene posición, tile ID y atributos (prioridad, flip, paleta).
- Prioridad del Fondo: Los sprites con prioridad (bit 7 = 1) se dibujan detrás del fondo, excepto si el fondo es color 0 (transparente). Esto permite efectos como sprites que pasan "detrás" de objetos del fondo.
- Transparencia: El color 0 en sprites siempre es transparente, permitiendo formas irregulares y efectos de superposición.
- Sprites 8x16: Los sprites grandes usan dos tiles consecutivos (tile_id y tile_id+1), uno para la mitad superior y otro para la inferior.
Lo que Falta Confirmar
- Orden de Renderizado: En hardware real, los sprites se dibujan en orden inverso (sprite 39 primero, sprite 0 último), lo que hace que los sprites con índice menor aparezcan encima. Esta implementación actual dibuja en orden directo, lo que podría causar problemas de ordenamiento visual.
- Límite de 10 Sprites: El límite de 10 sprites por línea se implementó, pero no se ha verificado si el hardware realmente detiene el renderizado después de 10 sprites o si simplemente ignora los demás.
Hipótesis y Suposiciones
Orden de Renderizado: Por simplicidad, se implementó el renderizado en orden directo (sprite 0 a sprite 39). Si hay problemas de ordenamiento visual (sprites apareciendo en el orden incorrecto), será necesario invertir el orden de iteración o implementar un sistema de ordenamiento por prioridad.
Próximos Pasos
- [ ] Ejecutar `python main.py roms/pkmn.gb` y verificar que los sprites aparecen correctamente.
- [ ] Si hay problemas de ordenamiento visual, implementar renderizado en orden inverso (sprite 39 a sprite 0).
- [ ] Verificar que la prioridad del fondo funciona correctamente (sprites pasando detrás de objetos).
- [ ] Implementar renderizado de Window (si es necesario para completar el pipeline de renderizado).