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
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:
LoopTraceEventyJOYPTraceEventse 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 getterssrc/core/cpp/CPU.cpp- Tracking en LDH A,(n) y JR NZ, exec coverage para ventana Mariosrc/core/cpp/MMU.hpp- Estructura JOYPTraceEvent (fuera de clase) y contadores por selecciónsrc/core/cpp/MMU.cpp- Tracking de JOYP trace en read() y write()src/core/cython/cpu.pxd- Declaraciones Cython para LoopTraceEvent y getterssrc/core/cython/cpu.pyx- Implementación Python de getters y conversión de LoopTraceEventsrc/core/cython/mmu.pxd- Declaraciones Cython para JOYPTraceEvent y getterssrc/core/cython/mmu.pyx- Implementación Python de getters y conversión de JOYPTraceEventtools/rom_smoke_0442.py- Captura de métricas Step 0485 en snapshotstests/test_joyp_press_with_selection_0485.py- Test: JOYP press con selección activatests/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
- Pan Docs: LCD Status Register (STAT) - LY Register
- Pan Docs: Joypad Input - P1 Register
- Step 0484: Análisis de errores y correcciones necesarias
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