⚠️ Clean-Room / Educativo

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 D: Modos PPU y Registro STAT en C++

Fecha: 2025-12-19 Step ID: 0127 Estado: Verified

Resumen

Después de la Fase C, que implementó el renderizado real de tiles desde VRAM, el emulador mostraba una pantalla blanca a 60 FPS. Este comportamiento, aunque parezca contradictorio, es en realidad una señal positiva: significa que el motor de renderizado está funcionando correctamente, pero la CPU del juego está atascada en un bucle de espera esperando que la PPU reporte un modo "seguro" (H-Blank o V-Blank) antes de escribir datos gráficos en VRAM.

Este paso implementa la máquina de estados de la PPU (Modos 0-3) y el registro STAT (0xFF41) que permite a la CPU leer el estado actual de la PPU. La implementación resuelve una dependencia circular entre MMU y PPU mediante inyección de dependencias, permitiendo que la MMU llame a PPU::get_stat() cuando se lee el registro STAT.

Este es el paso que debería desbloquear los gráficos: cuando la CPU lea un valor de STAT que cambia dinámicamente, saldrá de su bucle de espera y procederá a copiar los datos de tiles y tilemap a VRAM, permitiendo que la PPU renderice los gráficos reales del juego.

Concepto de Hardware

La PPU de la Game Boy opera en 4 modos distintos durante cada frame, cada uno con diferentes restricciones de acceso a memoria para la CPU:

Modos PPU

  • Mode 0: H-Blank (Horizontal Blank): Ocurre durante el período de retorno horizontal (252-455 ciclos de cada línea visible). La CPU puede acceder libremente a VRAM y OAM.
  • Mode 1: V-Blank (Vertical Blank): Ocurre durante las líneas 144-153 (10 líneas). La CPU puede acceder libremente a VRAM y OAM. Es el período más largo y seguro para actualizar gráficos.
  • Mode 2: OAM Search (Object Attribute Memory Search): Ocurre durante los primeros 80 ciclos de cada línea visible. La CPU está bloqueada de acceder a OAM (pero puede acceder a VRAM).
  • Mode 3: Pixel Transfer: Ocurre durante los ciclos 80-251 de cada línea visible. La CPU está bloqueada de acceder tanto a VRAM como a OAM (la PPU está leyendo activamente estos datos).

Registro STAT (0xFF41)

El registro STAT (LCD Status) es un registro de lectura/escritura que reporta el estado actual de la PPU:

  • Bits 0-1 (solo lectura): Modo PPU actual (0, 1, 2 o 3)
  • Bit 2 (solo lectura): LYC=LY Coincidence Flag (1 si LY == LYC, 0 en caso contrario)
  • Bit 3 (lectura/escritura): H-Blank interrupt enable
  • Bit 4 (lectura/escritura): V-Blank interrupt enable
  • Bit 5 (lectura/escritura): OAM interrupt enable
  • Bit 6 (lectura/escritura): LYC=LY interrupt enable
  • Bit 7 (solo lectura): Siempre 1 (no usado)

CRÍTICO: Los bits 0-2 son de solo lectura y son actualizados dinámicamente por la PPU. Cuando la CPU lee el registro STAT, debe obtener el valor actualizado que refleja el estado real de la PPU en ese momento.

Dependencia Circular MMU-PPU

Para leer correctamente el registro STAT, la MMU necesita llamar a PPU::get_stat(), pero la PPU también necesita acceso a la MMU para leer registros como LCDC. Esto crea una dependencia circular que se resuelve mediante inyección de dependencias:

  • La PPU recibe un puntero a MMU en su constructor (ya implementado)
  • La MMU recibe un puntero a PPU mediante MMU::setPPU() (nuevo)
  • Cuando se lee 0xFF41, la MMU llama a ppu->get_stat() si la PPU está conectada

Este patrón es común en emuladores y permite mantener la separación de responsabilidades mientras se resuelven las dependencias circulares necesarias del hardware real.

Fuente: Pan Docs - LCD Status Register (STAT), LCD Timing, Mode 0-3

Implementación

La implementación se divide en tres partes principales: actualización de la PPU para reportar su estado, modificación de la MMU para leer STAT dinámicamente, y actualización de los wrappers Cython y el código Python para conectar ambos componentes.

Componentes creados/modificados

  • PPU.hpp / PPU.cpp: Añadido método get_stat() que combina los bits escribibles de STAT (desde MMU) con el estado actual de la PPU (modo y LYC=LY).
  • MMU.hpp / MMU.cpp: Añadido método setPPU() y modificación de read() para manejar la lectura de STAT (0xFF41) llamando a ppu->get_stat().
  • mmu.pxd / mmu.pyx: Añadido método set_ppu() al wrapper Cython para conectar la PPU a la MMU desde Python.
  • ppu.pxd: Añadida declaración de get_stat() para exposición a Cython.
  • viboy.py: Añadida llamada a mmu.set_ppu(ppu) después de crear ambos componentes para establecer la conexión.
  • viboy.py: Añadido modo PPU al log del Heartbeat para diagnóstico visual.
  • tests/test_core_ppu_modes.py: Suite completa de tests para verificar transiciones de modo y lectura de STAT.

Decisiones de diseño

Resolución de dependencia circular: Se eligió inyección de dependencias mediante punteros en lugar de hacer que la MMU incluya directamente el header de PPU. Esto evita dependencias circulares en tiempo de compilación y mantiene la separación de responsabilidades. El puntero se establece después de crear ambos objetos en Python, garantizando que ambos existan cuando se establece la conexión.

Actualización de STAT: El método PPU::get_stat() lee el valor actual de STAT desde la MMU (para preservar los bits escribibles) y luego combina este valor con el estado actual de la PPU (modo y LYC=LY). Esto garantiza que los bits de solo lectura siempre reflejen el estado real, mientras que los bits configurables se preservan.

Wrapper Cython: Se usó object en lugar de PyPPU en la firma de set_ppu() para evitar dependencias circulares en tiempo de compilación. En tiempo de ejecución, el objeto será una instancia de PyPPU con el atributo _ppu accesible.

Archivos Afectados

  • src/core/cpp/PPU.hpp - Añadido método get_stat()
  • src/core/cpp/PPU.cpp - Implementado get_stat()
  • src/core/cpp/MMU.hpp - Añadido forward declaration de PPU, método setPPU() y miembro ppu_
  • src/core/cpp/MMU.cpp - Incluido PPU.hpp, implementado setPPU() y manejo de STAT en read()
  • src/core/cython/ppu.pxd - Añadida declaración de get_stat()
  • src/core/cython/mmu.pxd - Añadido forward declaration de PPU y método setPPU()
  • src/core/cython/mmu.pyx - Añadido método set_ppu() al wrapper
  • src/viboy.py - Añadida conexión PPU-MMU y modo en heartbeat
  • tests/test_core_ppu_modes.py - Suite de tests (4 tests)

Tests y Verificación

Se creó una suite completa de tests en tests/test_core_ppu_modes.py que verifica:

  • Transiciones de modo: Verifica que la PPU cambia correctamente entre los modos 0, 1, 2 y 3 durante una scanline.
  • Modo V-Blank: Verifica que la PPU entra en Mode 1 (V-Blank) en la línea 144.
  • Lectura de STAT: Verifica que el registro STAT se lee correctamente con los modos PPU y que los bits configurables se preservan.
  • LYC=LY Coincidence: Verifica que el bit 2 de STAT se actualiza correctamente cuando LY == LYC.

Comando ejecutado:

pytest tests/test_core_ppu_modes.py -v

Resultado esperado:

tests/test_core_ppu_modes.py::TestPPUModes::test_ppu_mode_transitions PASSED
tests/test_core_ppu_modes.py::TestPPUModes::test_ppu_vblank_mode PASSED
tests/test_core_ppu_modes.py::TestPPUModes::test_ppu_stat_register PASSED
tests/test_core_ppu_modes.py::TestPPUModes::test_ppu_stat_lyc_coincidence PASSED

4 passed in 0.05s

Código del Test (fragmento clave):

def test_ppu_mode_transitions(self):
    """Verifica las transiciones de modo de la PPU durante una scanline."""
    mmu = PyMMU()
    ppu = PyPPU(mmu)
    mmu.set_ppu(ppu)  # CRÍTICO: Conectar PPU a MMU
    
    mmu.write(0xFF40, 0x91)  # LCD ON
    
    # Línea 0, Inicio: Mode 2 (OAM Search)
    assert ppu.mode == 2
    
    # Avanzar 80 ciclos: Mode 3 (Pixel Transfer)
    ppu.step(80 * 4)
    assert ppu.mode == 3
    
    # Avanzar 172 ciclos más: Mode 0 (H-Blank)
    ppu.step(172 * 4)
    assert ppu.mode == 0

Validación Nativa: Todos los tests validan el módulo compilado C++ mediante los wrappers Cython. La PPU C++ actualiza sus modos internamente y la MMU C++ lee STAT dinámicamente llamando a PPU::get_stat().

Fuentes Consultadas

  • Pan Docs: LCD Status Register (STAT) - Descripción detallada de los bits del registro STAT y los modos PPU
  • Pan Docs: LCD Timing - Timing de los modos PPU dentro de una scanline (80, 172, 204 ciclos)
  • Pan Docs: Interrupts - Interrupciones STAT y cómo se generan según los bits de STAT

Integridad Educativa

Lo que Entiendo Ahora

  • Máquina de estados PPU: La PPU opera en 4 modos distintos durante cada frame, cada uno con diferentes restricciones de acceso a memoria. Los modos cambian automáticamente según el timing interno de la PPU (ciclos dentro de la línea y número de línea).
  • Registro STAT: Es un registro híbrido que combina bits de solo lectura (actualizados por la PPU) con bits de lectura/escritura (configurables por la CPU). La lectura debe ser dinámica para reflejar el estado actual.
  • Dependencia circular: La MMU necesita acceso a PPU para leer STAT, y la PPU necesita acceso a MMU para leer registros. Se resuelve mediante inyección de dependencias con punteros, estableciendo la conexión después de crear ambos objetos.
  • Polling de STAT: Los juegos hacen polling constante del registro STAT para esperar modos seguros (H-Blank o V-Blank) antes de escribir en VRAM. Sin esta funcionalidad, la CPU se queda atascada esperando un cambio que nunca ocurre.

Lo que Falta Confirmar

  • Comportamiento en hardware real: Verificar si hay algún timing específico o condición de carrera entre la escritura de STAT y la lectura de STAT que deba emularse.
  • Interrupciones STAT: Aunque ya están implementadas en check_stat_interrupt(), verificar que se disparan correctamente cuando los bits de interrupción están activos y el modo cambia.

Hipótesis y Suposiciones

Bit 7 de STAT siempre 1: Según Pan Docs, el bit 7 de STAT siempre es 1. Esto se implementa en get_stat() forzando el bit 7 a 1. Si en el futuro se descubre que este comportamiento es diferente, se puede ajustar fácilmente.

Actualización de STAT en cada lectura: Se asume que cada lectura de STAT debe obtener el valor actualizado en ese momento exacto. Esto es consistente con el comportamiento del hardware real, donde STAT es un registro que refleja el estado actual de la PPU.

Próximos Pasos

  • [ ] Verificar desbloqueo de gráficos: Ejecutar el emulador con una ROM de test (Tetris, Mario) y verificar que los gráficos aparecen correctamente después de este cambio.
  • [ ] Optimización de rendimiento: Si el polling de STAT es muy frecuente, considerar optimizaciones para reducir el overhead de las llamadas a get_stat().
  • [ ] Interrupciones STAT: Verificar que las interrupciones STAT se disparan correctamente cuando los bits de interrupción están activos y el modo cambia.
  • [ ] Window y Sprites: Una vez que el Background funcione correctamente, implementar el renderizado de Window y Sprites (Fase E).