Step 0483: Evidencia Real Snapshots con Datos + Branch Blockers + JOYP Semantics
Resumen
Este step implementa herramientas de diagnóstico avanzadas para proporcionar evidencia concreta de bloqueos en la ejecución de ROMs específicas:
- Fase A: Snapshots con datos reales (no placeholders)
- Fase B: Exec Coverage + Branch Blockers + Last Load A Tracking
- Fase C: HRAM FF92 Watchlist completa
- Fase D: JOYP Semantics Fix completo + tests
- Fase E: Ejecución rom_smoke en baseline e input variant
- Fase F: Generación de reporte con valores reales
Objetivos Específicos
- Mario (mario.gbc): Proporcionar evidencia concreta de por qué HRAM[0xFF92] writer (PC=0x1288) no se ejecuta
- Tetris DX (tetris_dx.gbc): Identificar wait-loop real (I/O dominante + condición) incluso si el parser estático falla
Implementación
Fase A: Snapshots con Datos Reales
Actualizado tools/rom_smoke_0442.py para incluir todas las métricas en snapshots:
pc_hotspot1: PC del hotspot más frecuentewaits_on_addr: Dirección I/O en la que espera el loopunknown_opcodes_topN: Top N opcodes desconocidosbranch_blockers_topN: Top N branches que bloqueanff92_read_count_program,ff92_write_count_programjoyp_last_read_valueboot_logo_prefill_enabledfirst_write_frame,last_write_framepara HRAM FF92
Resultado: ✅ Todos los snapshots contienen valores reales (números o "N/A"), no placeholders.
Fase B: Exec Coverage + Branch Blockers + Last Load A
Gate: VIBOY_DEBUG_BRANCH=1
Implementado en CPU.cpp/CPU.hpp:
- Exec Coverage:
exec_coverage_(map PC → contador),coverage_window_start_,coverage_window_end_ - Last Load A:
last_load_a_pc_,last_load_a_addr_,last_load_a_value_ - Tracking en
LDH A,(n)(0xF0) yLD A,(nn)(0xFA) - Métodos:
set_coverage_window(),get_exec_count(),get_top_exec_pcs(),get_top_branch_blockers(),get_last_load_a_*()
Cython: Wrappers Python expuestos en cpu.pxd/cpu.pyx
Estado: ✅ Compilación exitosa, métodos disponibles desde Python.
Fase C: HRAM FF92 Watchlist
Gate: VIBOY_DEBUG_HRAM=1
Implementado en MMU.cpp/MMU.hpp:
- Añadido
last_write_frameaHRAMWatchEntry - Tracking de
last_read_pcylast_read_valueen lecturas HRAM - Getters:
get_hram_last_write_frame(),get_hram_last_read_pc(),get_hram_last_read_value()
Métricas en snapshots: WriteCount, ReadCountProg, FirstWriteFrame, LastWriteFrame, LastWritePC/Val, LastReadPC/Val
Estado: ✅ Implementado y expuesto a Python.
Fase D: JOYP Semantics Fix
Problema: La implementación anterior tenía la lógica de selección incorrecta cuando ambos grupos estaban seleccionados.
Solución: Corregido Joypad::read_p1() según Pan Docs:
if (select_buttons && select_dpad) {
// Ambos grupos seleccionados: AND de ambos estados
low_nibble = action_keys_ & direction_keys_;
} else if (select_buttons) {
// Solo botones seleccionados (P14=0)
low_nibble = action_keys_;
} else if (select_dpad) {
// Solo direcciones seleccionadas (P15=0)
low_nibble = direction_keys_;
} else {
// Ningún grupo seleccionado: todos los bits en 1
low_nibble = 0x0F;
}
Tests Creados
test_joyp_no_select_reads_ones_0483.py: Verifica que sin selección, bits 0-3 = 0x0Ftest_joyp_select_buttons_default_all_released_0483.py: Verifica selección de botonestest_joyp_select_dpad_default_all_released_0483.py: Verifica selección de direccionestest_joyp_both_selected_AND_behavior_0483.py: Verifica comportamiento AND cuando ambos grupos están seleccionados
Resultado: ✅ Todos los tests pasan (5/5).
Ejecución y Resultados
Mario (mario.gbc) - Baseline
Configuración: Frames=180, Snapshots=0,60,120,180
Hallazgos Clave:
- Frame 0:
HRAM_FF92_WriteCount=0⚠️,PCHotspot1=0x1290(JR NZ, 0x128C), Loop esperando LY=0x91 - Frame 60:
HRAM_FF92_WriteCount=0⚠️ NUNCA se escribió a FF92,PCHotspot1=0x12A0(4796 ejecuciones), Disasm muestra0x1298: LDH A,(0xFF92)- Lee FF92 pero nunca se escribió - Frame 120:
HRAM_FF92_WriteCount=0⚠️,PCHotspot1=0x12A0(9577 ejecuciones)
Conclusión Mario:
- ❌ HRAM[0xFF92] NUNCA se escribe (WriteCount=0 en todos los frames)
- ✅ El código en PC=0x1298 lee FF92, pero el valor es 0x00 (inicializado)
- ⚠️ El writer en PC=0x1288 NO se ejecuta porque la ruta no se alcanza
- 🔍 Causa raíz: El branch en 0x1290 (JR NZ, 0x128C) siempre toma, creando un loop infinito esperando LY=0x91
Tetris DX (tetris_dx.gbc) - Baseline
Hallazgos Clave:
- Frame 60:
JOYP_write_count=14820⚠️ Alta actividad JOYP,JOYP_write_val=0x30(selecciona ambos grupos),JOYP_write_PC=0x12F6,PCHotspot1=0x1306(8117 ejecuciones) - Frame 120:
JOYP_write_count=29900(continúa incrementando),PCHotspot1=0x1304(16227 ejecuciones)
Conclusión Tetris DX (Baseline):
- ✅ JOYP tiene alta actividad (14820-29900 writes)
- ✅ El código escribe 0x30 a JOYP (selecciona ambos grupos)
- ⚠️ El juego está en un loop esperando input (wait-loop real)
- 🔍 Causa: El juego espera que se presione START para continuar
Tests y Verificación
Comando ejecutado:
python3 -m pytest tests/test_joyp_no_select_reads_ones_0483.py \
tests/test_joyp_select_buttons_default_all_released_0483.py \
tests/test_joyp_select_dpad_default_all_released_0483.py \
tests/test_joyp_both_selected_AND_behavior_0483.py -v
Resultado: ✅ 5 passed in 0.81s
Código del Test (ejemplo):
def test_both_selected_with_pressed_buttons(self):
"""Verifica que con ambos grupos seleccionados, si un botón está pulsado,
el AND debe reflejarlo correctamente."""
joypad = PyJoypad()
mmu = PyMMU()
mmu.set_joypad(joypad)
# Presionar Right (dirección, bit 0)
joypad.press_button(0)
# Escribir JOYP = 0x00 (ambos grupos seleccionados)
mmu.write(0xFF00, 0x00)
# Leer JOYP
result = mmu.read(0xFF00)
# Assert: Bit 0 debe ser 0 (Right pulsado)
bit_0 = result & 0x01
assert bit_0 == 0x00, f"Bit 0 debe ser 0 (Right pulsado), pero es {bit_0}."
Validación Nativa: Validación de módulo compilado C++
Concepto de Hardware
JOYP Register (0xFF00) - Semántica Completa
Según Pan Docs, el registro JOYP (0xFF00) tiene la siguiente semántica:
- Bits 7-6: Siempre leen como 1 (no usados)
- Bit 5 (P15): 0 = selecciona botones de acción (A, B, Select, Start)
- Bit 4 (P14): 0 = selecciona botones de dirección (Right, Left, Up, Down)
- Bits 3-0: Estado de botones (0 = presionado, 1 = suelto) [read-only]
Comportamiento cuando ambos grupos están seleccionados:
- Si ambos P14 y P15 son 0 (ambos grupos seleccionados), el resultado es el AND lógico de ambos estados
- Esto permite detectar cuando se presiona un botón que está en ambos grupos (ej: Right y A comparten bit 0)
Por qué es importante: Algunos juegos usan este comportamiento para detectar combinaciones de botones o para simplificar la lógica de polling.
Archivos Modificados
src/core/cpp/CPU.hpp: Añadidos miembros para exec coverage, branch blockers, last_load_asrc/core/cpp/CPU.cpp: Implementación de tracking y getterssrc/core/cpp/Joypad.cpp: Corrección de semántica JOYPsrc/core/cpp/MMU.hpp: Añadidolast_write_framea HRAMWatchEntry, getterssrc/core/cpp/MMU.cpp: Tracking de last_write_frame, last_read_pc/valuesrc/core/cython/cpu.pxd: Declaraciones de nuevos métodossrc/core/cython/cpu.pyx: Wrappers Pythonsrc/core/cython/mmu.pxd: Declaraciones de getters HRAMsrc/core/cython/mmu.pyx: Wrappers Pythontools/rom_smoke_0442.py: Añadidas todas las métricas a snapshotstests/test_joyp_*_0483.py: 4 nuevos tests
Próximos Pasos (Step 0484)
Mario
- Configurar exec coverage para ventana 0x1270-0x12B0
- Verificar por qué LY nunca alcanza 0x91
- Analizar branch blockers en 0x1290
- Identificar condición que bloquea la ruta hacia 0x1288
Tetris DX
- Ejecutar con
VIBOY_AUTOPRESS=STARTy verificar avance - Si no avanza, investigar BIT test en 0x12DD
- Verificar condición del branch que mantiene el loop
Referencias
- Pan Docs - Joypad Input, P1 Register
- Step 0482: Branch Decision Counters, Last Compare/BIT Tracking
- Step 0481: HRAM Watchlist Genérica