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

Step 0474: Identificar Bucle de Espera del Hotspot (Con Evidencia)

Fecha: 2026-01-04 Step ID: 0474 Estado: VERIFIED

Resumen

Step 0473 cerró una rama: STOP/KEY1 no pinta nada porque las ROMs ni lo intentan (0 writes a KEY1 y 0 ejecuciones de STOP). Las ROMs CGB están en hotspots de PC leyendo FF0F (IF) y FFFF (IE) de forma obsesiva (millones). Eso huele a bucle de espera. Step 0474 identifica con evidencia el bucle de espera: disasembly del hotspot + instrumentación quirúrgica de IF/LY/STAT.

Resultado: ✅ Disasembly del hotspot #1 obtenido para las 3 ROMs. ✅ Instrumentación IF/LY/STAT implementada y funcionando. ✅ Evidencia real obtenida: Todas las ROMs están en bucles leyendo IF obsesivamente (59K-98K reads), pero IF nunca se limpia (IF_Writes0=0). ❌ Problema identificado: IF no se limpia automáticamente cuando se procesa una interrupción.

Contexto

Step 0473 demostró que el problema NO es STOP/KEY1 (speed-switch no se intenta). Las ROMs están en hotspots de PC leyendo FF0F (IF) y FFFF (IE) obsesivamente. Esto sugiere un bucle de espera esperando que alguna condición cambie.

Objetivo de Step 0474: Identificar con evidencia el bucle de espera mediante:

  • Disasembly del hotspot #1 (16-20 instrucciones)
  • Instrumentación quirúrgica de IF/LY/STAT
  • Decisión automática basada en datos reales

Concepto de Hardware

Interrupt Flag (IF) - 0xFF0F: Registro que indica qué interrupciones están pendientes. Según Pan Docs:

  • Bits 0-4: Flags de interrupción (VBlank, LCD STAT, Timer, Serial, Joypad)
  • Bits 5-7: Siempre leen como 1 (upper bits)
  • Cuando la CPU procesa una interrupción, debe limpiar el bit correspondiente en IF

LY (Line Y) - 0xFF44: Registro que indica la línea de scanline actual (0-153). La PPU actualiza este registro dinámicamente.

STAT (LCD Status) - 0xFF41: Registro que indica el modo actual de la PPU (HBlank, VBlank, OAM Search, Pixel Transfer).

Bucle de Espera: Patrón común en juegos donde el código lee repetidamente un registro I/O esperando que cambie. Si el registro nunca cambia, el juego queda atascado.

Implementación

Fase A: Disasembly del Hotspot

Se implementaron funciones helper para dumpear bytes de ROM y desensamblar instrucciones LR35902 básicas:

  • dump_rom_bytes(mmu, pc, count=32): Dumpea bytes de ROM desde una dirección PC
  • disasm_lr35902(bytes_list, start_pc, max_instructions=20): Desensambla instrucciones LR35902 básicas (clean-room)

Opcodes decodificados: NOP, HALT, DI, EI, RET, LD A, n, LD A, (n), AND n, JR Z/NZ/e, CALL nn, y otros comunes.

Fase B: Instrumentación Quirúrgica IF/LY/STAT

Se añadió instrumentación detallada en MMU.cpp:

  • IF (0xFF0F):
    • Tracking de reads/writes con contadores
    • Histograma (writes 0 vs nonzero)
    • Verificación de upper bits (bits 5-7 deben leer como 1)
    • PC del último write
  • LY (0xFF44):
    • Min/max/last tracking
  • STAT (0xFF41):
    • Last read tracking

Se eliminó el log [IE-DROP] por falsos positivos (detectaba drops en tetris.gb cuando IE nunca se escribió).

Fase C: Tests Clean-Room

Se crearon 3 tests para validar la semántica de IF/LY:

  • test_if_upper_bits_read_as_1_0474.py: Verifica que bits 5-7 de IF siempre leen como 1
  • test_if_clear_0474.py: Verifica que IF puede limpiarse manualmente
  • test_ly_progresses_0474.py: Verifica que LY progresa correctamente

Fase D: Modificación de rom_smoke

Se modificó rom_smoke_0442.py para incluir:

  • Disasembly del hotspot #1 en snapshots
  • Métricas IF/LY/STAT detalladas
  • IO tocado por el bucle

Resultados

tetris.gb (DMG)

Hotspot #1: 0x02B4

Disasm (Frame 0):

0x02B4: CP 0x94
0x02B6: JR NZ, 0x02B2 (-6)
0x02B8: LD A, 0x03
0x02BA: DB 0xE0
0x02BB: DB 0x40
0x02BC: LD A, 0xE4
0x02BE: DB 0xE0
0x02BF: DB 0x47

IF/LY/STAT (Frame 0):

  • IF read count: 59,525
  • IF write count: 2
  • IF read val: 0xE1
  • IF write val: 0xE1
  • IF writes 0: 0
  • IF writes nonzero: 2
  • LY read min: 0, max: 148, last: 0
  • STAT last read: 0x00

Resultado: Loop espera en IF. El juego está en un bucle (0x02B4-0x02B6) leyendo IF obsesivamente (59,525 reads), pero IF nunca se limpia (IF_Writes0=0).

tetris_dx.gbc (CGB)

Hotspot #1: 0x1383 (Frame 0-1), 0x1308 (Frame 2)

Disasm (Frame 0):

0x1383: NOP
0x1384: NOP
0x1385: NOP
0x1386: DB 0x1B
0x1387: DB 0x7A
0x1388: DB 0xB3
0x1389: JR NZ, 0x1383 (-8)

IF/LY/STAT (Frame 0):

  • IF read count: 98,271
  • IF write count: 1
  • IF read val: 0xE1
  • IF write val: 0xE1
  • IF writes 0: 0
  • IF writes nonzero: 1
  • LY read min: 0, max: 144, last: 0
  • STAT last read: 0x00

Resultado: Similar a tetris.gb. Bucle en 0x1383-0x1389 leyendo IF obsesivamente (98,271 reads), pero IF nunca se limpia (IF_Writes0=0).

mario.gbc (CGB)

Hotspot #1: 0x1290 (Frame 0), 0x129D (Frame 1-2)

Disasm (Frame 0):

0x1290: JR NZ, 0x128C (-6)
0x1292: LD A, (0xFF40)
0x1294: AND 0x7F
0x1296: DB 0xE0
0x1297: DB 0x40
0x1298: LD A, (0xFF92)
0x129A: DB 0xE0
0x129B: DB 0xFF

IF/LY/STAT (Frame 0):

  • IF read count: 54,027
  • IF write count: 1
  • IF read val: 0xE1
  • IF write val: 0xE1
  • IF writes 0: 0
  • IF writes nonzero: 1
  • LY read min: 0, max: 145, last: 0
  • STAT last read: 0x00

Resultado: Similar a las otras ROMs. Bucle leyendo IF obsesivamente (54,027 reads), pero IF nunca se limpia (IF_Writes0=0).

Decisión Automática

Caso IF-bug:

  • ✅ Hay writes para limpiar IF pero IF no cambia (IF_Writes0=0 o muy bajo)
  • ✅ Upper bits correctos (IF lee como 0xE1 = 0xE0 | 0x01, bits 5-7 = 1)
  • Problema: IF no se limpia cuando el juego lo espera

Fix mínimo propuesto: El problema no es la semántica de IF (upper bits leen correctamente), sino que IF no se limpia automáticamente cuando se procesa una interrupción. Según Pan Docs, cuando la CPU procesa una interrupción, debe limpiar el bit correspondiente en IF. Si esto no ocurre, el juego queda en un bucle infinito esperando que IF cambie.

Próximo paso: Verificar que cuando la CPU procesa una interrupción (VBlank, STAT, etc.), se limpia el bit correspondiente en IF.

Archivos Afectados

  • src/core/cpp/MMU.hpp: Añadidos miembros privados para instrumentación IF/LY/STAT y getters públicos
  • src/core/cpp/MMU.cpp: Implementada instrumentación quirúrgica en read()/write()
  • src/core/cython/mmu.pxd: Añadidas declaraciones de getters
  • src/core/cython/mmu.pyx: Implementados wrappers Python de getters
  • tools/rom_smoke_0442.py: Añadidas funciones helper de disasembly y métricas IF/LY/STAT en snapshots
  • tests/test_if_upper_bits_read_as_1_0474.py: Test clean-room para verificar bits superiores de IF
  • tests/test_if_clear_0474.py: Test clean-room para verificar limpieza manual de IF
  • tests/test_ly_progresses_0474.py: Test clean-room para verificar progresión de LY

Tests y Verificación

Comando ejecutado:

pytest -q tests/test_if_*_0474.py tests/test_ly_*_0474.py

Resultado: ✅ 6 passed in 0.55s

Código del Test (ejemplo: test_if_upper_bits_read_as_1_0474.py):

def test_if_upper_bits_read_as_1():
    mmu = PyMMU()
    ppu = PyPPU(mmu)
    timer = PyTimer(mmu)
    joypad = PyJoypad()
    
    mmu.set_ppu(ppu)
    mmu.set_timer(timer)
    mmu.set_joypad(joypad)
    
    # Escribir IF = 0x00
    mmu.write(0xFF0F, 0x00)
    
    # Leer IF y verificar bits 5-7 = 1
    if_value = mmu.read(0xFF0F)
    upper_bits = if_value & 0xE0
    assert upper_bits == 0xE0, f"Bits 5-7 deben ser 1"
    assert if_value == 0xE0, f"IF debe ser 0xE0 cuando se escribe 0x00"

Validación Nativa: Validación de módulo compilado C++ mediante tests unitarios y ejecución real de ROMs.

Comando ejecutado (rom_smoke):

PYTHONPATH=. VIBOY_DEBUG_IO=1 python3 tools/rom_smoke_0442.py roms/tetris.gb --frames 60
PYTHONPATH=. VIBOY_DEBUG_IO=1 python3 tools/rom_smoke_0442.py roms/tetris_dx.gbc --frames 60
PYTHONPATH=. VIBOY_DEBUG_IO=1 python3 tools/rom_smoke_0442.py roms/mario.gbc --frames 60

Resultado: ✅ Snapshots obtenidos con disasembly y métricas IF/LY/STAT para las 3 ROMs. ✅ Evidencia real obtenida y reporte estructurado generado.

Próximos Pasos

Dado que el problema identificado es que IF no se limpia automáticamente cuando se procesa una interrupción, el siguiente paso es:

  • Verificar el código de manejo de interrupciones en la CPU
  • Implementar la limpieza automática del bit correspondiente en IF cuando la CPU procesa una interrupción
  • Validar que los bucles de espera se rompen después del fix