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.
Mejora de Actualización de STAT Bit 2 y write_byte_internal
Resumen
Se mejoró la implementación de interrupciones STAT añadiendo un método write_byte_internal() en la MMU
que permite a componentes internos (como la PPU) actualizar registros de hardware sin restricciones. Además, se mejoró
la actualización del bit 2 de STAT (LYC=LY Coincidence Flag) en _check_stat_interrupt() para mantener
consistencia en memoria, asegurando que el bit 2 se actualice correctamente cuando LY coincide con LYC, incluso si algún
código lee directamente de memoria sin pasar por read_byte().
Concepto de Hardware
El registro STAT (0xFF41) tiene una estructura híbrida: algunos bits son de solo lectura (actualizados por el hardware) y otros son configurables por el software. Específicamente:
- 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. Se pone a 1 cuando
LY == LYC. De solo lectura (hardware). - Bits 3-6: Flags de habilitación de interrupciones STAT (H-Blank, V-Blank, OAM Search, LYC=LY). Configurables por software.
- Bit 7: No usado (siempre 0).
En hardware real, cuando el software escribe en STAT, solo los bits configurables (3-6) se guardan. Los bits 0-2 siempre
reflejan el estado actual de la PPU y se actualizan automáticamente por el hardware. Sin embargo, para mantener
consistencia en el emulador, es útil que la PPU actualice estos bits en memoria cuando cambian, incluso aunque
técnicamente se calculen dinámicamente en get_stat().
Problema identificado: Aunque get_stat() calcula el bit 2 dinámicamente cuando se lee
a través de read_byte(), si algún código accede directamente a la memoria interna (por ejemplo, para
evitar recursión), el bit 2 puede no estar actualizado. Esto puede causar inconsistencias si la PPU lee STAT
directamente desde memoria en _check_stat_interrupt().
Fuente: Pan Docs - LCD Status Register (STAT), LYC Register
Implementación
Se implementaron dos mejoras principales:
1. Método write_byte_internal() en MMU
Se añadió un nuevo método write_byte_internal(addr, value) en src/memory/mmu.py que permite
escribir directamente en memoria sin pasar por las restricciones de write_byte(). Este método está
diseñado para uso interno de componentes del sistema (como la PPU) que necesitan actualizar registros de hardware
sin restricciones.
Uso: La PPU usa este método para actualizar el registro STAT (bits 0-2) sin que las restricciones
de write_byte() interfieran. Esto es necesario porque write_byte() para STAT solo guarda
los bits configurables (3-7) y limpia los bits 0-2, pero la PPU necesita actualizar estos bits cuando cambian.
2. Mejora de actualización del bit 2 en _check_stat_interrupt()
Se mejoró el método _check_stat_interrupt() en src/gpu/ppu.py para actualizar el bit 2 de STAT
en memoria cuando LY coincide con LYC (o cuando no coincide). Anteriormente, el bit 2 solo se calculaba dinámicamente
en get_stat(), pero ahora también se actualiza en memoria para mantener consistencia.
Cambios específicos:
- Cuando
LY == LYC: Se actualiza STAT en memoria con el bit 2 activo (0x04) y el modo actual (bits 0-1). - Cuando
LY != LYC: Se actualiza STAT en memoria con el bit 2 limpio y el modo actual (bits 0-1). - Se usa
write_byte_internal()para actualizar sin restricciones, preservando los bits configurables (3-7).
Decisiones de diseño
- Separación de responsabilidades:
write_byte_internal()está claramente marcado como "solo para uso interno" y documentado para evitar uso incorrecto desde código del juego. - Consistencia en memoria: Aunque técnicamente el bit 2 se calcula dinámicamente en
get_stat(), actualizarlo en memoria ayuda a mantener consistencia si algún código lee directamente de memoria (por ejemplo, para evitar recursión). - Preservación de bits configurables: Al actualizar STAT, se preservan los bits configurables (3-7) que el software puede haber escrito, y solo se actualizan los bits de solo lectura (0-2).
Archivos Afectados
src/memory/mmu.py- Añadido métodowrite_byte_internal()para escrituras internas sin restriccionessrc/gpu/ppu.py- Mejorado_check_stat_interrupt()para actualizar el bit 2 de STAT en memoria usandowrite_byte_internal()
Tests y Verificación
Ejecución de Tests: python -m pytest tests/test_ppu_stat.py -v
- Entorno: Windows, Python 3.13.5
- Resultado: ✅ 7 tests PASSED en 0.26s
- Qué valida:
- El bit 2 de STAT se activa correctamente cuando LY == LYC
- Las interrupciones STAT se solicitan cuando LY == LYC y el bit 6 está activo
- La detección de rising edge funciona correctamente (no dispara múltiples veces en la misma línea)
- Las interrupciones STAT por cambio de modo (H-Blank, V-Blank, OAM Search) funcionan correctamente
- Escribir en LYC verifica inmediatamente si LY == LYC y solicita interrupción si corresponde
Código del test (fragmento esencial):
def test_stat_interrupt_lyc_coincidence(self) -> None:
"""Test: Interrupción STAT se solicita cuando LY == LYC y bit 6 está activo."""
mmu = MMU(None)
ppu = PPU(mmu)
mmu.set_ppu(ppu)
# Encender LCD
mmu.write_byte(IO_LCDC, 0x80)
# Configurar LYC = 20
mmu.write_byte(IO_LYC, 20)
# Habilitar interrupción LYC (STAT bit 6 = 1)
mmu.write_byte(IO_STAT, 0x40) # Bit 6 activo
# Limpiar IF inicialmente
mmu.write_byte(IO_IF, 0x00)
# Avanzar PPU hasta LY = 20
ppu.step(20 * 456)
assert ppu.get_ly() == 20
# Verificar que se solicitó interrupción STAT (bit 1 de IF)
if_val = mmu.read_byte(IO_IF)
assert (if_val & 0x02) != 0, "Bit 1 de IF debe estar activo (STAT interrupt)"
Por qué este test demuestra algo del hardware: Este test verifica que cuando LY coincide con LYC y el bit 6 de STAT (LYC Int Enable) está activo, se activa el bit 1 de IF (LCD STAT interrupt). Esto es exactamente el comportamiento del hardware: la PPU compara LY con LYC constantemente, y cuando coinciden y la interrupción está habilitada, se solicita la interrupción STAT. El test también verifica que el bit 2 de STAT se actualiza correctamente (aunque esto se verifica indirectamente porque la interrupción se dispara).
Fuentes Consultadas
- Pan Docs: LCD Status Register (STAT)
- Pan Docs: LYC Register (LY Compare)
- Pan Docs: LCD STAT Interrupt
Integridad Educativa
Lo que Entiendo Ahora
- Registro STAT híbrido: El registro STAT tiene bits de solo lectura (0-2) que reflejan el estado actual de la PPU y bits configurables (3-6) que el software puede escribir. Esta estructura híbrida requiere cuidado al implementar: el software solo puede escribir los bits configurables, pero el hardware actualiza los bits de solo lectura automáticamente.
- Consistencia en memoria: Aunque técnicamente los bits de solo lectura se pueden calcular dinámicamente cuando se leen, mantenerlos actualizados en memoria ayuda a evitar inconsistencias si algún código accede directamente a la memoria (por ejemplo, para evitar recursión).
- Métodos internos: Los componentes del sistema (PPU, Timer, etc.) a veces necesitan actualizar
registros de hardware sin pasar por las restricciones de
write_byte(). Un métodowrite_byte_internal()permite esto de forma controlada, claramente marcado como "solo para uso interno".
Lo que Falta Confirmar
- Timing exacto de actualización del bit 2: ¿Se actualiza el bit 2 de STAT exactamente cuando LY
cambia a coincidir con LYC, o hay un pequeño delay? Por ahora, se actualiza inmediatamente cuando se verifica
en
_check_stat_interrupt(), lo que parece funcionar correctamente según los tests. - Impacto en rendimiento: ¿Actualizar STAT en memoria en cada verificación tiene algún impacto en rendimiento? Por ahora, parece despreciable, pero podría optimizarse si es necesario.
Hipótesis y Suposiciones
Se asume que actualizar el bit 2 de STAT en memoria (además de calcularlo dinámicamente en get_stat())
ayuda a mantener consistencia sin tener impacto negativo en el comportamiento. Esto parece ser correcto según los tests,
pero no está completamente documentado en todas las fuentes consultadas. La implementación es conservadora: mantiene
consistencia en memoria mientras preserva el cálculo dinámico en get_stat().
Próximos Pasos
- [ ] Verificar comportamiento con juegos reales (pkmn.gb, tetris_dx.gbc) para confirmar que las interrupciones STAT se disparan correctamente
- [ ] Analizar logs de ejecución para identificar si hay problemas con el timing de interrupciones STAT
- [ ] Continuar con otras funcionalidades pendientes del emulador (APU, mejoras de renderizado, etc.)