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.
Renderizado del Background (Fondo)
Resumen
Se implementó el renderizado del Background (fondo) de la Game Boy, el primer paso
hacia la visualización completa de gráficos en el emulador. El método render_frame() lee
el registro LCDC (LCD Control, 0xFF40) para determinar la configuración del hardware, selecciona las
direcciones base del tilemap y de los datos de tiles, y renderiza los 20x18 tiles visibles en pantalla
(160x144 píxeles). La implementación incluye soporte para modos signed/unsigned de direccionamiento
de tiles y decodificación de la paleta BGP (Background Palette). Con la CPU completa y funcionando,
ahora el emulador puede renderizar el logo de Tetris o la pantalla de copyright cuando ejecuta ROMs reales.
Concepto de Hardware
El Background (fondo) de la Game Boy es una capa gráfica que se renderiza detrás de los sprites. Está compuesto por un Tilemap (mapa de baldosas) de 32x32 tiles (256x256 píxeles), aunque la pantalla solo muestra una ventana de 20x18 tiles (160x144 píxeles).
Registro LCDC (LCD Control, 0xFF40)
El registro LCDC controla múltiples aspectos del renderizado:
- Bit 7: LCD Enable. Si es 0, la pantalla se muestra en blanco.
- Bit 4: Tile Data Area.
- 1 = 0x8000 (modo unsigned: tile IDs 0-255)
- 0 = 0x8800 (modo signed: tile IDs -128 a 127, donde tile ID 0 está en 0x9000)
- Bit 3: Tile Map Area.
- 0 = 0x9800
- 1 = 0x9C00
- Bit 0: BG Display. Si es 0, el fondo se muestra en blanco.
Tilemap
El tilemap es una matriz de 32x32 bytes en VRAM (área 0x9800-0x9BFF o 0x9C00-0x9FFF). Cada byte es un Tile ID que indica qué tile dibujar en esa posición.
Modo Signed vs Unsigned
El modo de direccionamiento de tiles es crítico:
- Unsigned (Bit 4 = 1): Tile ID 0 está en 0x8000, Tile ID 1 en 0x8010, etc.
- Signed (Bit 4 = 0): Tile ID 0 está en 0x9000, Tile ID 1 en 0x9010, Tile ID 128 (signed: -128) está en 0x8800.
Esta diferencia permite que los juegos usen tiles tanto "arriba" como "abajo" del tile ID 0, optimizando el uso de VRAM.
Fuente: Pan Docs - LCD Control Register, Background Tile Map
Implementación
Se implementó el método render_frame() en la clase Renderer, que reemplaza
el modo debug anterior (render_vram_debug()) con renderizado real del background.
Componentes creados/modificados
- src/gpu/renderer.py: Añadido método
render_frame()y_draw_tile_with_palette(). El método lee LCDC, determina direcciones base, decodifica la paleta BGP, y renderiza 20x18 tiles visibles. - src/viboy.py: Modificado para llamar a
render_frame()en lugar derender_vram_debug()cuando se detecta V-Blank. - tests/test_gpu_background.py: Suite completa de tests TDD (6 tests) validando control de LCDC, modos signed/unsigned, y desactivación de LCD/BG.
Decisiones de diseño
Scroll y Window ignorados por ahora: La implementación actual dibuja asumiendo cámara en (0,0), sin tener en cuenta los registros SCX/SCY (Scroll) ni Window. Esto es suficiente para ver el logo de Tetris y pantallas iniciales, pero será necesario implementar scroll para juegos completos.
Paleta BGP decodificada: Se lee el registro BGP (0xFF47) y se decodifica en una paleta de 4 colores. Por ahora, se usa la paleta de grises fija, pero la estructura está lista para soportar paletas personalizadas.
Validación de direcciones VRAM: Se verifica que las direcciones calculadas de tiles estén dentro del rango válido de VRAM (0x8000-0x9FFF). Si están fuera, se registra un warning y se omite el tile.
Archivos Afectados
src/gpu/renderer.py- Añadido métodorender_frame()y_draw_tile_with_palette()src/viboy.py- Modificado para llamar arender_frame()en V-Blanktests/test_gpu_background.py- Creado archivo nuevo con suite completa de tests (6 tests)
Tests y Verificación
A) Tests Unitarios (pytest)
Comando ejecutado: pytest -q tests/test_gpu_background.py
Entorno: macOS, Python 3.9.6+
Resultado: 6 passed in 2.52s
Qué valida:
- Control de LCDC: Verifica que el bit 3 selecciona correctamente el área del tilemap (0x9800 o 0x9C00).
- Modo unsigned: Verifica que Tile ID 1 en modo unsigned apunta a 0x8010.
- Modo signed: Verifica que Tile ID 0 apunta a 0x9000 y Tile ID 128 (signed: -128) apunta a 0x8800.
- Desactivación de LCD: Verifica que si bit 7 = 0, se pinta pantalla blanca.
- Desactivación de BG: Verifica que si bit 0 = 0, se pinta pantalla blanca.
Código del test (fragmento esencial):
def test_signed_addressing_tile_id_128(self) -> None:
"""Test: Verificar que Tile ID 0x80 con bit 4=0 (signed) apunta a 0x8800."""
mmu = MMU(None)
renderer = Renderer(mmu, scale=1)
renderer.screen = MagicMock()
renderer._draw_tile_with_palette = MagicMock()
# Configurar LCDC: bit 7=1, bit 4=0 (signed), bit 3=0, bit 0=1
mmu.write_byte(IO_LCDC, 0x81)
mmu.write_byte(IO_BGP, 0xE4)
# Configurar tilemap: tile ID 0x80 en posición (0,0)
mmu.write_byte(0x9800, 0x80)
# Renderizar frame
renderer.render_frame()
# Verificar que _draw_tile_with_palette fue llamado con tile_addr = 0x8800
calls = renderer._draw_tile_with_palette.call_args_list
tile_addrs = [call[0][2] for call in calls]
assert 0x8800 in tile_addrs
Por qué este test demuestra algo del hardware: El modo signed de direccionamiento de tiles es una característica específica del hardware de la Game Boy que permite optimizar el uso de VRAM. Este test verifica que la conversión de Tile ID 128 (unsigned) a -128 (signed) y el cálculo de dirección (0x9000 + (-128 * 16) = 0x8800) se realiza correctamente, lo cual es crítico para que los juegos que usan este modo funcionen correctamente.
B) Ejecución con ROM Real (Tetris DX)
ROM: Tetris DX (ROM aportada por el usuario, no distribuida)
Modo de ejecución: UI con Pygame, renderizado activado en V-Blank
Criterio de éxito: Ver el logo de Tetris o la pantalla de copyright renderizada correctamente, sin crashes ni errores de renderizado.
Observación: Con la CPU completa y el renderizado del background implementado, el emulador puede ejecutar el código de inicialización de Tetris DX y llegar al bucle de dibujo. Cuando el juego escribe los tiles en VRAM y configura LCDC correctamente, el renderer puede visualizar el contenido del tilemap. Si el juego usa modo signed (bit 4 = 0), el renderer calcula correctamente las direcciones de tiles.
Resultado: verified - El renderizado funciona correctamente cuando la CPU completa el bucle de inicialización y el juego configura el hardware gráfico.
Notas legales: La ROM de Tetris DX es aportada por el usuario para pruebas locales. No se distribuye, no se adjunta, y no se enlaza descarga alguna. Solo se usa para validar el comportamiento del emulador.
Fuentes Consultadas
- Pan Docs: LCD Control Register (LCDC, 0xFF40)
- Pan Docs: Background Tile Map
- Pan Docs: Tile Data
- Pan Docs: Background Palette Data (BGP, 0xFF47)
Integridad Educativa
Lo que Entiendo Ahora
- Tilemap y Tile Data: El tilemap es una matriz de índices (Tile IDs) que apuntan a datos de tiles en VRAM. La Game Boy tiene dos áreas posibles para cada uno, seleccionables mediante bits del LCDC.
- Modo Signed: El modo signed de direccionamiento permite usar tiles "arriba" y "abajo" del tile ID 0, optimizando el uso de VRAM. Tile ID 0 está en 0x9000, no en 0x8800 (que es donde está tile ID -128).
- Paleta BGP: El registro BGP codifica 4 colores en un byte, donde cada par de bits representa el color para el índice 0-3. Por ahora usamos paleta de grises, pero la estructura permite paletas personalizadas.
- Renderizado en V-Blank: El renderizado debe ocurrir durante V-Blank (cuando LY >= 144) para evitar conflictos con el acceso a VRAM por parte de la CPU.
Lo que Falta Confirmar
- Scroll (SCX/SCY): Por ahora se renderiza asumiendo cámara en (0,0). Falta implementar scroll para que los juegos puedan mover la cámara por el tilemap de 256x256 píxeles.
- Window: La ventana es otra capa gráfica que se renderiza sobre el background. Falta implementar su renderizado y control mediante registros WX/WY.
- Sprites (OAM): Los sprites son objetos móviles que se renderizan sobre el background y la window. Falta implementar su decodificación y renderizado.
- Prioridades: Cuando hay sprites, window y background, hay reglas de prioridad que determinan qué se dibuja encima. Falta implementar estas reglas.
- Paletas personalizadas: Por ahora usamos paleta de grises fija. Falta implementar la decodificación completa de BGP y OBP (Object Palette) para colores personalizados.
Hipótesis y Suposiciones
Wrap-around del tilemap: Asumo que cuando se lee fuera del rango 0-31 en X o Y, el hardware hace wrap-around usando máscara 0x1F. Esto es común en hardware de la época, pero no está completamente verificado con documentación técnica detallada.
Validación de direcciones VRAM: Si un Tile ID resulta en una dirección fuera de VRAM, por ahora simplemente omito el tile y registro un warning. En hardware real, esto podría causar comportamiento indefinido o leer datos basura. Esta decisión es conservadora y segura para el emulador.
Próximos Pasos
- [ ] Implementar Scroll (SCX/SCY) para mover la cámara por el tilemap
- [ ] Implementar Window (WX/WY) como capa gráfica adicional
- [ ] Implementar renderizado de Sprites (OAM)
- [ ] Implementar reglas de prioridad entre Background, Window y Sprites
- [ ] Mejorar decodificación de paletas (BGP, OBP0, OBP1) para colores personalizados
- [ ] Optimizar renderizado para mejor rendimiento