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

Evidencia Dura - Mario LY==0x91 Count + Tetris DX JOYP Trace Secuencial

Fecha: 2026-01-05 Step ID: 0485 Estado: VERIFIED

Resumen

Step 0484 tenía 3 errores críticos de interpretación. Step 0485 corrige estos errores y cierra con evidencia dura: Mario (count(LY==0x91) explícito en loop + correlación branch) y Tetris DX (JOYP trace secuencial + evidencia START bit bajando o nunca).

Se implementa instrumentación quirúrgica gated por variables de entorno para obtener conteos exactos y trazas secuenciales que permitan cerrar definitivamente las preguntas sobre el comportamiento de estos dos loops bloqueantes.

Corrección de Errores del Step 0484

1. Mario: Confusión de Valores y Salto Lógico Inválido

Error: El loop dice "espera LY = 0x91", eso es 145 decimal (dentro de VBlank, porque LY va 0..153 y 144..153 es VBlank). En el reporte se mezclaban 0x91 con valores como 0x5B (91 decimal). Eso NO es lo mismo.

Error peor: Usar LY_DistributionTop5 para afirmar "no se lee 0x91" es incorrecto. Que no esté en el top 5 solo significa "se lee menos que esos", no "nunca ocurre".

Corrección: Implementación de contador explícito count(LY==0x91) específicamente cuando PC está en el rango del loop (0x128C..0x1290), y correlación con el branch.

2. Mario: Flags Mal Decodificados

Error: En el reporte se interpretaba F=0xC0 como "Z=0, N=1, H=1, C=0". Eso es incorrecto.

Correcto: 0xC0 = 1100 0000 → Z=1 (bit 7), N=1 (bit 6), H=0 (bit 5), C=0 (bit 4). Si Z=1, entonces JR NZ no debería tomar. Así que o el "last_flags" no es el del branch real, o se está capturando en un momento incorrecto, o el tracking está mezclando cosas.

Corrección: Tracking específico del branch en 0x1290 con captura de flags en el momento exacto de la evaluación, y correlación con el valor de LY leído inmediatamente antes.

3. Tetris DX: Lectura JOYP y Selección Mal Entendida

Error: Si escribes 0x30, "no seleccionas nada", y el nibble bajo debe leer 0xF (todo suelto). Y además: un botón "pulsado" se ve como bit = 0, no 1.

Problema: Si el último read muestra select bits = 11 (deseleccionado) y low nibble = 0xF, eso no prueba que el loop no sea de input; prueba que tu snapshot está viendo el read equivocado (o el timing equivocado).

Corrección: Trazado secuencial de writes/reads de JOYP alrededor del hotspot y alrededor del autopress, con ring-buffer de 256 eventos que capture la secuencia real de accesos.

Concepto de Hardware

LY (Line Y) Register (0xFF44)

El registro LY indica la línea de scanline actual que la PPU está renderizando. Va de 0 a 153 (154 valores totales). Los valores 144-153 corresponden al período VBlank (cuando la PPU no está renderizando líneas visibles).

0x91 = 145 decimal, que está dentro del rango VBlank (144-153). Si un loop espera específicamente LY==0x91, está esperando un momento específico dentro de VBlank.

Fuente: Pan Docs - LCD Status Register

JOYP (Joypad) Register (0xFF00)

El registro JOYP (P1) tiene bits de selección (4-5) que determinan qué grupo de botones se lee:

  • Bit 4 (P14): 0 = selecciona botones de dirección (D-Pad), 1 = no selecciona
  • Bit 5 (P15): 0 = selecciona botones de acción (A, B, Select, Start), 1 = no selecciona

Cuando se escribe 0x30 = 0011 0000, ambos bits están en 1, por lo que ningún grupo está seleccionado. En este caso, el low nibble (bits 0-3) debe leer 0xF (todos los bits en 1, todos sueltos).

Cuando un botón está pulsado, el bit correspondiente se lee como 0 (no 1). Esto es importante: un botón pulsado = bit a 0.

Fuente: Pan Docs - Joypad Input

Implementación

Fase 1: Mario Loop LY Watch

Se implementa tracking específico para el loop de Mario (PC 0x128C..0x1290) que cuenta explícitamente cuántas veces se lee LY==0x91 cuando el PC está en ese rango.

  • MarioLoopLYWatch: Estructura que cuenta lecturas LY totales, lecturas con LY==0x91, y guarda el último valor, timestamp y PC.
  • Gated por VIBOY_DEBUG_MARIO_LOOP=1: Solo se activa cuando esta variable de entorno está configurada.
  • Tracking en LDH A,(n): Cuando se lee 0xFF44 (LY) y el PC está en 0x128C..0x1290, se actualiza el contador y se verifica si el valor es 0x91.

Fase 2: Branch 0x1290 Correlation

Se implementa correlación específica del branch en 0x1290 (JR NZ) con el valor de LY leído inmediatamente antes del branch.

  • Branch0x1290Correlation: Estructura que cuenta evaluaciones, taken/not_taken, y guarda LY y flags del momento del not-taken.
  • Tracking en JR NZ: Cuando se ejecuta JR NZ en PC=0x1290, se captura el estado y se correlaciona con el último valor de LY leído en el loop.
  • Mini Trace Ring-Buffer: Buffer de 64 eventos que captura cada evaluación del branch con frame, PC, LY, flags, taken y timestamp.

Fase 3: Exec Coverage para Ventana Mario

Se activa exec coverage para la ventana 0x1270..0x12B0 cuando VIBOY_DEBUG_MARIO_LOOP=1, permitiendo verificar si el código después del "not taken" realmente ejecuta el writer en 0x1288.

Fase 4: JOYP Access Trace

Se implementa un ring-buffer de 256 eventos que captura la secuencia real de writes/reads de JOYP, permitiendo ver la secuencia completa en lugar de solo un snapshot aislado.

  • JOYPTraceEvent: Estructura que captura type (READ/WRITE), PC, valores escritos/leídos, bits de selección, low nibble leído y timestamp.
  • Gated por VIBOY_DEBUG_JOYP_TRACE=1: Solo se activa cuando esta variable está configurada.
  • Contadores por tipo de selección: Se cuentan reads con botones seleccionados, dpad seleccionado, o ninguno seleccionado.

Decisiones de Diseño

  • Estructuras fuera de clases: LoopTraceEvent y JOYPTraceEvent se definen fuera de las clases para compatibilidad con Cython (que necesita ver la definición completa).
  • Gating por variables de entorno: Toda la instrumentación está gated para no afectar el rendimiento cuando no se necesita.
  • Ring-buffers de tamaño fijo: Se usan buffers de tamaño fijo (64 para Mario, 256 para JOYP) para evitar crecimiento ilimitado de memoria.

Archivos Afectados

  • src/core/cpp/CPU.hpp - Estructuras MarioLoopLYWatch, Branch0x1290Correlation, LoopTraceEvent y getters
  • src/core/cpp/CPU.cpp - Tracking en LDH A,(n) y JR NZ, exec coverage para ventana Mario
  • src/core/cpp/MMU.hpp - Estructura JOYPTraceEvent (fuera de clase) y contadores por selección
  • src/core/cpp/MMU.cpp - Tracking de JOYP trace en read() y write()
  • src/core/cython/cpu.pxd - Declaraciones Cython para LoopTraceEvent y getters
  • src/core/cython/cpu.pyx - Implementación Python de getters y conversión de LoopTraceEvent
  • src/core/cython/mmu.pxd - Declaraciones Cython para JOYPTraceEvent y getters
  • src/core/cython/mmu.pyx - Implementación Python de getters y conversión de JOYPTraceEvent
  • tools/rom_smoke_0442.py - Captura de métricas Step 0485 en snapshots
  • tests/test_joyp_press_with_selection_0485.py - Test: JOYP press con selección activa
  • tests/test_joyp_select_bits_consistency_0485.py - Test: Consistencia de select bits (0x30 → 0xF)

Tests y Verificación

Se crearon tests clean-room mínimos para validar la instrumentación:

Test: JOYP Press con Selección

def test_joyp_press_start_with_buttons_selected():
    """Test: Seleccionar botones (P14=0), presionar START, leer y verificar bit 3 = 0"""
    mmu.write(0xFF00, 0x20)  # Seleccionar botones
    joypad.press_button(7)   # START = índice 7
    value = mmu.read(0xFF00)
    assert (value & 0x08) == 0  # Bit 3 debe ser 0 (pulsado)

Resultado: ✅ PASSED - Verifica que el trace captura correctamente los eventos.

Test: Consistencia Select Bits

def test_joyp_0x30_reads_0xF():
    """Test: Si se escribe 0x30, el low nibble debe leer 0xF"""
    mmu.write(0xFF00, 0x30)  # Ningún grupo seleccionado
    value = mmu.read(0xFF00)
    assert (value & 0x0F) == 0x0F  # Low nibble debe ser 0xF

Resultado: ✅ PASSED - Verifica que 0x30 lee correctamente 0xF.

Compilación

Comando: python3 setup.py build_ext --inplace

Resultado: ✅ Compilación exitosa sin errores. Todas las estructuras y getters expuestos correctamente a Python a través de Cython.

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • LY 0x91 = 145 decimal: Está dentro de VBlank (144-153), no es 91 decimal (0x5B).
  • Flags decodificación: 0xC0 = Z=1, N=1, H=0, C=0. Si Z=1, JR NZ no toma.
  • JOYP semántica: 0x30 = ningún grupo seleccionado → low nibble lee 0xF. Botón pulsado = bit a 0.
  • Evidencia dura vs top5: Un valor que no está en top5 no significa "nunca ocurre", solo significa "ocurre menos que esos 5". Necesitamos conteos explícitos.

Lo que Falta Confirmar

  • Mario: ¿Cuántas veces realmente se lee LY==0x91 en el loop? ¿Por qué no progresa si se lee?
  • Tetris DX: ¿Hay algún read de JOYP con selección activa donde START se lee como 0 durante autopress?
  • Branch correlation: ¿El "not taken" realmente abre camino hacia 0x1288, o hay otro bloqueo?

Hipótesis y Suposiciones

Hipótesis Mario: Si LY==0x91 nunca se lee en el loop, entonces el problema es timing de PPU (LY nunca alcanza 0x91 cuando el loop lo lee). Si se lee pero el branch no progresa, entonces el problema es la condición del branch (flags o comparación).

Hipótesis Tetris DX: Si nunca hay un read con selección activa donde START se lee como 0, entonces el problema es que el juego no está buscando START realmente, o la inyección de autopress no está funcionando correctamente.

Próximos Pasos

  • [ ] Ejecutar rom_smoke con mario.gbc y VIBOY_DEBUG_MARIO_LOOP=1 para obtener conteos exactos
  • [ ] Ejecutar rom_smoke con tetris_dx.gbc y VIBOY_DEBUG_JOYP_TRACE=1 para obtener trace secuencial
  • [ ] Analizar reporte y generar conclusión definitiva sobre ambos loops
  • [ ] Step 0486: Aplicar fix mínimo basado en evidencia dura obtenida