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 Desacoplado de Interrupciones
Resumen
Se desacopló el renderizado de las interrupciones para garantizar que cada frame se dibuje en pantalla cuando la PPU alcanza V-Blank (LY=144), independientemente del estado de IME (Interrupt Master Enable) o si el juego usa polling manual de IF. Se añadió un flag `frame_ready` en la PPU que se activa cuando LY pasa de 143 a 144, y un método `is_frame_ready()` que permite al bucle principal comprobar y renderizar sin depender de las interrupciones. Esto soluciona el problema de pantalla azul/negra cuando el juego tiene IME=False y espera V-Blank mediante polling.
Concepto de Hardware
En la Game Boy real, el renderizado de píxeles y la generación de interrupciones son procesos independientes aunque relacionados. La PPU genera un evento V-Blank cada vez que LY alcanza 144, y este evento:
- Siempre actualiza el registro IF (Interrupt Flag) en 0xFF0F, activando el bit 0.
- Opcionalmente dispara una interrupción automática si IME=True y IE tiene el bit 0 activado.
Los juegos pueden usar dos estrategias para detectar V-Blank:
- Interrupciones automáticas: Activan IME y configuran IE para que la CPU salte automáticamente a la rutina de V-Blank.
- Polling manual: Desactivan IME y leen periódicamente el registro IF para detectar cuando el bit 0 está activo.
En ambos casos, el renderizado debe ocurrir cuando LY=144, independientemente de cómo el juego detecte el evento. Si el emulador solo renderiza cuando se procesa una interrupción, los juegos que usan polling nunca verán actualizaciones en pantalla.
Fuente: Pan Docs - V-Blank Interrupt, Interrupt Flag (IF), Interrupt Master Enable (IME)
Implementación
Se implementó un sistema de flag en la PPU que indica cuando un frame está listo para renderizar, desacoplando completamente el renderizado del sistema de interrupciones.
Componentes creados/modificados
- PPU (`src/gpu/ppu.py`): Se añadió el flag `frame_ready` que se activa cuando LY pasa de 143 a 144, y el método `is_frame_ready()` que devuelve el flag y lo resetea automáticamente.
- Viboy (`src/viboy.py`): Se modificó el bucle principal para usar `ppu.is_frame_ready()` en lugar de la lógica basada en `_prev_vblank`. Se eliminó la variable `_prev_vblank` que ya no es necesaria.
Decisiones de diseño
Flag con reset automático: El método `is_frame_ready()` resetea el flag a `False` después de leerlo. Esto garantiza que cada frame solo se renderiza una vez, evitando renderizados duplicados si el bucle principal llama al método múltiples veces en el mismo ciclo.
Activación en el momento exacto: El flag se activa cuando LY pasa de 143 a 144, que es el momento exacto en que comienza V-Blank. Esto asegura que el renderizado ocurre en el momento correcto del ciclo de la PPU.
Independencia de IME: El flag se activa independientemente del estado de IME o IE. Esto permite que el renderizado funcione tanto para juegos que usan interrupciones como para los que usan polling.
Archivos Afectados
src/gpu/ppu.py- Añadido flag `frame_ready` y método `is_frame_ready()`src/viboy.py- Modificado bucle principal para usar `is_frame_ready()` en lugar de `_prev_vblank`
Tests y Verificación
Esta modificación requiere verificación mediante ejecución de ROMs, ya que afecta el comportamiento visual del emulador. Los tests unitarios existentes de la PPU siguen pasando, pero no cubren el nuevo flag.
Próxima verificación: Ejecutar ROMs de test (p.ej. pkmn.gb, tetris_dx.gbc) y verificar que:
- La pantalla se actualiza correctamente cuando LY alcanza 144, incluso con IME=False.
- No hay renderizados duplicados (cada frame se renderiza exactamente una vez).
- El rendimiento no se degrada por comprobar el flag en cada iteración del bucle.
Nota: La verificación con ROMs se realizará en el siguiente paso de testing.
Fuentes Consultadas
Implementación basada en el análisis del trace de ejecución que mostró que el juego espera V-Blank mediante polling de IF, y en el principio de que el renderizado debe ser independiente del sistema de interrupciones.
Integridad Educativa
Lo que Entiendo Ahora
- Renderizado vs Interrupciones: El renderizado de frames y el sistema de interrupciones son procesos independientes. La PPU genera eventos V-Blank que siempre actualizan IF, pero el renderizado debe ocurrir independientemente de si se procesa una interrupción.
- Polling vs Interrupciones: Los juegos pueden usar dos estrategias para detectar V-Blank: interrupciones automáticas (IME=True) o polling manual (IME=False, leyendo IF). El emulador debe soportar ambas estrategias correctamente.
- Timing del renderizado: El momento exacto para renderizar es cuando LY pasa de 143 a 144, que es cuando comienza V-Blank. Este es el momento en que la PPU ha terminado de dibujar todas las líneas visibles.
Lo que Falta Confirmar
- Rendimiento: Verificar que comprobar `is_frame_ready()` en cada iteración del bucle no introduce overhead significativo.
- Comportamiento con múltiples frames: Confirmar que no hay condiciones de carrera o renderizados perdidos cuando la PPU avanza muy rápido.
- Compatibilidad con todos los juegos: Verificar que esta implementación funciona correctamente con juegos que usan interrupciones y con juegos que usan polling.
Hipótesis y Suposiciones
Hipótesis principal: El problema de pantalla azul/negra se debe a que el renderizado solo ocurría cuando se procesaba una interrupción, y los juegos que usan polling nunca disparaban el renderizado. Con esta modificación, el renderizado debería ocurrir siempre que la PPU alcance V-Blank, independientemente del sistema de interrupciones.
Esta hipótesis se basa en el análisis del trace de ejecución que mostró que el juego está en un bucle de polling esperando que IF tenga un valor específico, y que LY avanza correctamente (12→23 en el trace), lo que indica que la PPU funciona pero el renderizado no se dispara.
Próximos Pasos
- [ ] Ejecutar ROMs de test (pkmn.gb, tetris_dx.gbc) y verificar que la pantalla se actualiza correctamente
- [ ] Verificar que no hay renderizados duplicados o frames perdidos
- [ ] Si la pantalla sigue azul/negra, investigar otros posibles problemas (LCDC, BGP, renderizado de tiles, etc.)
- [ ] Añadir tests unitarios para el flag `frame_ready` si es necesario