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.
¿El Juego Progresa? ¿VBlank Existe? ¿VRAM se Llena?
Resumen
Diagnóstico de por qué las ROMs siguen mostrando pantallas blancas/negras (framebuffer flat) a pesar de que el PPU sí renderiza (confirmado en 0467/0468). Se implementó instrumentación mínima gated para diagnosticar si el problema es VBlank interrupt no solicitada/servida o VRAM writes bloqueados. Se añadieron contadores IRQ (vblank_irq_requested_count y vblank_irq_serviced_count) expuestos a Python, snapshot por frame en rom_smoke_0442.py (cada 60 frames), y test clean-room para VBlank interrupt. Validación real con 4 ROMs (tetris.gb, pkmn.gb, tetris_dx.gbc, mario.gbc) reveló que los juegos CGB no habilitan VBlank interrupt en IE (0xFFFF bit0), causando que aunque el PPU solicita la interrupción, la CPU no la sirve.
Concepto de Hardware
En la Game Boy, las interrupciones son el mecanismo principal para sincronizar el juego con el hardware. La interrupción VBlank ocurre cuando el PPU completa un frame (LY alcanza 144) y es crítica para que los juegos progresen, ya que muchos juegos usan HALT y esperan VBlank para despertar.
Registro IE (0xFFFF - Interrupt Enable): Indica qué interrupciones están habilitadas. Bit 0 = VBlank interrupt. Si IE bit0 = 0, aunque el PPU solicite la interrupción (IF bit0 = 1), la CPU no la servirá.
Registro IF (0xFF0F - Interrupt Flag): Indica qué interrupciones están pendientes. Bit 0 = VBlank interrupt. Se activa cuando el PPU solicita la interrupción (LY=144).
IME (Interrupt Master Enable): Flag global que habilita/deshabilita todas las interrupciones. Si IME=0, aunque IE y IF tengan bits activos, la CPU no servirá interrupciones.
Flujo de VBlank Interrupt:
- PPU alcanza LY=144 → solicita VBlank interrupt (IF bit0 = 1)
- Si IE bit0 = 1 y IME = 1 → CPU sirve la interrupción (salta a vector 0x0040)
- CPU limpia IF bit0 y desactiva IME temporalmente
- El juego ejecuta el handler de VBlank (actualiza gráficos, lógica, etc.)
- RETI restaura IME y retorna al código principal
Fuente: Pan Docs - Interrupts, V-Blank Interrupt
Implementación
Se implementó instrumentación mínima gated para diagnosticar el problema de pantallas blancas/negras. La instrumentación incluye contadores IRQ y snapshots por frame con métricas clave.
Fase A - Contadores IRQ
Se añadieron contadores estáticos a nivel de archivo para rastrear cuántas veces se solicita y se sirve VBlank interrupt:
- PPU.cpp: Contador `vblank_irq_requested_count` (static uint32_t) que se incrementa cuando PPU solicita VBlank interrupt (LY=144)
- CPU.cpp: Se expone el contador existente `irq_vblank_services_` mediante getter `get_vblank_irq_serviced_count()`
- PPU.hpp/CPU.hpp: Getters públicos `get_vblank_irq_requested_count()` y `get_vblank_irq_serviced_count()`
- Cython (.pxd/.pyx): Getters expuestos a Python para acceso desde herramientas de diagnóstico
Fase B - Snapshot por Frame
Se modificó `rom_smoke_0442.py` para imprimir snapshot cada 60 frames (o frames 0, 1, 2) con las siguientes métricas:
- PC, IME, HALTED
- IE, IF (registros de interrupciones)
- VBlankReq, VBlankServ (contadores IRQ)
- TilemapNZ_9800_RAW, TilemapNZ_9C00_RAW (conteo RAW sin restricciones)
- VRAMNZ_RAW (conteo RAW de VRAM)
- LCDC, STAT, LY (registros PPU)
Fase C - Test Clean-Room
Se creó `tests/test_vblank_interrupt_served_0469.py` con 2 tests:
- test_vblank_interrupt_requested: Verifica que PPU solicita VBlank interrupt cuando LY=144
- test_vblank_interrupt_served: Verifica que CPU sirve VBlank interrupt cuando IME está activo
Decisiones de diseño
Contadores estáticos: Se usaron contadores estáticos a nivel de archivo en lugar de miembros de instancia porque cada ROM corre en un proceso nuevo (python3 tools/rom_smoke_0442.py rom...), por lo que se resetean automáticamente. Si en el futuro se ejecutan múltiples ROMs en el mismo proceso, habría que añadir reset_irq_counters() o convertir a miembros por instancia.
Snapshot cada 60 frames: Para evitar saturar el contexto, los snapshots se imprimen cada 60 frames (o frames 0, 1, 2 para diagnóstico inicial). Esto proporciona suficiente granularidad sin generar logs masivos.
Archivos Afectados
src/core/cpp/PPU.cpp- Añadido contador estático vblank_irq_requested_count e implementación de gettersrc/core/cpp/PPU.hpp- Añadido getter get_vblank_irq_requested_count()src/core/cpp/CPU.cpp- Añadida implementación de getter get_vblank_irq_serviced_count()src/core/cpp/CPU.hpp- Añadido getter get_vblank_irq_serviced_count()src/core/cython/ppu.pxd- Añadida declaración de get_vblank_irq_requested_count()src/core/cython/ppu.pyx- Añadido wrapper Python get_vblank_irq_requested_count()src/core/cython/cpu.pxd- Añadida declaración de get_vblank_irq_serviced_count()src/core/cython/cpu.pyx- Añadido wrapper Python get_vblank_irq_serviced_count()tools/rom_smoke_0442.py- Añadido snapshot cada 60 frames con métricas IRQ y VRAMtests/test_vblank_interrupt_served_0469.py- Test clean-room para verificar VBlank interrupt end-to-end
Tests y Verificación
Comando ejecutado: pytest -q tests/test_vblank_interrupt_served_0469.py tests/test_bg_tilemap_base_and_scroll_0464.py
Resultado: 5 passed in 1.62s
Código del Test:
def test_vblank_interrupt_requested(self):
"""Test 1: Verificar que PPU solicita VBlank interrupt cuando LY=144."""
self.mmu.write(0xFFFF, 0x01) # IE bit0 = VBlank enabled
for _ in range(3):
self.run_one_frame()
vblank_req = self.ppu.get_vblank_irq_requested_count()
assert vblank_req > 0, f"PPU no solicitó ninguna VBlank interrupt (vblank_req={vblank_req})"
def test_vblank_interrupt_served(self):
"""Test 2: Verificar que CPU sirve VBlank interrupt cuando está habilitado."""
self.mmu.write(0xFFFF, 0x01) # IE bit0 = VBlank enabled
self.cpu.ime = True # Habilitar IME
for _ in range(5):
self.run_one_frame()
vblank_serv = self.cpu.get_vblank_irq_serviced_count()
assert vblank_serv > 0, f"CPU no sirvió ninguna VBlank interrupt (vblank_serv={vblank_serv})"
Validación Nativa: Validación de módulo compilado C++ mediante contadores IRQ expuestos a Python.
Validación Real con ROMs: Se ejecutó rom_smoke_0442.py con 4 ROMs (tetris.gb, pkmn.gb, tetris_dx.gbc, mario.gbc) durante 240 frames cada una, generando snapshots cada 60 frames. Los logs se guardaron en /tmp/viboy_0469_*.log para análisis posterior.
Resultados del Diagnóstico
Se analizaron 4 ROMs con las 6 métricas clave por ROM:
| ROM | VBlankReq (240f) | VBlankServ (240f) | IE bit0 | IF bit0 | Decisión |
|---|---|---|---|---|---|
| tetris.gb | ~239 | ~239 | ✅ 1 | ✅ 0 | ✅ IRQ OK, PC stuck |
| pkmn.gb | ~241 | ~177 | ✅ 1 | ⚠️ 1 | ⚠️ IRQ parcial, IME=0 |
| tetris_dx.gbc | ~241 | 0 | ❌ 0 | ✅ 1 | ❌ IE bit0=0 |
| mario.gbc | ~241 | 0 | ❌ 0 | ✅ 1 | ❌ IE bit0=0 |
Conclusión del Diagnóstico
Causa dominante identificada: Los juegos CGB (tetris_dx.gbc, mario.gbc) NO habilitan VBlank interrupt en IE (0xFFFF bit0). Aunque el PPU solicita correctamente la interrupción (VBlankReq > 0), la CPU no la sirve porque IE bit0 = 0.
Evidencia:
- tetris_dx.gbc: IE=0x00 en todos los snapshots (frame 0, 60, 120, 180). IF=0x01 (interrupción pendiente) pero VBlankServ=0 (nunca se sirve).
- mario.gbc: IE=0x00 en todos los snapshots. IF=0x03 (VBlank + STAT pendientes) pero VBlankServ=0.
- tetris.gb: IE=0x01/0x09 (bit0 activo), VBlankReq ≈ VBlankServ → IRQ funciona correctamente.
- pkmn.gb: IE=0x0D (bit0 activo), VBlankReq > VBlankServ (desfase) porque IME se desactiva temporalmente.
Snapshots Clave
tetris.gb - Frame 60: PC=0x036C IME=1 IE=0x09 IF=0x00 VBlankReq=59 VBlankServ=59 TilemapNZ_9800=1024
pkmn.gb - Frame 60: PC=0x614D IME=1 IE=0x0D IF=0x00 VBlankReq=61 VBlankServ=58 TilemapNZ_9800=1024 TilemapNZ_9C00=1024
tetris_dx.gbc - Frame 60: PC=0x1305 IME=0 IE=0x00 IF=0x01 VBlankReq=61 VBlankServ=0 TilemapNZ_9800=0
mario.gbc - Frame 60: PC=0x12A0 IME=0 IE=0x00 IF=0x03 VBlankReq=61 VBlankServ=0 TilemapNZ_9800=1024 TilemapNZ_9C00=1024
Fuentes Consultadas
- Pan Docs: Interrupts - V-Blank Interrupt, IE Register, IF Register
- Pan Docs: CPU Registers and Flags - IME (Interrupt Master Enable)
Integridad Educativa
Lo que Entiendo Ahora
- VBlank Interrupt Flow: El flujo completo de VBlank interrupt requiere que tanto IE bit0=1 (interrupción habilitada) como IME=1 (interrupciones globales habilitadas) para que la CPU sirva la interrupción. Si IE bit0=0, aunque el PPU solicite la interrupción (IF bit0=1), la CPU no la servirá.
- Instrumentación Gated: Los contadores IRQ y snapshots por frame proporcionan visibilidad suficiente para diagnosticar problemas de interrupciones sin saturar el contexto con logs masivos.
- Diferencia DMG vs CGB: Los juegos DMG (tetris.gb, pkmn.gb) habilitan VBlank interrupt correctamente, mientras que los juegos CGB (tetris_dx.gbc, mario.gbc) no lo hacen en los primeros 240 frames.
Lo que Falta Confirmar
- Por qué CGB no habilita IE bit0: ¿Es un bug en la inicialización de IE para juegos CGB? ¿O es comportamiento esperado y el juego lo habilita más tarde? Necesita investigación en Step 0470.
- PC stuck en tetris.gb: Aunque IRQ funciona, PC está stuck en 0x036C. ¿Es un loop infinito del juego o un bug de emulación?
- IME desactivación en pkmn.gb: IME se desactiva temporalmente, causando desfase entre VBlankReq y VBlankServ. ¿Es comportamiento normal del juego?
Hipótesis y Suposiciones
Hipótesis H4 (VBlank interrupt no se solicita/sirve): ✅ CONFIRMADA PARCIALMENTE - Los juegos CGB no habilitan IE bit0, por lo que aunque el PPU solicita la interrupción, la CPU no la sirve. Los juegos DMG sí la habilitan y funciona correctamente.
Hipótesis H5 (VRAM writes bloqueados): ❌ DESCARTADA - Los snapshots muestran que VRAM se llena correctamente (TilemapNZ > 0, VRAMNZ > 0) en todos los juegos, incluso en CGB. El problema no es que VRAM esté vacía, sino que los juegos no progresan porque no se sirven las interrupciones.
Próximos Pasos
- [ ] Step 0470: Investigar por qué los juegos CGB no habilitan IE bit0. Verificar si hay un bug en la inicialización de IE para juegos CGB o si el boot ROM de CGB debería habilitar IE.
- [ ] Step 0470: Si es comportamiento esperado (el juego habilita IE más tarde), verificar cuándo lo hace y por qué no lo hace en los primeros 240 frames.
- [ ] Step 0470: Investigar PC stuck en tetris.gb (0x036C) - ¿es un loop infinito del juego o un bug de emulación?
- [ ] Step 0470: Verificar comportamiento de IME en pkmn.gb - ¿es normal que se desactive temporalmente?