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.
Migración de PPU (Timing y Estado) a C++
Resumen
Se migró la lógica de timing y estado de la PPU (Pixel Processing Unit) a C++, implementando el motor de estados que gestiona los modos PPU (0-3), el registro LY, las interrupciones V-Blank y STAT. Esta es la Fase A de la migración de PPU, enfocada en el timing preciso sin renderizado de píxeles (que será la Fase B). La implementación mantiene toda la lógica de sincronización crítica de la v0.0.1 pero ahora ejecuta en código nativo para evitar el cuello de botella del cambio de contexto Python-C++.
Concepto de Hardware
La PPU (Pixel Processing Unit) de la Game Boy es responsable de generar la señal de video y mantener la sincronización de la pantalla. En esta primera fase, nos enfocamos únicamente en el motor de timing, que es crítico para la sincronización precisa de la emulación a 60 FPS.
Timing de Scanlines
La pantalla de la Game Boy tiene 144 líneas visibles (0-143) seguidas de 10 líneas de V-Blank (144-153), para un total de 154 líneas por frame. Cada línea de escaneo tarda exactamente 456 T-Cycles (ciclos de reloj), lo que da un total de 70,224 T-Cycles por frame (~59.7 FPS).
Modos PPU
Cada línea visible (0-143) se divide en 3 modos que representan diferentes fases del proceso de renderizado:
- Mode 2 (OAM Search): 0-79 ciclos. La PPU busca sprites en OAM (Object Attribute Memory). La CPU está bloqueada de OAM durante este período.
- Mode 3 (Pixel Transfer): 80-251 ciclos (172 ciclos). La PPU dibuja píxeles leyendo VRAM. La CPU está bloqueada de VRAM y OAM.
- Mode 0 (H-Blank): 252-455 ciclos (204 ciclos). Descanso horizontal. La CPU puede acceder libremente a VRAM.
- Mode 1 (V-Blank): Líneas 144-153 completas. Descanso vertical. La CPU puede acceder libremente a VRAM durante todo el V-Blank.
Registros Críticos
- LY (0xFF44): Línea actual (0-153). Solo lectura desde software.
- LYC (0xFF45): Comparador de línea. Cuando LY == LYC, se activa el bit 2 de STAT.
- STAT (0xFF41): Estado del LCD. Bits 0-1 = modo actual, bit 2 = LYC match, bits 3-6 = enables de interrupciones.
- LCDC (0xFF40): Control del LCD. Bit 7 = LCD enabled (si está apagado, la PPU se detiene y LY=0).
- IF (0xFF0F): Flags de interrupción. Bit 0 = V-Blank, bit 1 = STAT.
Fuente: Pan Docs - LCD Timing, V-Blank, STAT Register, LCD Control Register
Implementación
Se creó la clase PPU en C++ que replica toda la lógica de timing de la
implementación Python, pero ejecutándose en código nativo. La PPU utiliza inyección de
dependencias (recibe un puntero a MMU) para acceder a los registros I/O y solicitar
interrupciones.
Componentes creados/modificados
- PPU.hpp: Declaración de la clase PPU con constantes de timing y métodos públicos/privados.
- PPU.cpp: Implementación completa del motor de timing, gestión de modos e interrupciones.
- ppu.pxd: Definiciones Cython para la clase C++.
- ppu.pyx: Wrapper Python que expone PyPPU con propiedades y métodos.
- native_core.pyx: Incluye ppu.pyx para hacer PyPPU disponible desde viboy_core.
- setup.py: Añadido PPU.cpp a la lista de fuentes de compilación.
- tests/test_core_ppu_timing.py: Suite completa de 8 tests para validar la implementación nativa.
Decisiones de diseño
- Inyección de dependencias: La PPU recibe un puntero a MMU en el constructor. No posee la MMU, solo la usa, evitando problemas de ownership y permitiendo que la MMU Python existente pueda ser compartida con otros componentes.
- Tipo de ciclos: El método
step()recibeinten lugar de tipos pequeños (uint8_t,uint16_t) para evitar overflow. Los tests avanzan miles de ciclos de una vez para simular frames completos. - Clock interno como uint32_t: El contador interno
clock_esuint32_tpara poder acumular hasta ~70K ciclos por frame sin overflow. Inicialmente erauint16_t, causando bugs sutiles cuando se procesaban múltiples líneas a la vez. - Gestión de STAT: La PPU lee y escribe directamente en STAT usando
mmu_->read/write(). En la versión Python había métodos especiales (write_byte_internal) para evitar recursión, pero en C++ la MMU es simple y no tiene esa complejidad. - Rising Edge Detection: Las interrupciones STAT se disparan solo en "rising edge" (cuando
la condición pasa de False a True), controlado por el flag
stat_interrupt_line_. Esto previene múltiples interrupciones en la misma línea.
Detalles técnicos críticos
- LCD Enabled Check: Si el bit 7 de LCDC es 0, la PPU se detiene completamente: no acumula ciclos ni avanza líneas. LY se mantiene en 0.
- Wrap-around de frame: Cuando LY > 153, se reinicia a 0 y comienza un nuevo frame.
- V-Blank interrupt: Se activa cuando LY == 144, escribiendo el bit 0 de IF. Esto ocurre SIEMPRE, independientemente del estado de IME (permite polling manual).
- STAT interrupt: Se activa según 4 condiciones (LYC match, Mode 0/1/2 enable), escribiendo el bit 1 de IF. Solo se dispara en rising edge.
Archivos Afectados
src/core/cpp/PPU.hpp- Declaración de clase PPUsrc/core/cpp/PPU.cpp- Implementación de motor de timingsrc/core/cython/ppu.pxd- Definiciones Cythonsrc/core/cython/ppu.pyx- Wrapper Pythonsrc/core/cython/native_core.pyx- Incluye ppu.pyxsetup.py- Añadido PPU.cpp a compilacióntests/test_core_ppu_timing.py- Suite de tests (8 tests, todos pasando)
Tests y Verificación
Se creó una suite completa de tests que valida todos los aspectos críticos del timing:
- test_ly_increment: Verifica que LY se incrementa correctamente después de 456 T-Cycles.
- test_ly_increment_partial: Verifica que LY no cambia con menos de 456 ciclos.
- test_vblank_trigger: Valida que la interrupción V-Blank se activa cuando LY == 144.
- test_frame_wrap: Verifica que LY se reinicia a 0 después de la línea 153.
- test_ppu_modes: Valida que los modos PPU se actualizan correctamente según el timing.
- test_lyc_match_stat_interrupt: Verifica interrupciones STAT cuando LY == LYC.
- test_lcd_disabled: Valida que la PPU se detiene cuando el LCD está deshabilitado.
- test_multiple_frames: Verifica procesamiento de múltiples frames completos.
Resultado: ✅ Todos los 8 tests pasan correctamente después de corregir el overflow
de clock_ (cambiado de uint16_t a uint32_t).
Fuentes Consultadas
- Pan Docs: LCDC (LCD Control Register)
- Pan Docs: STAT (LCD Status Register)
- Pan Docs: LCD Timing
- Pan Docs: Interrupts (V-Blank, STAT)
- Implementación Python existente:
src/gpu/ppu.py(referencia para lógica de negocio)
Integridad Educativa
Lo que Entiendo Ahora
- Timing preciso es crítico: El motor de timing de la PPU debe ser extremadamente preciso porque los juegos dependen de la sincronización para actualizar gráficos durante V-Blank. Un error de un ciclo puede causar glitches visuales o fallos de sincronización.
- Overflow sutil: El uso de tipos pequeños (
uint16_t) puede causar bugs sutiles cuando se procesan muchos ciclos a la vez. Cambiar auint32_tpara el clock interno resolvió un problema donde LY se reseteaba incorrectamente después de procesar frames completos. - Separación de responsabilidades: La PPU C++ solo gestiona timing y estado. El renderizado de píxeles vendrá en la Fase B. Esta separación permite validar el timing antes de añadir complejidad.
Lo que Falta Confirmar
- Rendimiento real: Aunque el código ahora es nativo, aún no hemos medido el impacto real en FPS cuando la PPU se integre con la CPU nativa. La siguiente fase será conectar PPU y CPU en el bucle principal.
- Compatibilidad con MMU Python: La PPU C++ usa MMU C++ directamente. Cuando integremos con el sistema completo, necesitaremos asegurar que ambas MMUs (Python y C++) estén sincronizadas, o migrar completamente a MMU C++.
Hipótesis y Suposiciones
Se asume que la lógica de timing migrada desde Python es correcta, ya que fue validada extensivamente en la v0.0.1. Los tests confirmaron que el comportamiento es idéntico. La única diferencia es que ahora ejecuta en código nativo, eliminando el overhead de Python pero manteniendo la misma lógica.
Próximos Pasos
- [ ] Fase B: Renderizado de Píxeles - Implementar el motor de renderizado en C++ que genera el framebuffer píxel a píxel. Esto incluirá decodificación de tiles, renderizado de fondo, ventana y sprites.
- [ ] Integración con bucle principal - Conectar PPU nativa con CPU nativa en el bucle de emulación principal para medir el impacto real en rendimiento.
- [ ] Sincronización MMU - Resolver cómo sincronizar MMU Python con MMU C++ cuando ambos componentes necesiten acceso a memoria, o decidir migrar completamente a MMU C++.