Contexto
El Step 0386 aplicó un workaround deshabilitando STAT IRQ para resolver el problema de IF "pegado" en 0x02.
Tras esa corrección, se reportó que Zelda DX seguía sin ser jugable y crasheaba hacia PC:0xFEE6,
un rango que corresponde a la región OAM/No Usable (0xFE00-0xFEFF).
Ejecutar código en esa región normalmente indica:
- Corrupción del PC: Un return address inválido sacado del stack
- Corrupción del Stack (SP): Push/pop desbalanceados o escrituras fuera de rango
- Salto a dirección calculada incorrectamente: Ej.
JP (HL)conHLcorrupto
Este step implementa instrumentación exhaustiva para capturar el momento exacto del crash y determinar su causa raíz.
Concepto de Hardware
Región 0xFE00-0xFEFF en Game Boy
Según Pan Docs, el mapa de memoria en esta región es:
0xFE00-0xFE9F: OAM (Object Attribute Memory) - 160 bytes para 40 sprites0xFEA0-0xFEFF: "Not Usable" - Región no funcional, devuelve valores aleatorios en hardware real
Ejecutar código en esta región es siempre un error. Indica que el PC fue establecido
a un valor inválido, típicamente por:
Stack y Retornos (RETI/RET)
El stack de la Game Boy crece hacia abajo:
- PUSH:
SP = SP - 2, escribe high byte enSP+1, low byte enSP - POP: Lee low byte de
SP, high byte deSP+1, luegoSP = SP + 2 - RETI (0xD9):
PC = pop_word(); IME = 1;
Si el stack está corrupto o SP apunta a datos no válidos, RETI restaurará un
PC basura (ej. 0xFEE6).
Servicio de Interrupciones
Cuando ocurre una interrupción con IME=1:
- CPU deshabilita
IME(evita interrupciones anidadas) - Guarda
PCactual en el stack:push_word(PC) - Salta al vector (ej.
0x0040para VBlank) - El handler termina con
RETI, que restauraPCdesde el stack
Si el PUSH escribe a direcciones incorrectas o el POP lee de una región
sobrescrita, el retorno será a una dirección inválida.
Fuente: Pan Docs - Memory Map, Interrupts, Stack Operations
Implementación
1. Ring Buffer de Últimas 64 Instrucciones (CPU.hpp/CPU.cpp)
Se añade un ring buffer circular que captura un snapshot de cada instrucción ejecutada:
// En CPU.hpp
struct InstrSnapshot {
uint16_t pc, sp, af, bc, de, hl;
uint8_t bank, op, op1, op2, ime, ie, if_flag;
};
static constexpr int RING_SIZE = 64;
InstrSnapshot ring_buffer_[RING_SIZE];
int ring_idx_;
bool crash_dumped_;
En step(), después del fetch del opcode:
// Capturar snapshot en el ring buffer
ring_buffer_[ring_idx_].pc = original_pc;
ring_buffer_[ring_idx_].sp = regs_->sp;
ring_buffer_[ring_idx_].af = regs_->get_af();
// ... (resto de registros)
ring_idx_ = (ring_idx_ + 1) % RING_SIZE;
// Detectar crash en región FE00-FEFF
if (!crash_dumped_ && original_pc >= 0xFE00 && original_pc <= 0xFEFF) {
crash_dumped_ = true;
printf("[CRASH-PC] ⚠️ PC CORRUPTO: PC=0x%04X (región OAM/no usable)\n", original_pc);
// Dump completo del ring buffer (últimas 64 instrucciones)
for (int i = 0; i < RING_SIZE; i++) {
int idx = (ring_idx_ + i) % RING_SIZE;
printf("[CRASH-RING] #%02d PC:0x%04X Bank:%d OP:%02X %02X %02X | SP:%04X AF:%04X...\n",
i, ring_buffer_[idx].pc, ...);
}
}
2. Trazado de Stack en IRQ Push (CPU.cpp - handle_interrupts)
// ANTES del push_word(prev_pc)
uint16_t sp_before_push = regs_->sp;
printf("[IRQ-PUSH-PC] ANTES: SP=0x%04X PC_to_push=0x%04X\n", sp_before_push, prev_pc);
push_word(prev_pc);
// DESPUES del push
uint16_t sp_after_push = regs_->sp;
uint8_t byte_low = mmu_->read(sp_after_push);
uint8_t byte_high = mmu_->read(sp_after_push + 1);
printf("[IRQ-PUSH-PC] DESPUES: SP=0x%04X Written=[0x%02X,0x%02X] Reconstruct=0x%04X\n",
sp_after_push, byte_low, byte_high,
(static_cast(byte_high) << 8) | byte_low);
// Guardrail: verificar SP en rango peligroso
if (sp_after_push < 0xC000 || sp_after_push >= 0xFE00) {
printf("[STACK-WARN] ⚠️ SP en rango peligroso: 0x%04X\n", sp_after_push);
}
3. Trazado de RETI Pop (CPU.cpp - case 0xD9)
// ANTES del pop_word()
uint16_t sp_before_pop = regs_->sp;
uint8_t byte_low = mmu_->read(sp_before_pop);
uint8_t byte_high = mmu_->read(sp_before_pop + 1);
uint16_t reconstructed = (byte_high << 8) | byte_low;
printf("[RETI-POP-PC] ANTES: SP=0x%04X Bytes=[0x%02X,0x%02X] Reconstruct=0x%04X\n",
sp_before_pop, byte_low, byte_high, reconstructed);
uint16_t return_addr = pop_word();
// DESPUES del pop
printf("[RETI-POP-PC] DESPUES: return_addr=0x%04X SP=0x%04X IME=1\n", return_addr, regs_->sp);
// Guardrail: verificar return_addr corrupto
if (return_addr >= 0xFE00 && return_addr <= 0xFEFF) {
printf("[RETI-POP-PC] ⚠️ RETURN ADDRESS CORRUPTO: 0x%04X (región OAM!)\n", return_addr);
}
4. Instrumentación de Escrituras a FE00-FEFF (MMU.cpp)
// Al inicio de MMU::write()
if (addr >= 0xFE00 && addr <= 0xFEFF && fe_write_count < 60) {
printf("[MMU-FE-WRITE] PC=0x%04X addr=0x%04X value=0x%02X Bank=%d",
debug_current_pc, addr, value, get_current_rom_bank());
if (addr >= 0xFEA0) {
printf(" ⚠️ UNUSABLE REGION\n");
} else {
printf(" (OAM valid)\n");
}
fe_write_count++;
}
Tests y Verificación
Compilación
python3 setup.py build_ext --inplace > build_log_step0387.txt 2>&1
# ✅ Compilación exitosa sin errores
Ejecución de Prueba
timeout 10 python3 main.py roms/zelda-dx.gbc > logs/step0387_fe_pc_probe.log 2>&1
Análisis de Logs (Comandos Seguros)
# 1) Buscar crash en FE00-FEFF
grep -E "\[CRASH-PC\]" logs/step0387_fe_pc_probe.log | head -n 5
# Resultado: ❌ No encontrado (exit code 1)
# 2) Verificar push/pop de IRQ
grep -E "\[(IRQ-PUSH-PC|RETI-POP-PC|STACK-WARN)\]" logs/step0387_fe_pc_probe.log | head -n 60
# Resultado: ❌ No encontrado (las interrupciones no se están procesando)
# 3) Verificar writes a FE00-FEFF
grep -E "\[MMU-FE-WRITE\]" logs/step0387_fe_pc_probe.log | head -n 60
# Resultado: ❌ No encontrado
# 4) CPU Samples (verificar estado general)
grep -E "\[CPU-SAMPLE\]" logs/step0387_fe_pc_probe.log | head -n 20
# Resultado: ✅ CPU ejecutando normalmente (200K+ instrucciones)
Hallazgos Críticos
🔍 Hallazgo Principal: El Crash en 0xFEE6 NO Se Reproduce
Tras ejecutar 10 segundos (≈200K instrucciones), NO se detectó ningún salto a PC en rango 0xFE00-0xFEFF. El crash reportado en Step 0386 NO ocurre en la ejecución actual.
⚠️ Problema Real: Interrupciones Completamente Deshabilitadas
Análisis de los CPU samples revela:
PC: 0x6B95-0x6B9B(Bank 60) - Bucle de polling estrechoIME=0- Interrupciones deshabilitadas globalmenteIE=0x00- NINGUNA interrupción habilitada (ni VBlank, ni STAT, ni Timer...)IF=0x01- VBlank flag activo pero ignorado (no puede atenderse con IE=0x00)- El juego lee
P1 (0xFF00)repetidamente - bucle de polling de joypad
Diagnóstico: El workaround del Step 0386 (deshabilitar STAT IRQ) causó un efecto secundario
donde el juego deshabilita TODAS las interrupciones (IE=0x00), quedando atascado en un wait-loop.
✅ Evidencia de Renderizado Funcional
- Frame 94 alcanzado (más de 1.5 segundos de emulación)
- Framebuffer con píxeles válidos (80/160 no-cero por línea)
- Distribución de colores normal (índices 0 y 3)
Validación Nativa
✅ Módulo compilado C++ con instrumentación completa en:
CPU::step()- Ring buffer de 64 snapshots + detección de crashCPU::handle_interrupts()- Trazado de IRQ push con verificación de SPCPU (case 0xD9)- Trazado de RETI pop con detección de return_addr corruptoMMU::write()- Detección de writes a región FE00-FEFF
La instrumentación funcionó correctamente. No se detectó ningún crash, pero reveló
el problema subyacente: IE=0x00 (interrupciones totalmente deshabilitadas).
Archivos Modificados
src/core/cpp/CPU.hpp- Añadido struct InstrSnapshot y miembros del ring buffersrc/core/cpp/CPU.cpp- Implementación de ring buffer, detección de crash, trazado IRQ/RETIsrc/core/cpp/MMU.cpp- Trazado de writes a FE00-FEFFbuild_log_step0387.txt- Log de compilaciónlogs/step0387_fe_pc_probe.log- Log de ejecución (1.8MB)
Conclusión
El Step 0387 implementó instrumentación exhaustiva para diagnosticar el crash reportado en PC:0xFEE6,
pero el hallazgo principal es que ese crash NO se reproduce en la ejecución actual.
En su lugar, se identificó el problema real: IE=0x00 (interrupciones completamente deshabilitadas),
lo que deja al juego atascado en un bucle de polling sin capacidad de progresar.
Próximos pasos (Step 0388):
- Revisar el workaround del Step 0386 que deshabilita STAT IRQ
- Implementar rising edge detection correcto para STAT sin deshabilitar la interrupción completamente
- Verificar que
IEse inicialice correctamente (debería tener al menos VBlank habilitado) - Mantener la instrumentación del ring buffer como herramienta de diagnóstico permanente