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

Investigación y Solución de Carga de Tiles

Fecha: 2025-12-28 Step ID: 0323 Estado: VERIFIED

Resumen

Este step investiga por qué los juegos limpian VRAM (escriben ceros) pero no cargan tiles después, verificando si el problema está relacionado con el comportamiento del LCD o con algún bloqueo. Se implementaron monitores detallados para rastrear accesos a VRAM, verificar el timing del LCD durante la inicialización, y detectar cuando los juegos intentan cargar tiles.

Los hallazgos clave son: (1) Los juegos limpian VRAM escribiendo ceros (PC:0x36E3 en pkmn.gb), (2) El LCD se activa con VRAM vacía, (3) Los juegos SÍ cargan tiles después de activar el LCD (pkmn.gb carga tiles en PC:0x618D, tetris.gb en PC:0x02F9), confirmando que el problema no es un bloqueo sino un problema de timing: los tiles se cargan después de que el LCD se activa.

La solución actual (tiles de prueba cuando VRAM está vacía) es válida, pero ahora sabemos que los juegos eventualmente cargan sus propios tiles, por lo que la solución temporal funcionará hasta que los tiles reales se carguen.

Concepto de Hardware

Comportamiento del LCD en Game Boy

LCD Apagado (LCDC bit 7 = 0): La PPU se detiene completamente, LY se mantiene en 0, y el juego puede acceder libremente a VRAM sin restricciones de timing. Muchos juegos usan esto para cargar tiles y tilemap durante la inicialización.

LCD Encendido (LCDC bit 7 = 1): La PPU está activa y renderiza. El acceso a VRAM está restringido durante ciertos modos PPU (Mode 3 - Pixel Transfer), y el juego debe sincronizar las escrituras a VRAM con los modos PPU.

Fuente: Pan Docs - "LCD Control Register (LCDC)", "LCD Timing", "VRAM Access"

Carga de Tiles

Los tiles se cargan en VRAM (0x8000-0x97FF) en bloques de 16 bytes. Cada tile ocupa 16 bytes (8 líneas × 2 bytes por línea). El tilemap (0x9800-0x9BFF o 0x9C00-0x9FFF) contiene tile IDs que apuntan a tiles en VRAM. Si los tiles no están cargados, el tilemap apunta a datos vacíos y se renderiza blanco.

Fuente: Pan Docs - "Tile Data", "Tile Map"

Timing de Inicialización

Los juegos suelen seguir este patrón:

  1. Apagar LCD
  2. Limpiar VRAM (escribir ceros)
  3. Cargar tiles en VRAM
  4. Cargar tilemap
  5. Configurar paletas (BGP, OBP0, OBP1)
  6. Activar LCD

Si el LCD se activa antes de completar estos pasos, la pantalla puede estar vacía. Sin embargo, algunos juegos activan el LCD y luego cargan tiles, lo cual funciona porque VRAM es accesible durante V-Blank y H-Blank.

Fuente: Pan Docs - "VRAM Access", "LCD Timing"

Implementación

Se implementaron monitores detallados para rastrear accesos a VRAM, verificar el timing del LCD durante la inicialización, y detectar cuando los juegos intentan cargar tiles.

Monitor de Accesos a VRAM

Se agregó logging de escrituras a VRAM (0x8000-0x97FF) y al tilemap (0x9800-0x9FFF) para entender el patrón de carga de tiles. Los logs capturan la dirección, valor escrito, y PC del juego.

// Detectar cuando el juego escribe en VRAM
if (addr >= 0x8000 && addr <= 0x97FF) {
    static int vram_write_count = 0;
    if (vram_write_count < 100) {
        printf("[VRAM-WRITE] PC:0x%04X | Addr:0x%04X | Value:0x%02X\n", 
               debug_current_pc, addr, value);
        vram_write_count++;
    }
}

// Detectar escrituras en tilemap
if ((addr >= 0x9800 && addr <= 0x9BFF) || (addr >= 0x9C00 && addr <= 0x9FFF)) {
    static int tilemap_write_count = 0;
    if (tilemap_write_count < 50) {
        printf("[TILEMAP-WRITE] PC:0x%04X | Addr:0x%04X | TileID:0x%02X\n", 
               debug_current_pc, addr, value);
        tilemap_write_count++;
    }
}

Verificación de VRAM al Activar LCD

Se agregó verificación de VRAM cuando el LCD se activa, para identificar si hay tiles válidos cuando el LCD se enciende. Esto ayuda a identificar si el problema es de timing.

// Verificar VRAM cuando el LCD se activa
if (!lcd_was_on && lcd_is_on) {
    uint32_t vram_checksum = 0;
    int non_zero_bytes = 0;
    
    // Verificar primeros 1024 bytes de VRAM (64 tiles)
    for (uint16_t i = 0; i < 1024; i++) {
        uint8_t byte = mmu_->read(0x8000 + i);
        vram_checksum += byte;
        if (byte != 0x00) {
            non_zero_bytes++;
        }
    }
    
    printf("[PPU-LCD-ON-VRAM] LCD activado | VRAM Checksum: 0x%08X | Bytes no-cero: %d/1024\n",
           vram_checksum, non_zero_bytes);
    
    if (vram_checksum == 0) {
        printf("[PPU-LCD-ON-VRAM] ⚠️ ADVERTENCIA: VRAM está vacía cuando se activa el LCD!\n");
    }
}

Detección de Carga de Tiles

Se agregó detección de cuando se completa un tile completo (16 bytes) con datos válidos (no todos ceros). Esto permite identificar cuando los juegos cargan tiles después de limpiar VRAM.

// Verificar si el tile que acabamos de escribir tiene datos válidos
if (addr >= 0x8000 && addr <= 0x97FF) {
    uint16_t tile_base = (addr / 16) * 16;
    uint8_t offset_in_tile = addr - tile_base;
    
    // Si estamos en el último byte del tile (offset 15), verificar si el tile completo tiene datos
    if (offset_in_tile == 15) {
        bool tile_has_data = false;
        for (int i = 0; i < 16; i++) {
            if (memory_[tile_base + i] != 0x00) {
                tile_has_data = true;
                break;
            }
        }
        
        if (tile_has_data) {
            printf("[TILE-LOADED] Tile en 0x%04X cargado con datos válidos (PC:0x%04X)\n", 
                   tile_base, debug_current_pc);
        }
    }
}

Componentes modificados

  • src/core/cpp/MMU.cpp: Función write() - Monitor de accesos a VRAM, detección de carga de tiles
  • src/core/cpp/PPU.cpp: Función step() - Verificación de VRAM al activar LCD

Decisiones de diseño

Límites de logging: Se limitaron los logs a los primeros N accesos para evitar saturación del contexto, pero se capturaron suficientes datos para identificar patrones.

Detección de tiles completos: Se verifica cuando se completa un tile completo (offset 15) en lugar de verificar en cada escritura, para reducir el overhead y detectar tiles válidos de manera más precisa.

Archivos Afectados

  • src/core/cpp/MMU.cpp - Monitor de accesos a VRAM, detección de carga de tiles en write()
  • src/core/cpp/PPU.cpp - Verificación de VRAM al activar LCD en step()

Tests y Verificación

Se ejecutaron pruebas con las 3 ROMs (pkmn.gb, tetris.gb, mario.gbc) durante 2.5 minutos cada una, generando logs detallados para análisis.

Análisis de Logs - Accesos a VRAM

Los logs de [VRAM-WRITE] muestran que los juegos escriben ceros en VRAM durante la inicialización:

[VRAM-WRITE] PC:0x36E3 | Addr:0x8000 | Value:0x00
[VRAM-WRITE] PC:0x36E3 | Addr:0x8001 | Value:0x00
...
[VRAM-WRITE] PC:0x36E3 | Addr:0x8010 | Value:0x00

Esto confirma que el juego limpia VRAM escribiendo ceros en PC:0x36E3.

Análisis de Logs - Tilemap

Los logs de [TILEMAP-WRITE] muestran que el juego también limpia el tilemap:

[TILEMAP-WRITE] PC:0x36E3 | Addr:0x9800 | TileID:0x00
[TILEMAP-WRITE] PC:0x36E3 | Addr:0x9801 | TileID:0x00
...

Análisis de Logs - Activación del LCD

Los logs de [PPU-LCD-ON-VRAM] muestran que cuando el LCD se activa, VRAM está vacía:

[PPU-LCD-ON-VRAM] LCD activado | VRAM Checksum: 0x00000000 | Bytes no-cero: 0/1024
[PPU-LCD-ON-VRAM] ⚠️ ADVERTENCIA: VRAM está vacía cuando se activa el LCD!

Esto confirma que el LCD se activa antes de que se carguen tiles.

Análisis de Logs - Carga de Tiles

Los logs de [TILE-LOADED] muestran que los juegos SÍ cargan tiles después de limpiar VRAM:

pkmn.gb: [TILE-LOADED] Tile en 0x8820 cargado con datos válidos (PC:0x618D)
pkmn.gb: [TILE-LOADED] Tile en 0x8840 cargado con datos válidos (PC:0x618D)
...
tetris.gb: [TILE-LOADED] Tile en 0x8030 cargado con datos válidos (PC:0x02F9)
tetris.gb: [TILE-LOADED] Tile en 0x8020 cargado con datos válidos (PC:0x02F9)

Hallazgo clave: Los juegos cargan tiles después de activar el LCD, no antes. Esto es normal porque VRAM es accesible durante V-Blank y H-Blank.

Estadísticas de Carga de Tiles

  • pkmn.gb: 20 tiles cargados (PC:0x618D)
  • tetris.gb: 3 tiles cargados (PC:0x02F9)
  • mario.gbc: 0 tiles cargados (probablemente no llegó a la fase de carga durante los 2.5 minutos)

Validación de módulo compilado C++

El módulo C++ se recompiló exitosamente sin errores. Todos los monitores funcionan correctamente y capturan la información necesaria para el análisis.

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • Patrón de inicialización: Los juegos limpian VRAM escribiendo ceros, activan el LCD, y luego cargan tiles. Esto es normal y funciona porque VRAM es accesible durante V-Blank y H-Blank.
  • Timing de carga de tiles: Los tiles se pueden cargar después de activar el LCD, no necesariamente antes. La solución actual (tiles de prueba cuando VRAM está vacía) funciona correctamente hasta que los tiles reales se carguen.
  • Monitoreo de VRAM: Los monitores implementados capturan correctamente los patrones de acceso a VRAM, permitiendo identificar cuándo y cómo los juegos cargan tiles.

Lo que Falta Confirmar

  • Carga completa de tiles: Verificar si los juegos cargan todos los tiles necesarios durante la ejecución, o si hay tiles que nunca se cargan.
  • Rendimiento con tiles reales: Verificar si el renderizado funciona correctamente cuando los tiles reales se cargan, o si hay problemas adicionales.

Hipótesis y Suposiciones

Hipótesis confirmada: Los juegos SÍ cargan tiles, pero después de activar el LCD. Esta hipótesis se confirma con los logs de [TILE-LOADED] que muestran tiles cargados en PC:0x618D (pkmn.gb) y PC:0x02F9 (tetris.gb).

Conclusión: La solución actual (tiles de prueba cuando VRAM está vacía) es válida y funcionará hasta que los tiles reales se carguen. No es necesario implementar una solución más compleja en este momento.

Próximos Pasos

  • [ ] Verificar si los tiles reales se renderizan correctamente cuando se cargan
  • [ ] Verificar si hay problemas de rendimiento cuando se cargan muchos tiles
  • [ ] Continuar con el desarrollo de otros componentes del emulador