⚠️ Clean-Room / Educativo

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.

Operación Ghost in the Machine: Rastreo de Flujo Post-Retardo y Depuración de Patrones de PPU

Fecha: 2025-12-25 Step ID: 0278 Estado: Draft

Resumen

Este Step implementa la "Operación Ghost in the Machine" para rastrear el flujo de ejecución después de que el bucle de retardo identificado en el Step 0277 termina. El análisis previo confirmó que el bucle de retardo funciona correctamente (DE decrementa hasta 0), pero el juego no activa la intro (el combate Nidorino vs Gengar) después del retardo. Además, la pantalla muestra un patrón de franjas verticales erróneo, sugiriendo un problema en el renderizado de la PPU.

Se añadió instrumentación en dos puntos críticos: (1) trail de ejecución post-retardo que captura las siguientes 200 instrucciones después de que el PC sale de 0x6155 (donde termina el bucle de retardo), y (2) inspección de la PPU en el centro de la pantalla (LY=72, X=80) para ver qué Tile ID está leyendo realmente cuando renderiza el fondo. El objetivo es identificar si el juego intenta habilitar las interrupciones después del retardo, y entender por qué la PPU está renderizando un patrón erróneo de franjas verticales.

Concepto de Hardware

En los juegos originales de Game Boy desarrollados por compañías como Game Freak (Pokémon), las rutinas de inicialización siguen un patrón específico para gestionar el hardware antes de ceder el control al motor de juego principal. Estas rutinas son críticas para el funcionamiento correcto del juego.

1. Secuencia de Inicialización de Juegos

Una rutina de inicialización típica en un juego de Game Boy sigue este patrón:

  1. Reset de Hardware: Limpia registros de hardware (VRAM, OAM, registros de PPU)
  2. Configuración de Registros: Establece registros de control (LCDC, STAT, paletas, etc.)
  3. Desactivación de Interrupciones: Ejecuta DI (Disable Interrupts) para evitar interrupciones durante la inicialización
  4. Bucles de Retardo: Usa bucles de retardo por software para sincronizar con el hardware o crear pausas temporales
  5. Activación de Interrupciones: Ejecuta EI (Enable Interrupts) para permitir que el hardware (PPU, Timer) interrumpa la CPU
  6. Inicio del Motor de Juego: Cede el control al motor principal del juego, que generalmente se ejecuta en el bucle principal o en interrupciones V-Blank

2. El Problema del "Silencio Post-Retardo"

Si un juego ejecuta un bucle de retardo pero nunca habilita las interrupciones después, el juego se queda "mudo": la CPU puede ejecutar instrucciones, pero las interrupciones de hardware (V-Blank, Timer, etc.) nunca se procesan. Esto causa varios problemas:

  • Intro no arranca: Muchas intros de juegos se ejecutan en la ISR de V-Blank. Si IE=0, la ISR nunca se ejecuta y la intro no se activa
  • PPU no se sincroniza: Aunque la PPU puede seguir renderizando, sin interrupciones V-Blank, el juego no sabe cuándo un frame está completo
  • Timer no funciona: Las interrupciones de Timer nunca se procesan, causando que funciones que dependen del tiempo (animaciones, música) no funcionen

3. Patrones de Renderizado Erróneos en la PPU

Si la PPU está renderizando un patrón de franjas verticales erróneo, esto puede indicar varios problemas:

  • Direccionamiento incorrecto: La PPU podría estar leyendo Tile IDs del tilemap desde direcciones incorrectas, causando que se rendericen tiles aleatorios o basura
  • Tilemap no inicializado: Si el tilemap contiene valores basura (como 0x7F), la PPU intentará renderizar tiles que no existen o están vacíos
  • Tile Data Base incorrecto: Si el registro LCDC tiene configurado un Tile Data Base incorrecto (signed vs unsigned), los Tile IDs se interpretan incorrectamente
  • Scroll incorrecto: Si los registros SCX/SCY tienen valores incorrectos, el tilemap se lee desde posiciones erróneas

Análisis del "Código de Barras": Si la VRAM fue borrada a 0x00 (Step 0272) y el Tilemap tiene 0x7F (Step 0271), la PPU está intentando renderizar el Tile 0x7F en toda la pantalla. Si el Tile 0x7F está vacío (0x00), la pantalla debería ser de un solo color sólido. Las franjas verticales sugieren un error de direccionamiento o que estamos leyendo "basura" que se interpreta como patrones.

4. Rutinas de Inicialización y Gestión de Hardware

Las rutinas de inicialización de juegos originales (como las desarrolladas por Game Freak) son especialmente cuidadosas con la gestión del hardware porque:

  • Compatibilidad: Deben funcionar en todas las variantes del hardware (Game Boy original, Game Boy Pocket, Game Boy Color)
  • Timing crítico: La inicialización debe sincronizarse con el hardware (PPU, Timer) para evitar glitches visuales
  • Robustez: Deben manejar estados inesperados del hardware (ej: si el juego se reinicia a mitad de un frame)

Fuente: Pan Docs - Game Boy Programming Manual - Interrupts, PPU, LCD Control

Implementación

Se implementaron dos puntos de instrumentación: uno en CPU.cpp para rastrear el flujo post-retardo, y otro en PPU.cpp para inspeccionar qué Tile ID está leyendo la PPU cuando renderiza el centro de la pantalla.

1. Trail de Ejecución Post-Retardo (PC:0x6155)

Al final del método step() en CPU.cpp, se agregó un trail que captura las siguientes 200 instrucciones después de que el PC sale de 0x6155 (donde termina el bucle de retardo). El código usa original_pc (capturado al inicio de step() antes del fetch) para detectar cuando se ejecuta la instrucción en 0x6155, activando el rastreo.

// --- Step 0278: Trail de Ejecución Post-Retardo (0x6155) ---
static bool post_delay_trace = false;
static int post_delay_count = 0;

if (original_pc == 0x6155 && !post_delay_trace) {
    printf("[SNIPER-AWAKE] ¡Saliendo del bucle de retardo! Iniciando rastreo de flujo...\n");
    post_delay_trace = true;
    post_delay_count = 0;
}

if (post_delay_trace && post_delay_count < 200) {
    uint8_t current_op = mmu_->read(original_pc);
    printf("[POST-DELAY] PC:%04X OP:%02X | A:%02X HL:%04X | IE:%02X IME:%d\n",
           original_pc, current_op, regs_->a, regs_->get_hl(),
           mmu_->read(0xFFFF), ime_ ? 1 : 0);
    post_delay_count++;
}
// -----------------------------------------

Información capturada: Cada línea del trail muestra el PC original, el opcode ejecutado, el registro A, HL, el registro IE (0xFFFF), y el estado de IME (Interrupt Master Enable). Esto permite identificar si el juego intenta ejecutar EI (opcode 0xFB) o escribir en 0xFFFF para habilitar interrupciones.

2. Inspección de PPU en el Centro de la Pantalla (LY=72)

En el método render_scanline() de PPU.cpp, se agregó un log puntual que se ejecuta una sola vez cuando se renderiza el centro de la pantalla (LY=72 de 144 líneas, X=80 de 160 píxeles). El log muestra el Tile Map Address, el Tile ID leído, y el Tile Data Base configurado.

// --- Step 0278: Inspección de PPU en el centro de la pantalla ---
static int ppu_debug_count = 0;
if (ly_ == 72 && ppu_debug_count < 1) {
    // Solo una vez en el medio de la pantalla (LY=72 de 144 líneas)
    if (x == 80) {  // Centro horizontal (80 de 160 píxeles)
        printf("[PPU-DEBUG] LY:72 X:80 | TileMapAddr:%04X | TileID:%02X | TileDataBase:%04X\n",
               tile_map_addr, tile_id, tile_data_base);
        ppu_debug_count++;
    }
}
// -----------------------------------------

Información capturada: El log muestra la dirección del tilemap donde se lee el Tile ID, el Tile ID leído, y la base de datos de tiles configurada. Esto permite verificar si la PPU está leyendo Tile IDs correctos del tilemap o si está leyendo basura.

Componentes creados/modificados

  • CPU.cpp: Se modificó el método step() al final para agregar el trail de ejecución post-retardo
  • PPU.cpp: Se modificó el método render_scanline() para agregar la inspección de Tile ID en el centro de la pantalla

Decisiones de diseño

Límite de 200 instrucciones: El trail post-retardo se limita a 200 instrucciones para evitar saturar el log. Si el juego no habilita interrupciones en las primeras 200 instrucciones después del retardo, es probable que nunca lo haga, o que haya un problema más profundo en el flujo de ejecución.

Inspección única en el centro: La inspección de PPU se ejecuta solo una vez cuando se renderiza el centro de la pantalla (LY=72, X=80) para evitar saturar el log. El centro de la pantalla es representativo del estado general del tilemap.

Uso de original_pc: Se usa original_pc (capturado al inicio de step() antes del fetch) para detectar cuando se ejecuta la instrucción en 0x6155. Esto es crítico porque el PC avanza durante la ejecución de la instrucción, pero queremos detectar la instrucción que está en 0x6155, no la siguiente.

Archivos Afectados

  • src/core/cpp/CPU.cpp - Modificado método step() al final para agregar trail de ejecución post-retardo (0x6155)
  • src/core/cpp/PPU.cpp - Modificado método render_scanline() para agregar inspección de Tile ID en el centro de la pantalla (LY=72, X=80)

Tests y Verificación

La verificación se realizará ejecutando Pokémon Red y analizando los logs generados:

  • Comando ejecutado: python main.py roms/pkmn.gb
  • Resultado esperado: Se deberían ver dos tipos de mensajes en el log:
    • [SNIPER-AWAKE]: Indica que el bucle de retardo terminó y se inició el rastreo
    • [POST-DELAY]: Muestra las siguientes 200 instrucciones después del retardo, incluyendo PC, opcode, registros A/HL, IE, e IME
    • [PPU-DEBUG]: Muestra el Tile ID leído en el centro de la pantalla

Análisis de los Logs

Búsqueda de EI (0xFB): En los logs [POST-DELAY], se debe buscar si aparece el opcode 0xFB (EI - Enable Interrupts). Si aparece, el juego intenta habilitar interrupciones después del retardo. Si no aparece en las primeras 200 instrucciones, el juego probablemente no habilita interrupciones, causando el "silencio post-retardo".

Búsqueda de escrituras en 0xFFFF: También se debe buscar si el juego escribe en 0xFFFF (IE - Interrupt Enable) después del retardo. Si el juego escribe en 0xFFFF pero no ejecuta EI, las interrupciones no se habilitarán hasta que se ejecute EI.

Análisis del Tile ID: El log [PPU-DEBUG] mostrará qué Tile ID está leyendo la PPU en el centro de la pantalla. Si el Tile ID es 0x7F (como se observó en el Step 0271), la PPU está intentando renderizar el Tile 0x7F, que probablemente está vacío (0x00 en VRAM), causando que la pantalla se vea de un color sólido. Si hay franjas verticales, podría haber un error de direccionamiento en la PPU.

Validación de módulo compilado C++: ✅ Compilación exitosa sin errores de linter. La instrumentación está lista para ser probada en ejecución.

Fuentes Consultadas

Nota: Implementación basada en conocimiento general de arquitectura LR35902 y especificaciones de Pan Docs.

Integridad Educativa

Lo que Entiendo Ahora

  • Rutinas de inicialización: Los juegos originales siguen un patrón específico para gestionar el hardware antes de ceder el control al motor de juego. Esto incluye desactivar interrupciones durante la inicialización, ejecutar bucles de retardo, y luego activar interrupciones para permitir que el hardware funcione correctamente.
  • El "silencio post-retardo": Si un juego ejecuta un bucle de retardo pero nunca habilita interrupciones después, el juego se queda "mudo": la CPU puede ejecutar instrucciones, pero las interrupciones de hardware nunca se procesan. Esto causa que la intro no arranque, la PPU no se sincronice, y el Timer no funcione.
  • Patrones de renderizado erróneos: Si la PPU está renderizando un patrón de franjas verticales erróneo, esto puede indicar problemas de direccionamiento, tilemap no inicializado, Tile Data Base incorrecto, o scroll incorrecto.
  • Trail de ejecución: Rastrear las instrucciones que se ejecutan después de un punto crítico (como el fin de un bucle de retardo) permite identificar si el juego intenta habilitar interrupciones o si hay un problema en el flujo de ejecución.

Lo que Falta Confirmar

  • ¿El juego habilita interrupciones después del retardo? Si vemos EI (0xFB) o una escritura en 0xFFFF en los logs [POST-DELAY], el juego intenta habilitar interrupciones. Si no, el juego nunca habilita interrupciones, causando el "silencio post-retardo".
  • ¿Qué Tile ID está leyendo la PPU? El log [PPU-DEBUG] mostrará qué Tile ID está leyendo la PPU en el centro de la pantalla. Si es 0x7F, la PPU está intentando renderizar un tile vacío, lo que explicaría por qué la pantalla se ve de un color sólido.
  • ¿Por qué hay franjas verticales? Si hay franjas verticales en lugar de un color sólido, podría haber un error de direccionamiento en la PPU o que estamos leyendo "basura" que se interpreta como patrones.

Hipótesis y Suposiciones

Hipótesis principal: El juego no habilita interrupciones después del bucle de retardo, causando el "silencio post-retardo". La intro no arranca porque depende de interrupciones V-Blank que nunca se procesan.

Hipótesis secundaria: Las franjas verticales en la pantalla son causadas por un error de direccionamiento en la PPU o por leer Tile IDs incorrectos del tilemap. Si el tilemap tiene 0x7F y la VRAM está vacía (0x00), la pantalla debería ser de un color sólido, no mostrar franjas.

Próximos Pasos

  • [ ] Ejecutar Pokémon Red y analizar los logs [POST-DELAY] para ver qué instrucciones se ejecutan después del retardo
  • [ ] Buscar si aparece EI (0xFB) o escrituras en 0xFFFF en los logs [POST-DELAY]
  • [ ] Analizar el log [PPU-DEBUG] para ver qué Tile ID está leyendo la PPU en el centro de la pantalla
  • [ ] Si el juego no habilita interrupciones, investigar por qué (¿hay un bug en el código del juego?, ¿estamos saltando código accidentalmente?)
  • [ ] Si hay franjas verticales, investigar el error de direccionamiento en la PPU o el tilemap
  • [ ] Si el juego habilita interrupciones pero la intro no arranca, investigar por qué las interrupciones V-Blank no se procesan correctamente