Viboy Color - Bitácora de Desarrollo

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:

  1. Fase A: Snapshots con datos reales (no placeholders)
  2. Fase B: Exec Coverage + Branch Blockers + Last Load A Tracking
  3. Fase C: HRAM FF92 Watchlist completa
  4. Fase D: JOYP Semantics Fix completo + tests
  5. Fase E: Ejecución rom_smoke en baseline e input variant
  6. 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 frecuente
  • waits_on_addr: Dirección I/O en la que espera el loop
  • unknown_opcodes_topN: Top N opcodes desconocidos
  • branch_blockers_topN: Top N branches que bloquean
  • ff92_read_count_program, ff92_write_count_program
  • joyp_last_read_value
  • boot_logo_prefill_enabled
  • first_write_frame, last_write_frame para 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) y LD 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_frame a HRAMWatchEntry
  • Tracking de last_read_pc y last_read_value en 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 = 0x0F
  • test_joyp_select_buttons_default_all_released_0483.py: Verifica selección de botones
  • test_joyp_select_dpad_default_all_released_0483.py: Verifica selección de direcciones
  • test_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 muestra 0x1298: 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_a
  • src/core/cpp/CPU.cpp: Implementación de tracking y getters
  • src/core/cpp/Joypad.cpp: Corrección de semántica JOYP
  • src/core/cpp/MMU.hpp: Añadido last_write_frame a HRAMWatchEntry, getters
  • src/core/cpp/MMU.cpp: Tracking de last_write_frame, last_read_pc/value
  • src/core/cython/cpu.pxd: Declaraciones de nuevos métodos
  • src/core/cython/cpu.pyx: Wrappers Python
  • src/core/cython/mmu.pxd: Declaraciones de getters HRAM
  • src/core/cython/mmu.pyx: Wrappers Python
  • tools/rom_smoke_0442.py: Añadidas todas las métricas a snapshots
  • tests/test_joyp_*_0483.py: 4 nuevos tests

Próximos Pasos (Step 0484)

Mario

  1. Configurar exec coverage para ventana 0x1270-0x12B0
  2. Verificar por qué LY nunca alcanza 0x91
  3. Analizar branch blockers en 0x1290
  4. Identificar condición que bloquea la ruta hacia 0x1288

Tetris DX

  1. Ejecutar con VIBOY_AUTOPRESS=START y verificar avance
  2. Si no avanza, investigar BIT test en 0x12DD
  3. 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