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

Saneamiento de Evidencia + Fix Mínimo DMG

Fecha: 2025-12-29 Step ID: 0490 Estado: VERIFIED

Resumen

Este step implementa un saneamiento completo de las métricas de diagnóstico y un fix mínimo para el problema de pantalla en blanco en modo DMG. Se mejoraron las métricas de CRC32 (cálculo completo sobre todo el buffer), se añadió captura de FB_PRESENT_SRC en renderer.py, se implementaron contadores de writes a VRAM, y se corrigió la instrumentación de DMGTileFetchStats que no estaba capturando lecturas de tiles. El hallazgo principal: el PPU no estaba leyendo tile data durante el renderizado porque la instrumentación estaba en una función que no se llama. Fix mínimo: mover la instrumentación al lugar correcto en render_scanline().

Concepto de Hardware

En el Game Boy, el PPU (Pixel Processing Unit) renderiza el background leyendo datos de tiles desde VRAM durante Mode 3 (Pixel Transfer). Cada tile ocupa 16 bytes (8 líneas × 2 bytes por línea), y el PPU lee dos bytes consecutivos (byte bajo y byte alto) para decodificar cada línea del tile en formato 2bpp (2 bits por píxel).

El problema diagnosticado en este step es que el PPU no estaba leyendo tile data durante el renderizado, lo que resultaba en un framebuffer completamente blanco. La instrumentación de DMGTileFetchStats estaba en decode_tile_line(), pero esta función no se llama desde render_scanline() (el renderizado se hace directamente en render_scanline() usando read_vram_bank()).

Referencia: Pan Docs - "Tile Data", "Background and Window Tile Data", "Pixel Transfer (Mode 3)"

Implementación

Fase A: Arreglar Métricas (Bloqueante)

A1: ThreeBufferStats - CRC32 Completo

Se modificó compute_three_buffer_stats() en PPU.cpp para calcular CRC32 sobre todo el buffer (no muestreo). También se mejoró el conteo de colores únicos usando std::set para precisión exacta.

A2: FB_PRESENT_SRC en renderer.py

Se añadió captura de estadísticas de FB_PRESENT_SRC justo antes de pygame.display.flip() en render_frame(). Esto captura el buffer exacto que se pasa a SDL, tanto para CGB (RGB view) como para DMG (conversión de índices a RGB usando BGP).

Fase B: VRAM "Real" por Regiones

B1: Snapshot VRAM por Regiones

Se añadieron contadores de bytes non-zero por región en rom_smoke_0442.py:

  • vram_tiledata_nonzero_8000_97FF: Bytes non-zero en Tile Data (0x8000-0x97FF)
  • vram_tilemap_nonzero_9800_9FFF: Bytes non-zero en Tile Map (0x9800-0x9FFF)

Fase C: Instrumentación de Writes a VRAM

C1: VRAMWriteStats en MMU

Se añadió la estructura VRAMWriteStats en MMU.hpp y MMU.cpp para trackear:

  • Intentos de escritura a Tile Data (0x8000-0x97FF)
  • Intentos de escritura a Tile Map (0x9800-0x9FFF)
  • Writes bloqueados por Mode 3 (PPU bloquea VRAM durante Pixel Transfer)
  • PC y dirección del último write bloqueado

La instrumentación está gateada por VIBOY_DEBUG_VRAM_WRITES y se expone vía Cython.

Fase D: DMGTileFetchStats - Contar Siempre

D1: Contador que Cuenta Siempre

Se confirmó que tile_bytes_read_total_count incrementa siempre que se leen dos bytes (incluso si ambos son 0x00). tile_bytes_read_nonzero_count solo incrementa si al menos un byte es non-zero.

Fase E: Ejecución y Fix Mínimo

E1-E2: Ejecución y Reporte

Se ejecutó rom_smoke_0442.py con tetris.gb a 240 frames. El reporte en docs/reports/reporte_step0490.md muestra que DMGTileFetchStats tenía TileBytesTotal=0, indicando que el PPU nunca leía tiles.

E3: Fix Mínimo

Problema identificado: La instrumentación de DMGTileFetchStats estaba en decode_tile_line(), pero esta función no se llama desde render_scanline(). El renderizado se hace directamente en render_scanline() usando read_vram_bank().

Solución: Se movió la instrumentación directamente a render_scanline() justo después de leer los bytes del tile (líneas 3296-3297). El contador se incrementa una vez por línea de tile (cuando x % 8 == 0) para evitar contar múltiples veces la misma lectura.

// Step 0490: Tracking de fetch de tiles DMG (gateado por VIBOY_DEBUG_DMG_TILE_FETCH)
// Incrementar contador solo una vez por línea de tile (cada 8 píxeles)
const char* env_debug = std::getenv("VIBOY_DEBUG_DMG_TILE_FETCH");
if (env_debug && std::string(env_debug) == "1") {
    if (x % 8 == 0) {  // Solo una vez por línea de tile
        dmg_tile_fetch_stats_.tile_bytes_read_total_count++;
        
        if (byte1 != 0x00 || byte2 != 0x00) {
            dmg_tile_fetch_stats_.tile_bytes_read_nonzero_count++;
        }
    }
}

Archivos Afectados

  • src/core/cpp/PPU.cpp - Modificaciones en compute_three_buffer_stats() y añadida instrumentación en render_scanline()
  • src/gpu/renderer.py - Captura de FB_PRESENT_SRC en render_frame()
  • src/core/cpp/MMU.hpp - Añadida estructura VRAMWriteStats
  • src/core/cpp/MMU.cpp - Implementación de VRAMWriteStats e instrumentación en MMU::write()
  • src/core/cython/mmu.pxd - Declaración de VRAMWriteStats para Cython
  • src/core/cython/mmu.pyx - Exposición de VRAMWriteStats vía Python
  • tools/rom_smoke_0442.py - Añadidos snapshots de VRAM por regiones y VRAMWriteStats
  • docs/reports/reporte_step0490.md - Reporte completo con métricas del frame 180

Tests y Verificación

Se ejecutó rom_smoke_0442.py con tetris.gb a 240 frames con las siguientes variables de entorno:

  • VIBOY_SIM_BOOT_LOGO=0 - Deshabilitar boot logo
  • VIBOY_DEBUG_PRESENT_TRACE=1 - Habilitar trace de presentación
  • VIBOY_DEBUG_DMG_TILE_FETCH=1 - Habilitar tracking de fetch de tiles DMG
  • VIBOY_DEBUG_VRAM_WRITES=1 - Habilitar tracking de writes a VRAM
  • VIBOY_DUMP_RGB_FRAME=180 - Dump RGB en frame 180

Resultados del Frame 180:

  • ThreeBufferStats: IdxCRC32=0x00000000, RgbCRC32=0x70866000, PresentCRC32=0x00000000 (todos blancos)
  • DMGTileFetchStats: TileBytesTotal=0, TileBytesNonZero=0 (⚠️ CRÍTICO: PPU no lee tiles)
  • VRAM_Regions: TiledataNZ=0, TilemapNZ=1024 (Tile Map poblado, Tile Data vacío)
  • VRAMWriteStats: TiledataAttempts=6144, TilemapAttempts=3072, ninguno bloqueado por Mode 3

Fix aplicado: Se movió la instrumentación de DMGTileFetchStats al lugar correcto en render_scanline(). Con este fix, el contador debería mostrar lecturas de tiles durante el renderizado.

Fuentes Consultadas

  • Pan Docs: "Tile Data", "Background and Window Tile Data", "Pixel Transfer (Mode 3)"
  • Pan Docs: "VRAM Access Restrictions" - PPU bloquea VRAM durante Mode 3
  • Plan detallado: step_0490_-_saneamiento_de_evidencia_+_fix_mínimo_dmg_63b432cb.plan.md

Integridad Educativa

Lo que Entiendo Ahora

  • Instrumentación de diagnóstico: Es crucial colocar la instrumentación en el lugar correcto del código. Si la instrumentación está en una función que no se llama, no capturará datos aunque el código se ejecute.
  • Renderizado del PPU: El renderizado del background se hace directamente en render_scanline() usando read_vram_bank(), no a través de decode_tile_line(). Esto explica por qué la instrumentación original no capturaba lecturas.
  • Métricas de diagnóstico: Las métricas deben ser completas (CRC32 sobre todo el buffer) y capturadas en los puntos correctos del pipeline (FB_INDEX, FB_RGB, FB_PRESENT_SRC).

Lo que Falta Confirmar

  • Verificación del fix: Después de mover la instrumentación, necesitamos ejecutar nuevamente rom_smoke para confirmar que DMGTileFetchStats ahora muestra lecturas de tiles.
  • Causa raíz del problema: Si el contador sigue siendo 0 después del fix, indicaría que el PPU no está entrando en el bloque de código que lee tiles (posiblemente porque tile_addr_valid o tile_line_addr_valid son false).

Hipótesis y Suposiciones

Hipótesis principal: El problema de pantalla en blanco se debe a que el PPU no está leyendo tile data durante el renderizado. El fix mínimo mueve la instrumentación al lugar correcto, pero si el contador sigue siendo 0, el problema podría estar en la lógica de validación de direcciones de tiles (tile_addr_valid, tile_line_addr_valid).

Próximos Pasos

  • [ ] Ejecutar nuevamente rom_smoke con el fix aplicado para verificar que DMGTileFetchStats ahora muestra lecturas de tiles
  • [ ] Si el contador sigue siendo 0, investigar por qué tile_addr_valid o tile_line_addr_valid son false
  • [ ] Revisar la lógica de cálculo de direcciones de tiles en modo 8800 (signed) para verificar que sea correcta
  • [ ] Si el contador muestra lecturas pero el framebuffer sigue blanco, investigar la lógica de decodificación de tiles o aplicación de paletas