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 0481: Cerrar Init Loops Reales (HRAM FF92 + Wait-Loop Exacto)
Resumen
Step 0480 identificó que mario.gbc espera HRAM[FF92] pero nunca se escribe, y que tetris_dx.gbc tiene un wait-loop sobre JOYP. Step 0481 implementa un sistema genérico de watchlist HRAM para trackear cualquier dirección HRAM (no solo FF92), añade static scan de ROM para encontrar writers de FF92, mejora la instrumentación de JOYP con métricas completas, y refina el parser de wait-loops para detectar loops reales con jumps y BIT patterns.
Resultado: ✅ Watchlist HRAM genérica implementada. ✅ Static scan de ROM funcionando (encontró writer de FF92 en PC=0x1288). ✅ Instrumentación JOYP mejorada. ✅ Parser de wait-loops refinado. ✅ Tests clean-room (6/7 pasando). ✅ Ejecutado rom_smoke: mario.gbc nunca escribe FF92 (writer existe pero no se ejecuta), tetris_dx.gbc escribe JOYP pero no hay wait-loop detectado (puede ser otro I/O).
Concepto de Hardware
HRAM (High RAM) en Game Boy
HRAM es una región de memoria rápida de 127 bytes ubicada en el rango 0xFF80-0xFFFE. Es distinta de los registros I/O (0xFF00-0xFF7F) y es usada por los juegos para almacenar datos temporales que necesitan acceso rápido. Los juegos pueden usar direcciones HRAM específicas como variables de estado o flags de inicialización.
Fuente: Pan Docs - Memory Map, High RAM (HRAM)
Static Analysis de ROM
El análisis estático de ROM permite buscar patrones de instrucciones sin ejecutar el código. Esto es útil para encontrar dónde el juego podría escribir a una dirección, aunque esa ruta de código no se ejecute durante la inicialización. Los patrones típicos son:
LDH (0x92),A(opcode 0xE0 + offset 0x92) - Escribe A a 0xFF92LD (0xFF92),A(opcode 0xEA + addr low + addr high) - Escribe A a 0xFF92LD C,0x92seguido deLD (FF00+C),A- Escribe A a 0xFF92 vía registro C
Wait-Loops con Jumps
Los wait-loops pueden tener jumps dentro del loop si el código está estructurado con múltiples condiciones. El parser debe permitir un "jump_window" (ventana de saltos) alrededor del hotspot para detectar el loop-back jump, incluso si hay instrucciones intermedias (ej: BIT para verificar bits, múltiples CP, etc.).
Implementación
Fase A: Watchlist HRAM Genérica
Se reemplazó la instrumentación específica de FF92 con un sistema genérico que permite trackear cualquier dirección HRAM:
HRAMWatchEntrystruct: Contiene contadores de writes/reads, PC y valores de primera/última escritura, frame de primera escrituraadd_hram_watch(uint16_t addr): Añade una dirección HRAM a la watchlist- Getters:
get_hram_write_count(),get_hram_last_write_pc(),get_hram_first_write_frame(), etc. - Gate: Solo trackea si
VIBOY_DEBUG_HRAM=1
Fase B: Static Scan de ROM
Se implementó scan_rom_for_hram8_writes(rom_bytes, target_addr) en tools/rom_smoke_0442.py que busca patrones de escritura estáticos:
- Pattern 1:
0xE0, target_addr→LDH (target_addr),A - Pattern 2:
0xEA, target_addr, 0xFF→LD (0xFF92),A - Pattern 3:
LD C,target_addrseguido deLD (FF00+C),Adentro de 20 bytes
El scan genera snippets de disasm alrededor de cada writer encontrado para análisis manual.
Fase C: Instrumentación JOYP Mejorada
Se añadieron métricas completas de JOYP:
- Writes:
joyp_write_count,joyp_last_write_pc,joyp_last_write_value - Reads:
joyp_read_count_program,joyp_last_read_pc,joyp_last_read_value - Filter: Reads durante
irq_poll_activeNO se cuentan enread_count_program
Fase D: Parser de Wait-Loops Refinado
Se mejoró parse_loop_io_pattern():
- Excluye HRAM (0xFF80-0xFFFF) de I/O (solo 0xFF00-0xFF7F)
- Acepta
jump_window(default 16) para permitir jumps dentro del loop - Detecta patrones
BITademás deAND - Mejor distinción entre I/O real y HRAM
Componentes creados/modificados
src/core/cpp/MMU.hpp: AñadidoHRAMWatchEntrystruct,hram_watchlist_vector, miembros JOYP trackingsrc/core/cpp/MMU.cpp: Implementación de watchlist genérica, tracking JOYP mejoradosrc/core/cython/mmu.pxdymmu.pyx: Exposición de nuevos getters a Pythontools/rom_smoke_0442.py:scan_rom_for_hram8_writes(),parse_loop_io_pattern()mejoradotests/test_hram_ff92_tracking_0481.py: Tests de watchlist HRAM (3 tests, todos pasando)tests/test_joyp_metrics_0481.py: Tests de métricas JOYP (4 tests, 3/4 pasando)
Decisiones de diseño
Watchlist genérica vs específica: Se eligió una watchlist genérica porque permite trackear múltiples direcciones HRAM simultáneamente y es más mantenible que instrumentación específica por dirección.
Static scan en Python: El static scan se hace en Python porque es más fácil de mantener y modificar que en C++. Los resultados se integran en el reporte de rom_smoke.
JOYP read filtering: Se filtra irq_poll_active porque las lecturas durante el polling interno de la CPU no son lecturas "del programa", solo lecturas automáticas del sistema.
Archivos Afectados
src/core/cpp/MMU.hpp- Watchlist HRAM genérica, miembros JOYP trackingsrc/core/cpp/MMU.cpp- Implementación watchlist, tracking JOYP mejoradosrc/core/cython/mmu.pxd- Declaraciones Cythonsrc/core/cython/mmu.pyx- Wrappers Python, propiedad debug_current_pctools/rom_smoke_0442.py- Static scan, parser mejoradotests/test_hram_ff92_tracking_0481.py- Tests watchlist HRAM (nuevo)tests/test_joyp_metrics_0481.py- Tests métricas JOYP (nuevo)
Tests y Verificación
Tests unitarios:
test_hram_ff92_tracking_0481.py: 3/3 tests pasandotest_hram_ff92_tracking: Verifica tracking básico de FF92test_hram_watchlist_multiple_addresses: Verifica múltiples direccionestest_hram_watchlist_not_tracked_when_gate_off: Verifica gate VIBOY_DEBUG_HRAM
test_joyp_metrics_0481.py: 3/4 tests pasandotest_joyp_write_tracking: ✅ Tracking de writestest_joyp_read_tracking: ✅ Tracking de readstest_joyp_write_read_sequence: ✅ Secuencia write-readtest_joyp_read_not_counted_during_irq_poll: ⚠️ Fallando (problema con variables estáticas compartidas entre instancias MMU)
ROMs ejecutadas:
- mario.gbc (60 frames):
- Static scan: Encontró 1 writer de FF92 en PC=0x1288 (
LDH (0xFF92),A) - Dynamic tracking:
HRAM_FF92_WriteCount=0(nunca se ejecuta) - Conclusión: El writer existe en ROM pero no se alcanza durante init. El loop que bloquea está en otro lado (probablemente esperando algo que no llega).
- Static scan: Encontró 1 writer de FF92 en PC=0x1288 (
- tetris_dx.gbc (60 frames):
JOYP_write_count: Aumenta (Frame 1: 32, Frame 2: 260)JOYP_write_val=0x30,JOYP_write_PC=0x12E8o0x12F6- Wait-loop:
LoopPattern=NO_LOOP(no detectado por parser) - Conclusión: El juego escribe JOYP pero no hay wait-loop detectado. Puede ser que el wait-loop real esté esperando otro I/O o que el parser no lo detecte por la estructura del código.
- tetris.gb (60 frames):
- Funciona correctamente:
IME=1en Frame 2,VBlankServ=1 - No está bloqueado en wait-loop
- Funciona correctamente:
Validación de módulo compilado C++:
python3 setup.py build_ext --inplace
python3 -c "from viboy_core import PyMMU; m=PyMMU(); print('add_hram_watch' in dir(m))" # True
Fuentes Consultadas
- Pan Docs - Memory Map, High RAM (HRAM)
- Pan Docs - I/O Registers, Joypad Input (P1 Register)
- Game Boy CPU Manual (LR35902) - Instrucciones LDH, LD, BIT
Integridad Educativa
Lo que Entiendo Ahora
- HRAM vs I/O: HRAM (0xFF80-0xFFFE) es memoria RAM, no registros I/O. Los juegos pueden usar direcciones HRAM específicas como variables de estado, pero estas no tienen comportamiento hardware especial (a diferencia de I/O que puede trigger eventos).
- Static vs Dynamic Analysis: El static scan encuentra código que podría ejecutarse, pero el dynamic tracking muestra qué se ejecuta realmente. La discrepancia (writer existe pero no se ejecuta) indica que hay una condición que bloquea esa ruta.
- Wait-Loop Detection: Los wait-loops pueden tener estructuras complejas con jumps intermedios y múltiples condiciones. El parser necesita ser flexible pero preciso para evitar falsos positivos.
Lo que Falta Confirmar
- mario.gbc: ¿Por qué el writer de FF92 en PC=0x1288 no se ejecuta? ¿Qué condición falta para que se alcance esa ruta?
- tetris_dx.gbc: ¿Cuál es el wait-loop real si no es JOYP? ¿Está esperando otro I/O o hay un problema con el parser?
- Test irq_poll: El test de irq_poll está fallando por variables estáticas compartidas. ¿Deberíamos hacerlas miembros de instancia en lugar de estáticas globales?
Hipótesis y Suposiciones
Hipótesis mario.gbc: El writer de FF92 está en una ruta de código que solo se ejecuta después de alguna condición que no se cumple durante init. Puede ser que el juego espere algún evento (VBlank, timer, etc.) antes de llegar a esa ruta. El loop actual (PC=0x129D) probablemente está esperando algo diferente.
Hipótesis tetris_dx.gbc: El juego escribe JOYP pero el wait-loop puede estar esperando que JOYP lea un valor específico, no solo que se escriba. O puede estar esperando otro I/O que no hemos identificado aún.
Próximos Pasos
- [ ] Investigar por qué el writer de FF92 en mario.gbc no se ejecuta: ¿qué condición falta?
- [ ] Mejorar parser de wait-loops para tetris_dx.gbc: ¿hay otro I/O que esté esperando?
- [ ] Considerar hacer JOYP tracking con miembros de instancia en lugar de estáticas globales para evitar problemas en tests
- [ ] Ejecutar rom_smoke con más frames para ver si FF92 se escribe más tarde en mario.gbc