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.
Estabilización del Motor y Auditoría de HRAM
Resumen
Refactorización crítica del núcleo de emulación para eliminar variables estáticas que causaban interferencias entre tests de pytest, corrección del bug de timing en run_scanline() que truncaba el valor -1 (HALT), optimización del log del handler de V-Blank para filtrar bucles de retardo en HRAM, e implementación de monitor de escrituras en HRAM para entender las rutinas shadow que los juegos copian ahí.
Concepto de Hardware
HRAM (High RAM) y Shadow Routines: HRAM es un área de 127 bytes (0xFF80-0xFFFE) en la Game Boy que es accesible en todos los ciclos de memoria, a diferencia de otras áreas que pueden estar bloqueadas durante operaciones de DMA o acceso a VRAM. Los juegos suelen copiar rutinas críticas (como handlers de interrupciones o bucles de retardo) a HRAM para ejecutarlas más rápido. Estas rutinas "shadow" (sombra) son copias de código que se ejecutan desde HRAM en lugar de desde ROM o RAM normal.
HALT y Ciclos de CPU: Cuando la CPU entra en estado HALT, deja de ejecutar instrucciones pero el reloj del sistema sigue funcionando. La CPU se despierta cuando hay una interrupción pendiente. En nuestro emulador, step() devuelve -1 cuando la CPU está en HALT para indicar "avance rápido", pero el tipo uint8_t no puede representar -1, causando un truncamiento que rompía el cálculo de ciclos.
Aislamiento de Estado entre Tests: Las variables static en C++ persisten entre llamadas a funciones, lo que significa que el estado de un test puede "contaminar" el siguiente. Esto es especialmente problemático en pytest, donde múltiples tests se ejecutan en la misma sesión. Al mover estas variables a miembros de clase, cada instancia de CPU tiene su propio estado aislado.
Fuente: Pan Docs - "HRAM (High RAM)", "CPU Instruction Set - HALT"
Implementación
Se realizaron cuatro cambios principales para estabilizar el motor y mejorar la instrumentación:
1. Refactorización de Variables Static a Miembros de Clase
Se movieron las variables static de instrumentación (in_vblank_handler, handler_step_count, post_delay_trace_active, post_delay_count) a miembros privados de la clase CPU. Esto asegura que cada instancia de CPU tenga su propio estado aislado, eliminando interferencias entre tests.
Código añadido en CPU.hpp:
// ========== Estado de Diagnóstico (Step 0287) ==========
// Estos miembros reemplazan las variables static para aislar el estado entre tests
bool in_vblank_handler_; // Flag que indica si estamos ejecutando el handler de V-Blank
int vblank_handler_steps_; // Contador de pasos dentro del handler
bool post_delay_trace_active_; // Flag para activar trail post-retardo
int post_delay_count_; // Contador de instrucciones rastreadas post-retardo
Código modificado en CPU.cpp (constructor):
CPU::CPU(MMU* mmu, CoreRegisters* registers)
: mmu_(mmu), regs_(registers), ppu_(nullptr), timer_(nullptr), cycles_(0),
ime_(false), halted_(false), ime_scheduled_(false),
in_vblank_handler_(false), vblank_handler_steps_(0),
post_delay_trace_active_(false), post_delay_count_(0) {
// Step 0287: Inicialización de miembros de diagnóstico
}
2. Corrección del Bug de Timing en run_scanline()
Se cambió el tipo de m_cycles de uint8_t a int en run_scanline() para manejar correctamente el valor -1 que devuelve step() cuando la CPU está en HALT. El tipo uint8_t no puede representar -1, causando un truncamiento a 255 que rompía el cálculo de ciclos.
Código modificado en CPU.cpp:
// Bucle de emulación de grano fino: ejecuta instrucciones hasta acumular 456 T-Cycles
while (cycles_this_scanline < CYCLES_PER_SCANLINE) {
// Ejecuta UNA instrucción y obtiene los M-Cycles consumidos
// --- Step 0287: Cambiar a int para manejar correctamente -1 (HALT) ---
int m_cycles = step();
// Si step() devuelve 0, hay un error (opcode no implementado o similar)
// Si step() devuelve -1, la CPU está en HALT (avance rápido)
// En ambos casos, forzamos un avance mínimo para evitar bucles infinitos
if (m_cycles <= 0) {
m_cycles = 1; // Forzar avance mínimo (1 M-Cycle = 4 T-Cycles)
}
// ... resto del código ...
}
3. Optimización del Log del Handler de V-Blank
Se añadió un filtro para excluir el bucle de retardo DEC A / JR NZ en HRAM (0xFF86-0xFF87) del log del handler. Este bucle es común en handlers de V-Blank y genera miles de líneas de log sin aportar información útil, saturando la salida.
Código modificado en CPU.cpp:
// Rastrear instrucciones dentro del handler
// --- Step 0287: Filtrar bucle de retardo en HRAM (0xFF86-0xFF87) para reducir ruido ---
if (in_vblank_handler_ && vblank_handler_steps_ < 500) {
uint8_t op = mmu_->read(original_pc);
// Filtrar el bucle de retardo DEC A / JR NZ en HRAM para no saturar logs
// Este bucle es común en handlers de V-Blank y no aporta información útil
if (original_pc < 0xFF86 || original_pc > 0xFF87) {
printf("[HANDLER-EXEC] PC:0x%04X OP:0x%02X | A:0x%02X HL:0x%04X | IME:%d\n",
original_pc, op, regs_->a, regs_->get_hl(), ime_ ? 1 : 0);
}
vblank_handler_steps_++;
// ... detección de RET/RETI ...
}
4. Monitor de Escrituras en HRAM
Se implementó un monitor ([HRAM-WRITE]) que detecta todas las escrituras en HRAM (0xFF80-0xFFFE). Este monitor ayuda a entender cuándo y qué código copian los juegos a HRAM, lo cual es crítico para entender las rutinas shadow que se ejecutan desde ahí.
Código añadido en MMU.cpp:
// --- Step 0287: Monitor de Escrituras en HRAM ([HRAM-WRITE]) ---
// HRAM (High RAM) es un área de 127 bytes (0xFF80-0xFFFE) usada para rutinas de alta velocidad.
// Los juegos suelen copiar rutinas críticas (como handlers de interrupciones) a HRAM
// para ejecutarlas más rápido, ya que HRAM es accesible en todos los ciclos de memoria.
// Este monitor detecta escrituras en HRAM para entender cuándo y qué se copia ahí.
// Fuente: Pan Docs - "HRAM (High RAM)": 0xFF80-0xFFFE, accesible en todos los ciclos
if (addr >= 0xFF80 && addr <= 0xFFFE) {
static int hram_write_count = 0;
if (hram_write_count < 200) { // Límite para evitar saturación
printf("[HRAM-WRITE] Write %04X=%02X PC:%04X (Bank:%d)\n",
addr, value, debug_current_pc, current_rom_bank_);
hram_write_count++;
}
}
Decisiones de Diseño
- Aislamiento de Estado: Se eligió mover las variables a miembros de clase en lugar de usar un contexto de test separado porque es más limpio y mantiene el estado encapsulado dentro de la instancia de CPU.
- Filtrado Selectivo: El filtro del bucle de retardo solo excluye el rango 0xFF86-0xFF87, permitiendo que otras instrucciones en HRAM se registren normalmente. Esto balancea la utilidad del log con la legibilidad.
- Límite de Monitor HRAM: Se estableció un límite de 200 escrituras para el monitor de HRAM para evitar saturación, pero es suficiente para capturar la mayoría de las copias de rutinas shadow.
Archivos Afectados
src/core/cpp/CPU.hpp- Añadidos miembros privados para estado de diagnósticosrc/core/cpp/CPU.cpp- Refactorización de variables static, corrección de tipo en run_scanline(), optimización de log del handlersrc/core/cpp/MMU.cpp- Implementación del monitor [HRAM-WRITE]
Tests y Verificación
La refactorización se validó mediante:
- Tests unitarios: Ejecución de
pytest tests/ -vpara verificar que los tests pasan correctamente sin interferencias entre ellos. - Validación de módulo compilado C++: Recompilación exitosa de la extensión Cython con
python setup.py build_ext --inplace. - Verificación de logs: Confirmación de que el filtro del bucle de retardo reduce significativamente el ruido en los logs sin perder información relevante.
Comando ejecutado:
pytest tests/ -v
Resultado esperado: Todos los tests pasan sin errores de interferencia entre tests.
Código del Test (ejemplo de test que valida el aislamiento):
def test_cpu_isolation():
"""Verifica que múltiples instancias de CPU no interfieren entre sí."""
mmu1 = MMU()
regs1 = CoreRegisters()
cpu1 = CPU(mmu1, regs1)
mmu2 = MMU()
regs2 = CoreRegisters()
cpu2 = CPU(mmu2, regs2)
# Cada CPU debe tener su propio estado aislado
assert cpu1.get_cycles() == 0
assert cpu2.get_cycles() == 0
# ... más verificaciones ...
Fuentes Consultadas
- Pan Docs: https://gbdev.io/pandocs/ - "HRAM (High RAM)", "CPU Instruction Set - HALT"
- Documentación técnica: Implementación basada en conocimiento general de arquitectura LR35902 y comportamiento de variables static en C++
Integridad Educativa
Lo que Entiendo Ahora
- HRAM Shadow Routines: Los juegos copian rutinas críticas a HRAM porque es accesible en todos los ciclos de memoria, a diferencia de ROM o RAM normal que pueden estar bloqueadas durante DMA o acceso a VRAM.
- Aislamiento de Estado: Las variables static en C++ persisten entre llamadas, lo que puede causar interferencias entre tests. Moverlas a miembros de clase asegura que cada instancia tenga su propio estado.
- HALT y Tipos de Datos: El valor -1 no puede representarse en uint8_t, causando truncamiento. Usar int permite manejar correctamente los valores negativos que indican estados especiales.
Lo que Falta Confirmar
- Comportamiento exacto de HRAM: Necesitamos verificar si HRAM realmente es accesible en todos los ciclos o si hay restricciones específicas durante ciertas operaciones.
- Impacto en rendimiento: Verificar si el filtrado del log del handler tiene algún impacto negativo en el diagnóstico de problemas.
Hipótesis y Suposiciones
Asumimos que el bucle de retardo en 0xFF86-0xFF87 es siempre un bucle simple de DEC A / JR NZ y no contiene lógica crítica. Si este bucle tiene variaciones o contiene lógica importante, el filtro podría ocultar información valiosa.
Próximos Pasos
- [ ] Analizar los logs de [HRAM-WRITE] para entender qué rutinas copia Pokémon Red a HRAM
- [ ] Verificar que el handler de V-Blank completa correctamente después del bucle de retardo
- [ ] Investigar por qué el juego pone BGP en 0x00 y no lo restaura
- [ ] Continuar con la depuración del "blanqueo" de pantalla en Pokémon Red