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.
Interrupciones STAT y Registro LYC
Resumen
El emulador mostraba el logo de "GAME FREAK" en Pokémon Red pero se quedaba congelado ahí. El diagnóstico clásico en emulación de Game Boy es que el juego espera una Interrupción STAT (específicamente LY=LYC) para animar la intro, pero el emulador no la generaba. Se implementó completamente la lógica de interrupciones STAT y el registro LYC (LY Compare), incluyendo la comparación LY==LYC, actualización del bit 2 de STAT, y solicitud de interrupción cuando se cumplen las condiciones configuradas por el juego.
Concepto de Hardware
Los juegos de Pokémon (y muchos otros) usan un truco avanzado de vídeo para hacer efectos especiales como la estrella fugaz que pasa volando en la intro. Para lograr esto, configuran el registro LYC (0xFF45) con un número de línea específico y piden a la PPU que lance una Interrupción STAT cuando la línea de dibujo actual (LY) coincida con LYC.
Registro LYC (0xFF45): El juego escribe aquí un número de línea (0-153). La PPU debe comparar LY con LYC constantemente durante el renderizado.
Registro STAT (0xFF41) - Estructura completa:
- Bits 0-1: Modo PPU actual (00=H-Blank, 01=V-Blank, 10=OAM Search, 11=Pixel Transfer). De solo lectura.
- Bit 2: LYC=LY Coincidence Flag. La PPU pone este bit a 1 si
LY == LYC. De solo lectura (hardware). - Bit 3: Mode 0 (H-Blank) Interrupt Enable. Si está activo, genera interrupción cuando la PPU entra en H-Blank.
- Bit 4: Mode 1 (V-Blank) Interrupt Enable. Si está activo, genera interrupción cuando la PPU entra en V-Blank.
- Bit 5: Mode 2 (OAM Search) Interrupt Enable. Si está activo, genera interrupción cuando la PPU entra en OAM Search.
- Bit 6: LYC=LY Coincidence Interrupt Enable. Si está activo y
LY == LYC, genera interrupción STAT (vector0x0048). - Bit 7: No usado (siempre 0).
Comportamiento de Interrupciones STAT:
- Las interrupciones STAT se disparan en "rising edge": solo cuando la condición pasa de False a True, no mientras permanece True. Esto evita disparar múltiples interrupciones en la misma línea.
- Cuando se cumple una condición (LY==LYC o cambio de modo con bit habilitado), se activa el bit 1 del registro IF (0xFF0F), que corresponde a la interrupción LCD STAT.
- El vector de interrupción es
0x0048(segundo vector, después de V-Blank que es0x0040).
Problema identificado: Si el emulador no implementa esta interrupción específica, el juego se queda esperando eternamente a que la línea coincida para mover el siguiente sprite. Por eso Pokémon Red se quedaba congelado en el logo de "GAME FREAK": el juego había configurado LYC y esperaba la interrupción para animar la estrella fugaz.
Fuente: Pan Docs - LCD Status Register (STAT), LYC Register, STAT Interrupt
Implementación
Se implementó completamente la lógica de interrupciones STAT y el registro LYC en tres componentes principales:
1. src/gpu/ppu.py - PPU (Pixel Processing Unit)
- Nuevos atributos en
__init__():self.lyc: int = 0: Registro LYC que almacena el valor de línea con el que se compara LY.self.stat_interrupt_line: bool = False: Flag para evitar disparar múltiples interrupciones STAT en la misma línea (rising edge detection).
- Método
_check_stat_interrupt()(nuevo):- Verifica si
LY == LYCy actualiza el bit 2 de STAT dinámicamente. - Verifica las condiciones de interrupción según los bits 3-6 de STAT configurados por el software.
- Solicita interrupción STAT (bit 1 de IF) solo en rising edge (cuando la condición pasa de False a True).
- Verifica si
- Modificación de
step():- Guarda
old_lyyold_modepara detectar cambios. - Reinicia
stat_interrupt_line = Falsecuando LY cambia (permite nueva interrupción en la nueva línea). - Llama a
_check_stat_interrupt()cuando LY cambia o el modo cambia.
- Guarda
- Modificación de
_update_mode():- Guarda
old_modepara detectar cambios de modo. - Llama a
_check_stat_interrupt()cuando el modo cambia.
- Guarda
- Actualización de
get_stat():- Ahora incluye correctamente el bit 2 (LYC=LY Coincidence Flag) calculado dinámicamente.
- Si
LY == LYC, set bit 2; si no, clear bit 2.
- Nuevos métodos
get_lyc()yset_lyc():get_lyc(): Devuelve el valor actual de LYC (usado por MMU al leer 0xFF45).set_lyc(value): Establece LYC y verifica interrupciones STAT inmediatamente si LYC cambió.
2. src/memory/mmu.py - MMU (Memory Management Unit)
- Lectura de LYC (0xFF45):
- Intercepta lectura de
IO_LYCy redirige appu.get_lyc(). - Si no hay PPU conectada, devuelve 0 (comportamiento por defecto).
- Intercepta lectura de
- Escritura de LYC (0xFF45):
- Intercepta escritura en
IO_LYCy redirige appu.set_lyc(value). - También guarda en memoria para consistencia (aunque la PPU es la fuente de verdad).
- Intercepta escritura en
- Actualización de escritura de STAT (0xFF41):
- Ahora solo guarda bits 3-7 (limpiar bits 0-2) porque los bits 0-2 son de solo lectura.
- Cambio de
value & 0xFCavalue & 0xF8para reflejar que el bit 2 también es de solo lectura.
Decisiones de diseño
- Rising Edge Detection: Se usa el flag
stat_interrupt_linepara implementar detección de rising edge. Solo se dispara la interrupción cuando la condición pasa de False a True, no mientras permanece True. Esto evita múltiples interrupciones en la misma línea y es el comportamiento real del hardware. - Verificación inmediata al cambiar LYC: Cuando el juego escribe en LYC, se verifica inmediatamente si LY == nuevo LYC para actualizar el bit 2 de STAT y solicitar interrupción si corresponde. Esto es crítico porque el juego puede cambiar LYC en cualquier momento.
- Verificación en cambios de LY y modo: Se verifica interrupciones STAT tanto cuando LY cambia (nueva línea) como cuando el modo cambia (dentro de la misma línea). Esto cubre todos los casos posibles de interrupciones STAT.
- Acceso directo a
_memoryenget_stat(): Para evitar recursión infinita (STAT se lee desde MMU, que llama a PPU.get_stat(), que no debe llamar a MMU.read_byte(STAT) de nuevo), se accede directamente aself.mmu._memory[0xFF41]. Esto es un detalle de implementación necesario pero documentado.
Archivos Afectados
src/gpu/ppu.py- Implementación completa de interrupciones STAT y registro LYC:- Añadidos atributos
lycystat_interrupt_line. - Nuevo método
_check_stat_interrupt(). - Modificados
step(),_update_mode()yget_stat(). - Nuevos métodos
get_lyc()yset_lyc().
- Añadidos atributos
src/memory/mmu.py- Integración de LYC en MMU:- Añadida interceptación de lectura/escritura de
IO_LYC (0xFF45). - Actualizada escritura de STAT para limpiar bits 0-2 (no solo 0-1).
- Añadida interceptación de lectura/escritura de
Tests y Verificación
La implementación se validó ejecutando el juego Pokémon Red (ROM aportada por el usuario, no distribuida):
- ROM: Pokémon Red (ROM aportada por el usuario, no distribuida)
- Modo de ejecución: UI con renderizado completo, logging desactivado para rendimiento
- Criterio de éxito: El juego debe pasar del logo de "GAME FREAK" y mostrar la animación de la estrella fugaz, luego avanzar a la intro de la pelea (Gengar vs Nidorino/Jigglypuff) y finalmente llegar al menú "Press Start".
- Observación:
- Antes de la implementación: El juego se quedaba congelado en el logo de "GAME FREAK".
- Después de la implementación: El juego avanza correctamente, mostrando la estrella fugaz animada, la intro de la pelea y llegando al menú principal.
- El emulador ahora genera interrupciones STAT cuando el juego las espera, permitiendo que la animación y el flujo del juego funcionen correctamente.
- Resultado: Verified - El juego funciona correctamente y avanza más allá del logo de "GAME FREAK".
- Notas legales: La ROM de Pokémon Red es propiedad de Nintendo/Game Freak. Se usa únicamente para pruebas locales del autor. No se distribuye ni se enlaza en este proyecto.
Validación técnica:
- El registro LYC se lee y escribe correctamente desde el juego (verificado indirectamente por el comportamiento del juego).
- El bit 2 de STAT se actualiza dinámicamente cuando LY == LYC (verificado porque el juego avanza correctamente).
- Las interrupciones STAT se solicitan correctamente cuando se cumplen las condiciones (verificado porque el juego no se queda congelado).
- La detección de rising edge funciona correctamente (no hay múltiples interrupciones en la misma línea).
Fuentes Consultadas
- Pan Docs: LCD Status Register (STAT)
- Pan Docs: LYC (LY Compare) Register
- Pan Docs: LCD STAT Interrupt
Integridad Educativa
Lo que Entiendo Ahora
- Interrupciones STAT: Son interrupciones generadas por la PPU cuando se cumplen condiciones específicas relacionadas con el estado del LCD (modo PPU o coincidencia LY==LYC). Son diferentes de la interrupción V-Blank y permiten a los juegos sincronizarse con eventos específicos del renderizado.
- Rising Edge Detection: Las interrupciones STAT se disparan solo cuando la condición pasa de False a True, no mientras permanece True. Esto evita múltiples interrupciones en la misma línea y es crítico para el comportamiento correcto del hardware.
- Registro LYC: Permite a los juegos configurar un valor de línea específico y recibir una interrupción cuando LY coincide con ese valor. Esto es esencial para efectos especiales como animaciones sincronizadas con líneas específicas de la pantalla.
- Bit 2 de STAT: Es un flag de solo lectura que se actualiza dinámicamente por el hardware cuando LY == LYC. No puede ser escrito por el software, solo leído para hacer polling manual.
Lo que Falta Confirmar
- Timing exacto de la interrupción: ¿Se dispara la interrupción STAT exactamente cuando LY cambia a coincidir con LYC, o hay un pequeño delay? Por ahora, se verifica inmediatamente cuando LY cambia, lo que parece funcionar correctamente.
- Comportamiento cuando LYC > 153: ¿Qué pasa si el juego escribe un valor > 153 en LYC? Por ahora, se enmascara a 8 bits (0-255), pero LY nunca excede 153. Esto debería funcionar correctamente, pero no está completamente verificado.
- Interrupciones STAT durante V-Blank: ¿Se pueden generar interrupciones STAT por cambio de modo durante V-Blank? Por ahora, se verifican todas las condiciones en todos los modos, lo que debería ser correcto.
Hipótesis y Suposiciones
Se asume que la verificación de interrupciones STAT debe hacerse tanto cuando LY cambia como cuando el modo cambia, cubriendo todos los casos posibles. Esto parece ser correcto según el comportamiento observado en el juego, pero no está completamente documentado en todas las fuentes consultadas.
Próximos Pasos
- [ ] Verificar comportamiento con otros juegos que usan interrupciones STAT (p.ej. efectos de parallax scrolling)
- [ ] Crear tests unitarios para interrupciones STAT (verificar que se solicitan correctamente cuando se cumplen las condiciones)
- [ ] Documentar timing exacto de interrupciones STAT si se encuentra información más detallada
- [ ] Continuar con otras funcionalidades pendientes del emulador (APU, mejoras de renderizado, etc.)