⚠️ 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.

Mejora de Actualización de STAT Bit 2 y write_byte_internal

Fecha: 2025-12-18 Step ID: 0074 Estado: Verified

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étodo write_byte_internal() para escrituras internas sin restricciones
  • src/gpu/ppu.py - Mejorado _check_stat_interrupt() para actualizar el bit 2 de STAT en memoria usando write_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

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étodo write_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.)