⚠️ 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.

Debug: Trazado de la CPU para Diagnosticar VRAM Vacía

Fecha: 2025-12-19 Step ID: 0149 Estado: 🔍 DRAFT

Resumen

Después de resolver el Segmentation Fault y lograr que el emulador corra estable a 60 FPS, el siguiente problema identificado es una pantalla en blanco. El diagnóstico indica que la VRAM está vacía porque la CPU no está ejecutando la rutina que copia los datos gráficos desde la ROM a la VRAM. Se añadió instrumentación de diagnóstico en CPU::step() para trazar las primeras 100 instrucciones ejecutadas por la ROM, mostrando el PC (Program Counter) y el opcode de cada instrucción. Esta traza permitirá identificar qué instrucción falta o qué bucle está bloqueando la ejecución.

Concepto de Hardware

En la Game Boy, cuando se inicia una ROM, la CPU ejecuta una secuencia de instrucciones que inicializa el hardware y copia los datos gráficos (tiles, sprites, mapas) desde la ROM (memoria de solo lectura) a la VRAM (Video RAM, rango 0x8000-0x9FFF). La PPU (Picture Processing Unit) lee estos datos de la VRAM para renderizar la pantalla.

El problema de la pantalla blanca: Si la pantalla está en blanco pero el emulador corre a 60 FPS, significa que:

  • El framebuffer se está creando y pasando a Pygame correctamente
  • El renderizador de Python está dibujando el contenido del framebuffer
  • El contenido del framebuffer es uniformemente el color de fondo (índice de color 0, que nuestra paleta por defecto traduce a blanco)

Esto indica que la PPU está renderizando correctamente, pero está leyendo una VRAM que está completamente vacía (llena de ceros). La VRAM está vacía porque la CPU aún no ha ejecutado la rutina de código que copia los datos gráficos del logo de Nintendo desde la ROM a la VRAM.

¿Por qué estaría la VRAM vacía? La CPU está ejecutando código, pero probablemente está atascada en un bucle o le falta una instrucción clave que le impide llegar a la rutina de copia de gráficos. Para diagnosticar esto, necesitamos ver exactamente qué instrucciones está ejecutando la CPU y en qué punto se detiene o entra en un bucle infinito.

La solución: trazado de instrucciones Añadiendo logs de diagnóstico en el método CPU::step() que muestren el PC (Program Counter) y el opcode de cada instrucción antes de ejecutarla, podemos ver el flujo exacto de ejecución. Limitando el número de logs a las primeras 100 instrucciones, obtenemos suficiente información para identificar el problema sin saturar la consola.

Implementación

Se añadió instrumentación de diagnóstico en src/core/cpp/CPU.cpp para trazar las primeras 100 instrucciones ejecutadas por la CPU. El logging muestra el contador de instrucción, el PC (Program Counter) antes de leer el opcode, y el opcode leído.

Componentes modificados

  • src/core/cpp/CPU.cpp: Añadido #include <cstdio>, variables estáticas para contador de debug, y bloque de logging en step()

Cambios aplicados

1. Inclusión de librería de I/O:

  • Añadido #include <cstdio> al principio del archivo para usar printf

2. Variables estáticas para logging:

  • Añadida variable estática debug_instruction_counter para contar las instrucciones loggeadas
  • Añadida constante DEBUG_INSTRUCTION_LIMIT = 100 para limitar el número de logs
  • El contador se resetea a 0 en el constructor de CPU para cada nueva instancia

3. Bloque de logging en step():

  • Se guarda el PC actual antes de leer el opcode (porque fetch_byte() incrementa el PC)
  • Se añade un bloque condicional que imprime el contador, PC y opcode si el contador es menor que el límite
  • El formato del log es: [CPU TRACE N] PC: 0xXXXX | Opcode: 0xXX

Código clave

Variables estáticas añadidas:

// Variables estáticas para logging de diagnóstico
static int debug_instruction_counter = 0;
static const int DEBUG_INSTRUCTION_LIMIT = 100;

Reset del contador en el constructor:

CPU::CPU(MMU* mmu, CoreRegisters* registers)
    : mmu_(mmu), regs_(registers), cycles_(0), ime_(false), halted_(false), ime_scheduled_(false) {
    // ...
    // Resetear contador de debug al crear nueva instancia
    debug_instruction_counter = 0;
}

Bloque de logging en step():

// ========== FASE 4: Fetch-Decode-Execute ==========
// Fetch: Leer opcode de memoria
uint16_t current_pc = regs_->pc;  // Guardamos el PC actual para el log
uint8_t opcode = fetch_byte();

// --- BLOQUE DE LOGGING DE DIAGNÓSTICO ---
if (debug_instruction_counter < DEBUG_INSTRUCTION_LIMIT) {
    printf("[CPU TRACE %d] PC: 0x%04X | Opcode: 0x%02X\n",
           debug_instruction_counter, current_pc, opcode);
    debug_instruction_counter++;
}
// --- FIN DEL BLOQUE DE LOGGING ---

Archivos Afectados

  • src/core/cpp/CPU.cpp - Añadido #include <cstdio>, variables estáticas para logging, y bloque de logging en step()

Tests y Verificación

Para verificar la instrumentación:

  1. Recompilar el módulo: Ejecutar .\rebuild_cpp.ps1 (o python setup.py build_ext --inplace)
  2. Ejecutar el emulador: Ejecutar python main.py roms/tetris.gb
  3. Analizar la salida: La consola mostrará las primeras 100 instrucciones ejecutadas con el formato:
    [CPU TRACE 0] PC: 0x0100 | Opcode: 0x31
    [CPU TRACE 1] PC: 0x0103 | Opcode: 0xAF
    [CPU TRACE 2] PC: 0x0104 | Opcode: 0x21
    ...
    [CPU TRACE 99] PC: 0xXXXX | Opcode: 0xXX
  4. Identificar el problema: Buscar en la traza:
    • El último opcode ejecutado antes de que la CPU entre en un bucle
    • Un opcode no implementado (que retornaría 0 ciclos y causaría un bucle infinito)
    • Un bucle infinito evidente (mismo PC y opcode repetidos)

Nota: Esta instrumentación es temporal y se eliminará después de identificar el problema para restaurar el rendimiento máximo.

Fuentes Consultadas

Nota: La instrumentación de diagnóstico es una técnica estándar de depuración de bajo nivel. El formato del log sigue convenciones comunes de trazas de CPU.

Integridad Educativa

Lo que Entiendo Ahora

  • Trazado de instrucciones: El trazado de instrucciones es una técnica fundamental de depuración que muestra el flujo exacto de ejecución de un programa. En un emulador, esto es especialmente útil porque podemos ver exactamente qué instrucciones está ejecutando la ROM y en qué punto se detiene o entra en un bucle.
  • Pantalla blanca vs Segmentation Fault: Una pantalla blanca a 60 FPS indica que el emulador está funcionando correctamente a nivel de sincronización, pero la CPU no está ejecutando el código necesario para inicializar la VRAM. Esto es un problema diferente y más predecible que un Segmentation Fault.
  • Variables estáticas en C++: Las variables estáticas en C++ tienen un scope de archivo y se inicializan una sola vez. Son útiles para contadores globales que deben persistir entre llamadas a funciones, pero deben usarse con cuidado en contextos multi-threaded.

Lo que Falta Confirmar

  • Opcode faltante: Necesitamos ejecutar el emulador y analizar la traza para identificar qué opcode falta o qué bucle está bloqueando la ejecución.
  • Rutina de inicialización: Una vez identificado el opcode faltante, necesitaremos implementarlo y verificar que la CPU pueda continuar hasta la rutina de copia de gráficos.

Hipótesis y Suposiciones

Hipótesis principal: La CPU está encontrando un opcode que aún no hemos migrado a C++ y que es esencial para la inicialización. Este opcode probablemente retorna 0 ciclos (porque está en el default del switch), causando que la CPU no avance y entre en un bucle infinito o se detenga.

Suposición: El problema no está en la PPU ni en la MMU (ambas están funcionando correctamente), sino en la CPU que no puede ejecutar todas las instrucciones necesarias para inicializar la VRAM.

Próximos Pasos

  • [ ] Recompilar el módulo C++ con la instrumentación
  • [ ] Ejecutar el emulador y capturar la traza de las primeras 100 instrucciones
  • [ ] Analizar la traza para identificar el último opcode ejecutado o el bucle infinito
  • [ ] Implementar el opcode faltante o corregir el bucle
  • [ ] Verificar que la CPU pueda continuar hasta la rutina de copia de gráficos
  • [ ] Eliminar la instrumentación de diagnóstico para restaurar el rendimiento