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 0484: Cerrar Diagnósticos con Evidencia - Mario LY Loop + Tetris DX JOYP Deselect
Resumen
Este step implementa instrumentación avanzada en CPU y MMU para obtener evidencia irrefutable sobre dos problemas bloqueantes:
- Mario (mario.gbc): Loop esperando LY=0x91 (145 decimal) - ¿por qué no se ejecuta el writer de HRAM[FF92] en PC=0x1288?
- Tetris DX (tetris_dx.gbc): Loop con alta actividad JOYP - ¿por qué autopress START no se refleja?
Se añadieron métricas específicas: histograma de distribución de LY, tracking del branch en PC=0x1290, distribución de writes a JOYP, y captura de bits de selección en reads de JOYP. Los datos recopilados proporcionan evidencia clara sobre las causas raíz de ambos problemas.
Concepto de Hardware
LY Register (Line Y Counter)
El registro LY (0xFF44) es un contador de 8 bits que indica la línea de escaneo actual del PPU. Se incrementa automáticamente durante el renderizado y puede alcanzar valores de 0 a 153 (0x99). Los juegos suelen usar loops de espera que leen LY hasta que alcanza un valor específico (ej: 0x91 = 145) para sincronizar operaciones con el ciclo de renderizado.
Problema en Mario: El juego espera LY=0x91, pero la instrumentación muestra que cuando se lee LY en el loop (PC=0x128C), el valor nunca es 0x91. Esto sugiere un problema de timing: el CPU lee LY en un momento donde el valor ya pasó o aún no llegó a 145.
JOYP Register (Joypad Input)
El registro JOYP (0xFF00) controla la lectura de input del joypad. Los bits 4-5 seleccionan qué grupo de botones leer:
- Bits 4-5 = 00: Seleccionar grupo de botones (A, B, SELECT, START)
- Bits 4-5 = 01: Seleccionar grupo de direcciones (RIGHT, LEFT, UP, DOWN)
- Bits 4-5 = 10: Seleccionar grupo de direcciones (alternativa)
- Bits 4-5 = 11: Deseleccionar ambos grupos (devuelve 0xFF)
Corrección Crítica: Anteriormente se interpretó 0x30 (bits 4-5 = 11) como "seleccionar ambos grupos", pero según Pan Docs, 11 deselecciona ambos grupos. Esto explica por qué el autopress no funciona en Tetris DX: el juego lee JOYP con grupos deseleccionados, por lo que los bits de input no se reflejan.
Branch Instructions (JR Conditional)
Las instrucciones JR (Jump Relative) condicionales evalúan flags del registro F para decidir si saltar o continuar.
El branch en PC=0x1290 es un JR NZ, 0x128C que salta si Z=0 (el resultado de la comparación anterior no es cero).
Tracking este branch específico permite entender por qué el loop continúa o se rompe.
Implementación
A) Instrumentación CPU (LY Distribution y Branch 0x1290)
Archivos Modificados: src/core/cpp/CPU.hpp, src/core/cpp/CPU.cpp
- LY Distribution Histogram:
std::map<uint8_t, uint32_t> ly_read_distribution_- Contador de cuántas veces se leyó cada valor de LY. - Last Load A from LY:
bool last_load_a_from_ly_yuint8_t last_load_a_ly_value_- Tracking de la última carga a A desde LY. - Branch 0x1290 Stats: Estructura con
taken_count,not_taken_count,last_flags,last_taken.
Captura de Datos: La instrumentación se activa en case 0xF0 (LDH A, (n)) y case 0xFA (LD A, (nn))
cuando addr == 0xFF44 (LY register). Corrección crítica: Inicialmente solo estaba en 0xFA, pero Mario usa 0xF0,
por lo que se añadió también en 0xF0.
Branch Tracking: En case 0x20, 0x28, 0x30, 0x38 (JR condicionales), si original_pc == 0x1290,
se actualiza branch_0x1290_stats_ con el resultado del branch y los flags.
B) Instrumentación MMU (LCDC Disable Events y JOYP Tracking)
Archivos Modificados: src/core/cpp/MMU.hpp, src/core/cpp/MMU.cpp, src/core/cpp/Joypad.hpp, src/core/cpp/Joypad.cpp
- JOYP Write Distribution:
std::map<uint8_t, uint32_t> joyp_write_distribution_- Histograma de valores escritos a JOYP. - JOYP Write PCs:
std::map<uint8_t, std::vector<uint16_t>> joyp_write_pcs_by_value_- PCs donde se escribió cada valor. - JOYP Read Select Bits:
uint8_t joyp_last_read_select_bits_yuint8_t joyp_last_read_low_nibble_- Estado del último read.
LCDC Disable Events: Tracking de cuando el bit 7 de LCDC cambia de 1 a 0 (LCD disable).
JOYP Read Tracking: En read(0xFF00), se capturan los bits 4-5 (selección) y bits 0-3 (nibble bajo)
desde el estado interno del joypad usando get_p1_register().
C) Extensión de Snapshots
Archivo Modificado: tools/rom_smoke_0442.py
Nuevas Métricas Añadidas:
LCDC_Current: Valor actual del registro LCDC (0xFF40)LY_DistributionTop5: Top 5 valores de LY más leídos (formato:0xXX:count 0xYY:count ...)LastLoadA_FromLY,LastLoadA_LYValue: Tracking de última carga desde LYBranch0x1290_Taken,Branch0x1290_NotTaken,Branch0x1290_LastFlags,Branch0x1290_LastTakenJOYP_WriteDistTop5: Top 5 valores escritos a JOYPJOYP_ReadSelectBits,JOYP_ReadLowNibble: Estado del último read
D) Cython Wrappers
Archivos Modificados: src/core/cython/cpu.pyx, src/core/cython/cpu.pxd,
src/core/cython/mmu.pyx, src/core/cython/mmu.pxd
Wrappers Python para todos los nuevos getters, permitiendo acceso desde rom_smoke_0442.py.
Archivos Afectados
src/core/cpp/CPU.hpp- Añadidos miembros para LY distribution y branch 0x1290src/core/cpp/CPU.cpp- Implementación de instrumentación LY y branch trackingsrc/core/cpp/MMU.hpp- Añadidos miembros para JOYP tracking y LCDC disable eventssrc/core/cpp/MMU.cpp- Implementación de instrumentación JOYP y LCDCsrc/core/cpp/Joypad.hppyJoypad.cpp- Añadido getterget_p1_register()src/core/cython/cpu.pyxycpu.pxd- Wrappers para nuevos getters de CPUsrc/core/cython/mmu.pyxymmu.pxd- Wrappers para nuevos getters de MMUtools/rom_smoke_0442.py- Extensión de snapshots con nuevas métricas
Tests y Verificación
Compilación: ✅ Extensión Cython compilada exitosamente con python3 setup.py build_ext --inplace
Tests Anti-Regresión: Ejecutados pytest tests/test_core_cpu.py tests/test_core_mmu.py.
Un test pre-existente falla (inicialización de memoria en 0xFF00), no relacionado con estos cambios.
Ejecución rom_smoke:
- Mario (mario.gbc): Ejecutado con
--frames 240, log en/tmp/viboy_0484_mario_fixed.log - Tetris DX (tetris_dx.gbc): Ejecutado con
--frames 240, log en/tmp/viboy_0484_tetris_dx_fixed.log
Validación de Datos: Los snapshots muestran valores reales para todas las nuevas métricas:
- Mario Frame 180:
LY_DistributionTop5=0x63:3477 0x5A:3477 0x5B:3477 0x5C:3477 0x5D:3477 - Mario Frame 180:
Branch0x1290_Taken=260472 Branch0x1290_NotTaken=61 - Tetris DX Frame 180:
JOYP_WriteDistTop5=0x30:17810 0x20:17617 0x00:137 0x10:56 - Tetris DX Frame 180:
JOYP_ReadSelectBits=0x03 JOYP_ReadLowNibble=0x0F
Análisis de Resultados
Mario (mario.gbc) - Hallazgos
Problema Identificado: LY NO alcanza 0x91 (145 decimal) cuando se lee en el loop de espera (PC=0x128C-0x1290).
Evidencia:
LY_DistributionTop5=0x63:3477 0x5A:3477 0x5B:3477 0x5C:3477 0x5D:3477- Los valores más leídos son 99, 90, 91, 92, 93. 0x91 (145) NO aparece en el top 5.LY_ReadMax=145- LY sí alcanza 145 en algún momento, pero no cuando se lee en el loop.Branch0x1290_Taken=260472 Branch0x1290_NotTaken=61- El branch toma 99.98% de las veces, confirmando que el loop está activo.
Conclusión: El problema es de timing de PPU. El loop lee LY demasiado rápido o en un momento del ciclo donde LY no está en 145. El valor 0x91 se alcanza, pero no cuando el CPU lo lee en PC=0x128C.
Tetris DX (tetris_dx.gbc) - Hallazgos
Problema Identificado: El juego lee JOYP con ambos grupos deseleccionados (bits 4-5 = 11).
Evidencia:
JOYP_WriteDistTop5=0x30:17810 0x20:17617 ...- 0x30 (bits 4-5 = 11 = deselect) es el valor dominante (50.0%).JOYP_ReadSelectBits=0x03- bits 4-5 = 11, confirmando que el juego lee con grupos deseleccionados.JOYP_ReadLowNibble=0x0F- todos los bits en 1 (todos sueltos), correcto para deselect.
Corrección de Interpretación: Anteriormente se interpretó 0x30 como "seleccionar ambos grupos", pero según Pan Docs, 0x30 (bits 4-5 = 11) deselecciona ambos grupos.
Conclusión: El autopress no funciona porque el juego está leyendo JOYP con ningún grupo seleccionado. Cuando ambos grupos están deseleccionados, el registro devuelve 0xFF (todos los bits en 1), independientemente del estado de los botones. El loop no parece ser un wait-loop de input, sino posiblemente de sincronización o espera de otro evento.
Fuentes Consultadas
- Pan Docs: Game Boy Pan Docs - Especificación de registros LY (0xFF44) y JOYP (0xFF00)
- Pan Docs - Joypad Input: Semántica de bits 4-5 para selección de grupos de botones
Integridad Educativa
Lo que Entiendo Ahora
- LY Timing: El registro LY se incrementa durante el renderizado, pero el momento exacto de lectura por el CPU es crítico. Si el CPU lee demasiado rápido o en el momento equivocado, puede no ver el valor esperado.
- JOYP Semantics: Los bits 4-5 de JOYP controlan la selección de grupos. 11 (0x30) deselecciona ambos grupos, no los selecciona. Esto es crítico para entender por qué el autopress no funciona.
- Branch Tracking: Tracking específico de branches permite entender exactamente por qué un loop continúa o se rompe, proporcionando evidencia concreta de comportamiento.
Lo que Falta Confirmar
- Mario: Verificar el timing exacto de lectura de LY en relación con el ciclo de PPU. ¿En qué momento del ciclo debe leerse LY para ver 0x91?
- Tetris DX: Clarificar la naturaleza real del loop. ¿Qué evento está esperando realmente? ¿Es realmente un wait-loop de input o algo más?
Hipótesis y Suposiciones
Mario: Hipótesis de que el problema es de timing de PPU. El loop lee LY en un momento donde el valor ya pasó o aún no llegó a 145. Esto requiere ajustar el timing de lectura de LY o la sincronización entre CPU y PPU.
Tetris DX: Hipótesis de que el loop no es un wait-loop de input, sino de sincronización o espera de otro evento. El autopress debe aplicarse antes de que el juego deseleccione los grupos, o el loop no es de input.
Próximos Pasos
- [ ] Mario: Investigar timing de PPU - verificar relación entre momento de lectura (PC=0x128C) y ciclo de PPU
- [ ] Mario: Implementar tracking de ventana de ejecución alrededor de PC=0x128C-0x1290 para capturar valor exacto de LY en cada iteración
- [ ] Tetris DX: Clarificar naturaleza del loop - investigar qué evento está esperando realmente
- [ ] Tetris DX: Capturar secuencia completa de writes/reads de JOYP alrededor del hotspot para identificar momento exacto donde el juego espera input