Step 0379: Implementación de la Interrupción de Joypad
📋 Contexto
Durante las pruebas del Step 0378, se observó que el emulador mostraba los créditos de los juegos (confirmando que el pipeline PPU funciona), pero el usuario reportó que no podía interactuar con el juego. El juego se quedaba "paralizado" en la pantalla de créditos sin responder a los controles.
La auditoría del código reveló el problema: aunque el sistema de Joypad registraba correctamente las pulsaciones
de botones (actualizando direction_keys_ y action_keys_), nunca solicitaba
la interrupción de Joypad que el juego esperaba. Según la documentación oficial de Pan Docs, cuando
un botón cambia de "suelto" (1) a "presionado" (0) — un "falling edge" — se debe solicitar la interrupción
de Joypad (bit 4, vector 0x0060).
🔧 Concepto de Hardware: Interrupción de Joypad
1. ¿Qué es la Interrupción de Joypad?
La Interrupción de Joypad es una señal que la Game Boy genera automáticamente cuando se detecta un cambio en el estado de los botones. Según Pan Docs - Joypad Input:
"La interrupción de Joypad se solicita cuando un botón cambia de high (1 = suelto) a low (0 = presionado). Esto se conoce como un 'falling edge'."
2. Registro P1 (0xFF00) y Selección de Fila
El registro P1 es una matriz de 2x4 botones que la CPU escanea para leer el estado de los controles:
- Bit 4 = 0: Selecciona la fila de direcciones (Derecha, Izquierda, Arriba, Abajo)
- Bit 5 = 0: Selecciona la fila de acciones (A, B, Select, Start)
- Bits 0-3: Leen el estado de los botones (0 = presionado, 1 = suelto)
3. Condiciones para Solicitar la Interrupción
La interrupción de Joypad debe solicitarse SOLO si se cumplen estas condiciones:
- Un botón cambia de 1 (suelto) a 0 (presionado) — falling edge
- La fila correspondiente está seleccionada (bit 4 o 5 del registro P1 = 0)
- La interrupción de Joypad está habilitada en el registro IE (bit 4 = 1)
4. Vector de Interrupción
Cuando se solicita la interrupción de Joypad, la CPU salta al vector 0x0060:
IF (0xFF0F) bit 4 = 1 → CPU salta a 0x0060 → Juego maneja el input
🔨 Cambios Técnicos
1. Actualización de Joypad.hpp
Agregamos la forward declaration de MMU y el método setMMU():
// Forward declaration de MMU para solicitar interrupciones
class MMU;
class Joypad {
public:
// ... métodos existentes ...
/**
* Establece el puntero a la MMU para poder solicitar interrupciones.
* Step 0379: El Joypad necesita acceso a la MMU para solicitar la interrupción de Joypad
* cuando se presiona un botón (falling edge en P14-P17).
*/
void setMMU(MMU* mmu);
private:
// ... miembros existentes ...
/**
* Puntero a la MMU para solicitar interrupciones.
* Necesario para solicitar la interrupción de Joypad (bit 4, vector 0x0060)
* cuando se detecta un "falling edge" (botón presionado).
*/
MMU* mmu_;
};
2. Actualización de Joypad.cpp
Implementamos la detección de falling edge y la solicitud de interrupción:
void Joypad::press_button(int button_index) {
// ... validación ...
// Guardar estado anterior para detectar "falling edge"
uint8_t old_direction_keys = direction_keys_;
uint8_t old_action_keys = action_keys_;
// Actualizar estado del botón
if (button_index < 4) {
direction_keys_ &= ~(1 << button_index);
} else {
int action_index = button_index - 4;
action_keys_ &= ~(1 << action_index);
}
// Detectar falling edge y verificar si la fila está seleccionada
bool direction_row_selected = (p1_register_ & 0x10) == 0;
bool action_row_selected = (p1_register_ & 0x20) == 0;
bool falling_edge_detected = false;
if (button_index < 4) {
bool old_state = (old_direction_keys & (1 << button_index)) != 0;
bool new_state = (direction_keys_ & (1 << button_index)) != 0;
if (old_state && !new_state && direction_row_selected) {
falling_edge_detected = true;
}
} else {
int action_index = button_index - 4;
bool old_state = (old_action_keys & (1 << action_index)) != 0;
bool new_state = (action_keys_ & (1 << action_index)) != 0;
if (old_state && !new_state && action_row_selected) {
falling_edge_detected = true;
}
}
// Solicitar interrupción si se detectó falling edge
if (falling_edge_detected && mmu_ != nullptr) {
mmu_->request_interrupt(0x10); // Bit 4 = Joypad Interrupt
// Log temporal para diagnóstico
printf("[JOYPAD-INT] Button %d pressed | Interrupt requested (bit 0x10, vector 0x0060)\n",
button_index);
}
}
void Joypad::setMMU(MMU* mmu) {
mmu_ = mmu;
printf("[JOYPAD-INIT] MMU connected to Joypad | Interrupt requests enabled\n");
}
3. Actualización de MMU.cpp
Establecemos la conexión bidireccional entre MMU y Joypad:
void MMU::setJoypad(Joypad* joypad) {
joypad_ = joypad;
// Conexión bidireccional: el Joypad necesita acceso a la MMU
// para solicitar interrupciones cuando se presiona un botón
if (joypad_ != nullptr) {
joypad_->setMMU(this);
}
}
4. Actualización de joypad.pxd
Agregamos el método setMMU() en la interfaz de Cython:
# Forward declaration de MMU
cdef extern from "MMU.hpp":
cdef cppclass MMU:
pass
cdef extern from "Joypad.hpp":
cdef cppclass Joypad:
# ... métodos existentes ...
# Step 0379: Establecer puntero a MMU para solicitar interrupciones
void setMMU(MMU* mmu)
✅ Tests y Verificación
1. Compilación
Comando ejecutado:
python3 setup.py build_ext --inplace
Resultado: ✅ Compilación exitosa sin errores
2. Verificación de Inicialización
Comando ejecutado:
timeout 5 python3 main.py roms/pkmn.gb > test_joypad_step0379.log 2>&1
Resultado:
[JOYPAD-INIT] MMU connected to Joypad | Interrupt requests enabled
✅ El log confirma que el Joypad se conectó correctamente a la MMU
3. Validación de la Arquitectura
- MMU →
setJoypad(Joypad*)→ establecejoypad_ - MMU →
joypad_->setMMU(this)→ establecemmu_en Joypad - Joypad →
press_button()→ detecta falling edge - Joypad →
mmu_->request_interrupt(0x10)→ solicita interrupción - CPU →
check_interrupts()→ detecta IF bit 4 → salta a 0x0060
🔍 Tarea 2: Depuración del Renderizado (Rayas Verticales)
Problema Reportado
El usuario reportó que el emulador mostraba un patrón de rayas horizontales y verticales (checkerboard) en lugar de los gráficos del juego, a pesar de que el emulador corría a 62.5 FPS estables.
Investigación Inicial
Los logs mostraban una discrepancia crítica:
[MMU-VRAM-INITIAL-STATE] VRAM initial state | Non-zero bytes: 5867/6144 (95.49%) ✅
[PPU-VRAM-CHECK] Frame 1 | VRAM non-zero: 0/6144 | Empty: YES ❌
La MMU reportaba que VRAM tenía 5867 bytes, pero la PPU leía 0 bytes. Esto era imposible.
Bug Crítico Encontrado
Al analizar el código de MMU.cpp, encontré un error de cálculo de offset en dos funciones:
check_initial_vram_state()(línea 1941)check_vram_state_at_point()(línea 1985)
El Bug:
// ❌ BUG: Restaba 0x8000, leyendo desde ROM en lugar de VRAM
for (int i = 0; i < 16; i++) {
uint8_t byte = memory_[addr - 0x8000 + i]; // Si addr=0x8000, lee memory_[0] (ROM)
// ...
}
La Corrección:
// ✅ CORRECTO: Lee directamente desde VRAM
for (int i = 0; i < 16; i++) {
uint8_t byte = memory_[addr + i]; // Si addr=0x8000, lee memory_[0x8000] (VRAM)
// ...
}
Impacto del Bug
Este bug causaba que las funciones de diagnóstico leyeran desde ROM (0x0000-0x3FFF) en lugar de VRAM (0x8000-0x97FF):
- La ROM contiene datos del juego (5867 bytes no-cero)
- Las verificaciones reportaban falsamente que "VRAM tiene datos"
- Esto confundía el diagnóstico, haciéndonos creer que había un problema con
vram_is_empty_
Verificación Post-Corrección
Después de corregir el bug y recompilar:
[MMU-VRAM-INITIAL-STATE] VRAM initial state | Non-zero bytes: 0/6144 (0.00%) ✅
[PPU-VRAM-CHECK] Frame 1 | VRAM non-zero: 0/6144 | Empty: YES ✅
✅ Ahora ambas verificaciones coinciden: VRAM está realmente vacía.
Hallazgo Importante
El checkerboard es correcto. VRAM está realmente vacía porque:
- El juego limpia VRAM durante la inicialización (Frame 6)
- El juego muestra los "créditos" y espera input del usuario
- Los logs muestran escrituras de 0x00 en VRAM (limpieza/inicialización)
[TILE-LOAD-EXT] CLEAR | Write 8000=00 (TileID~0) PC:36E3 Frame:6 Init:YES
[TILE-LOAD-EXT] CLEAR | Write 8001=00 (TileID~0) PC:36E3 Frame:6 Init:YES
...
⚠️ Problemas Conocidos y Próximos Pasos
1. CPU en Bucle de Retardo
El log muestra que la CPU está atrapada en un bucle de retardo en PC:0x0038:
[SNIPER-AWAKE] ¡Saliendo del bucle de retardo! Iniciando rastreo de flujo...
[POST-DELAY] PC:0038 OP:FF | A:87 HL:A387 | IE:39 IME:0
Esto sugiere que el juego está esperando una interrupción diferente (posiblemente Timer o V-Blank). Este problema es independiente de la interrupción de Joypad y requiere investigación separada.
2. Prueba Interactiva Pendiente
Próximo paso sugerido: Ejecutar el emulador interactivamente y presionar Enter/Z (simulando Start) para verificar si el juego responde a la interrupción de Joypad y carga los tiles del menú principal.
📊 Conclusión
Se completaron exitosamente dos tareas críticas:
- Interrupción de Joypad: Implementada completamente siguiendo Pan Docs. El Joypad ahora solicita interrupciones cuando se presiona un botón (falling edge).
- Bug de Lectura de VRAM: Corregido bug crítico en funciones de diagnóstico que leían desde ROM en lugar de VRAM, causando logs falsos que confundían el análisis.
Estas correcciones son fundamentales para la jugabilidad del emulador y la precisión del diagnóstico.
Archivos Modificados
Tarea 1: Interrupción de Joypad
src/core/cpp/Joypad.hpp— Forward declaration de MMU, métodosetMMU(), punterommu_src/core/cpp/Joypad.cpp— Detección de falling edge, solicitud de interrupción 0x10src/core/cpp/MMU.cpp(Tarea 1) — ActualizadosetJoypad()para conexión bidireccionalsrc/core/cython/joypad.pxd— MétodosetMMU()en interfaz Cython
Tarea 2: Bug de Lectura de VRAM
src/core/cpp/MMU.cpp(Tarea 2) — Corregidas funcionescheck_initial_vram_state()ycheck_vram_state_at_point()
Commits
1f8490b— feat(joypad): Implementar interrupción de Joypad (Tarea 1)c34c3d9— fix(mmu): Corregir lectura de VRAM en verificaciones (Tarea 2)
Estado de Tareas del Plan 0379
- ✅ Tarea 1: Auditoría del Sistema de Joypad C++ — COMPLETADA
- ✅ Tarea 2: Depuración del Renderizado — COMPLETADA
- ⏳ Tarea 3: Análisis de CPU (Halt/Loop) — PENDIENTE (requiere prueba interactiva)
- ✅ Tarea 4: Documentación — COMPLETADA