⚠️ 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: Instrumentación Detallada de render_scanline

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

Resumen

Se añadió instrumentación de depuración detallada al método render_scanline() de la PPU en C++ para identificar el origen exacto del Segmentation Fault que ocurre al ejecutar el emulador con la ROM de Tetris. A pesar de que el test unitario para el modo "signed addressing" pasa correctamente, la ejecución real sigue crasheando, lo que indica que existe otro caso de uso no cubierto por el test que provoca un acceso a memoria inválido.

La instrumentación añade logs usando printf para capturar los valores críticos (línea de escaneo, scroll, direcciones de tilemap, tile IDs, direcciones calculadas) justo antes de intentar leer la memoria de los tiles. Estos logs nos permitirán identificar exactamente qué valores causan el crash.

Concepto de Hardware

En la Game Boy, la PPU renderiza cada línea de escaneo (scanline) accediendo a dos áreas principales de memoria: el Tilemap (en 0x9800-0x9BFF o 0x9C00-0x9FFF) y la Tile Data (en 0x8000-0x97FF). El proceso de renderizado para cada píxel de la línea implica:

  1. Calcular la posición en el tilemap usando scroll (SCX, SCY)
  2. Leer el Tile ID desde el tilemap
  3. Calcular la dirección del tile en VRAM según el modo de direccionamiento (signed/unsigned)
  4. Leer los datos del tile (2 bytes por línea)
  5. Decodificar el píxel y escribir en el framebuffer

Si cualquiera de estas direcciones calculadas está fuera del rango válido de VRAM (0x8000-0x9FFF), el acceso a memoria puede causar un Segmentation Fault. Aunque añadimos validaciones en el Step 0138, el crash sugiere que hay algún caso específico que no está siendo capturado por estas validaciones o que ocurre antes de llegar a ellas.

Fuente: Pan Docs - Background, Tile Data, Tile Maps

Implementación

Se añadió instrumentación de depuración usando printf (en lugar de std::cout) porque es más seguro para depurar crashes, ya que no depende de buffers que pueden no vaciarse antes de un crash.

Componentes creados/modificados

  • src/core/cpp/PPU.cpp: Añadidos logs detallados en render_scanline()

Decisiones de diseño

  • Variable estática para control de impresión: Se usa una variable estática global debug_printed para imprimir logs solo una vez (durante la primera línea de escaneo) y evitar inundar la consola.
  • Logs selectivos: Se imprimen los primeros 20 píxeles de la primera línea para capturar información relevante sin saturar la salida.
  • Logs de advertencia: Se añadieron logs especiales cuando se detectan direcciones fuera de rango, ya que estos son casos sospechosos que podrían causar el crash.
  • printf en lugar de std::cout: printf es más directo y seguro para depuración de crashes, ya que escribe directamente al stream sin buffers intermedios.

Código añadido

// Al principio del archivo
#include <cstdio>

// Variable estática para controlar la impresión
static bool debug_printed = false;

// En render_scanline(), al inicio
if (!debug_printed) {
    printf("[PPU DEBUG] render_scanline(ly=%d) | scx=%d, scy=%d | tile_map_base=0x%04X | signed_addressing=%d\n",
           ly_, scx, scy, tile_map_base, signed_addressing ? 1 : 0);
}

// Dentro del bucle de renderizado, para los primeros 20 píxeles
if (!debug_printed && x < 20) {
    printf("  [PIXEL x=%d] map_x=%d, map_y=%d | tile_map_addr=0x%04X, tile_id=%d, tile_addr=0x%04X\n",
           x, map_x, map_y, tile_map_addr, tile_id, tile_addr);
}

// Al final del método
debug_printed = true;

Archivos Afectados

  • src/core/cpp/PPU.cpp - Añadidos logs de depuración en render_scanline() para capturar valores críticos antes de acceder a memoria

Tests y Verificación

Estado actual: Esta es una entrada de depuración. El código se ha modificado para añadir instrumentación, pero aún no se ha recompilado ni ejecutado.

Plan de verificación:

  1. Recompilación: Ejecutar .\rebuild_cpp.ps1 para recompilar el módulo C++ con los nuevos logs
  2. Ejecución con ROM: Ejecutar python main.py roms/tetris.gb y capturar la salida de los logs antes del crash
  3. Análisis: Analizar la última línea impresa antes del Segmentation Fault para identificar los valores exactos que causan el problema

Resultado esperado: Los logs deberían mostrar los valores de ly, scx, scy, tile_map_addr, tile_id, y tile_addr para los primeros píxeles. La última línea impresa antes del crash indicará qué valores están causando el acceso a memoria inválido.

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • Depuración de crashes: Cuando un programa crashea con un Segmentation Fault, la instrumentación con logs es una herramienta esencial para identificar el punto exacto donde ocurre el problema. Los logs deben capturar los valores de las variables críticas justo antes de la operación que causa el crash.
  • printf vs std::cout: Para depuración de crashes, printf es preferible porque escribe directamente al stream sin buffers intermedios que pueden no vaciarse antes del crash.
  • Validación de direcciones: Aunque añadimos validaciones en el Step 0138, el crash persiste, lo que sugiere que hay un caso específico no cubierto o que el crash ocurre en un lugar diferente al esperado.

Lo que Falta Confirmar

  • Valores exactos del crash: Necesitamos ejecutar el emulador con la instrumentación para ver los valores exactos que causan el Segmentation Fault.
  • Ubicación del crash: Aunque sospechamos que ocurre en render_scanline(), los logs nos confirmarán si es así o si ocurre en otro lugar (por ejemplo, en la MMU al leer).
  • Casos no cubiertos: Una vez tengamos los valores del crash, podremos identificar qué casos específicos no están siendo manejados correctamente por nuestras validaciones.

Hipótesis y Suposiciones

Hipótesis principal: El crash ocurre durante el renderizado de una línea específica cuando se intenta acceder a una dirección de VRAM fuera de los límites válidos (0x8000-0x9FFF). Los logs nos permitirán confirmar esta hipótesis y ver exactamente qué valores causan el problema.

Suposición: El test unitario pasa porque crea una situación ideal y predecible, mientras que la ROM real usa combinaciones de valores (tile IDs, scroll, etc.) que exponen bugs en casos límite que no están cubiertos por el test.

Próximos Pasos

  • [ ] Recompilar el módulo C++ con los nuevos logs: .\rebuild_cpp.ps1
  • [ ] Ejecutar el emulador con Tetris: python main.py roms/tetris.gb
  • [ ] Capturar y analizar la salida de los logs antes del crash
  • [ ] Identificar los valores exactos que causan el Segmentation Fault
  • [ ] Corregir el bug identificado en el siguiente step (0140)
  • [ ] Eliminar los logs de depuración una vez resuelto el problema (para mejorar el rendimiento)