⚠️ 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 de Escrituras Tempranas y Restricciones de Acceso a VRAM

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

Resumen

Se implementó monitoreo de VRAM desde el inicio de la ejecución para investigar por qué los juegos escriben solo ceros (0x00) a VRAM durante la ejecución. Se verificó el estado inicial de VRAM cuando se carga la ROM, se investigaron restricciones de acceso a VRAM cuando LCD=ON, y se monitorearon cambios de estado del LCD. Los resultados muestran que los juegos SÍ tienen datos iniciales en VRAM (92-98% de bytes no-cero), pero todas las escrituras durante la ejecución son ceros (0% de escrituras no-cero). El LCD se apaga y se enciende durante la ejecución, pero cuando se apaga, VRAM ya tiene solo 40 bytes no-cero (0.65%), sugiriendo que los datos iniciales se borran antes de que el juego pueda cargar tiles nuevos.

Concepto de Hardware

Restricciones de Acceso a VRAM: En el hardware real de Game Boy, el acceso a VRAM está restringido cuando el LCD está encendido. Solo se puede acceder a VRAM durante VBLANK (cuando el LCD está en modo VBLANK, LY >= 144) o cuando el LCD está completamente apagado (LCDC bit 7 = 0). Escribir a VRAM cuando el LCD está encendido (fuera de VBLANK) puede causar problemas o ser ignorado por el hardware. Esta restricción es crítica porque la PPU está leyendo VRAM constantemente durante el renderizado, y escribir mientras se lee puede causar corrupción de datos.

Timing de LCD: Los juegos suelen apagar el LCD para cargar tiles en VRAM de forma segura. Después de cargar los tiles, los juegos encienden el LCD. El LCD se puede apagar/encender mediante el registro LCDC (bit 7). Cuando el LCD está apagado, la PPU se detiene y LY se mantiene en 0, permitiendo que el juego modifique VRAM sin restricciones. Si un juego intenta escribir tiles cuando el LCD está encendido, las escrituras pueden ser ignoradas o causar corrupción.

Carga de Tiles: Los tiles se cargan en VRAM durante la inicialización del juego o durante la ejecución cuando el LCD está apagado. Los tiles se escriben en secuencias de 16 bytes consecutivos (cada tile es 8x8 píxeles, 2 bytes por línea, 8 líneas = 16 bytes). Si un juego escribe solo 0x00 a VRAM, no se están cargando tiles reales, solo se está limpiando VRAM. Si un juego tiene datos iniciales en VRAM (desde la ROM o desde la inicialización), pero luego escribe solo ceros, los datos iniciales se están borrando.

Estado Inicial de VRAM: Algunos juegos pueden tener datos iniciales en VRAM desde la ROM o desde la inicialización del juego. Estos datos pueden ser tiles de prueba, tiles del logo de Nintendo, o tiles básicos del juego. Si estos datos se borran (escribiendo 0x00) antes de que el juego cargue tiles nuevos, VRAM quedará vacía y no se mostrará nada en pantalla.

Implementación

Se implementaron 4 tareas principales de diagnóstico según el plan Step 0353:

1. Monitoreo de VRAM Desde el Inicio de la Ejecución (MMU.cpp)

Se agregó código en MMU::write() para monitorear VRAM desde el inicio de la ejecución, no solo después de activar logs. El código inicializa el monitoreo en la primera escritura a VRAM y loggea todas las escrituras no-cero (hasta 200) con información sobre el estado del LCD y el PC.

// --- Step 0353: Monitoreo Desde el Inicio ---
static int vram_write_from_start_count = 0;
static int vram_write_non_zero_from_start = 0;
static bool vram_monitoring_initialized = false;

if (!vram_monitoring_initialized) {
    vram_monitoring_initialized = true;
    printf("[MMU-VRAM-MONITOR-INIT] Monitoreo de VRAM inicializado desde el inicio\n");
}

if (addr >= 0x8000 && addr < 0x9800) {
    vram_write_from_start_count++;
    
    if (value != 0x00) {
        vram_write_non_zero_from_start++;
        
        // Loggear todas las escrituras no-cero (hasta 200)
        if (vram_write_non_zero_from_start <= 200) {
            uint16_t pc = debug_current_pc;
            bool lcd_is_on = false;
            if (ppu_ != nullptr) {
                lcd_is_on = ppu_->is_lcd_on();
            }
            
            printf("[MMU-VRAM-WRITE-FROM-START] Non-zero write #%d | Addr=0x%04X | Value=0x%02X | "
                   "PC=0x%04X | LCD=%s | Total writes=%d\n",
                   vram_write_non_zero_from_start, addr, value, pc,
                   lcd_is_on ? "ON" : "OFF", vram_write_from_start_count);
        }
    }
    
    // Loggear estadísticas cada 1000 escrituras
    if (vram_write_from_start_count % 1000 == 0) {
        printf("[MMU-VRAM-WRITE-FROM-START-STATS] Total writes=%d | Non-zero writes=%d | "
               "Non-zero ratio=%.2f%%\n",
               vram_write_from_start_count, vram_write_non_zero_from_start,
               (vram_write_non_zero_from_start * 100.0) / vram_write_from_start_count);
    }
}
// -------------------------------------------

2. Verificación del Estado Inicial de VRAM (MMU.cpp)

Se agregó la función MMU::check_initial_vram_state() que se llama desde MMU::load_rom() para verificar el estado inicial de VRAM cuando se carga la ROM. La función cuenta bytes no-cero y tiles completos en VRAM, y genera un log con estadísticas.

// --- Step 0353: Verificación del Estado Inicial de VRAM ---
void MMU::check_initial_vram_state() {
    int non_zero_bytes = 0;
    int complete_tiles = 0;
    
    for (uint16_t addr = 0x8000; addr < 0x9800; addr += 16) {
        int tile_non_zero = 0;
        
        for (int i = 0; i < 16; i++) {
            uint8_t byte = memory_[addr - 0x8000 + i];
            if (byte != 0x00) {
                non_zero_bytes++;
                tile_non_zero++;
            }
        }
        
        if (tile_non_zero >= 8) {
            complete_tiles++;
        }
    }
    
    printf("[MMU-VRAM-INITIAL-STATE] VRAM initial state | Non-zero bytes: %d/6144 (%.2f%%) | "
           "Complete tiles: %d/384 (%.2f%%)\n",
           non_zero_bytes, (non_zero_bytes * 100.0) / 6144,
           complete_tiles, (complete_tiles * 100.0) / 384);
    
    if (non_zero_bytes > 200) {
        printf("[MMU-VRAM-INITIAL-STATE] ✅ VRAM tiene datos iniciales (posiblemente desde ROM)\n");
    } else {
        printf("[MMU-VRAM-INITIAL-STATE] ⚠️ VRAM está vacía al inicio\n");
    }
}
// -------------------------------------------

3. Verificación de Restricciones de Acceso a VRAM Cuando LCD=ON (MMU.cpp)

Se agregó código en MMU::write() para verificar si hay restricciones de acceso a VRAM cuando LCD=ON. El código detecta escrituras no-cero a VRAM cuando el LCD está encendido y no estamos en VBLANK, y loggea estas escrituras (hasta 50) con información sobre el estado del LCD.

// --- Step 0353: Verificación de Restricciones de Acceso a VRAM ---
bool lcd_is_on = false;
bool in_vblank = false;

if (ppu_ != nullptr) {
    lcd_is_on = ppu_->is_lcd_on();
    uint8_t ly = ppu_->get_ly();
    in_vblank = (ly >= 144);
}

bool access_restricted = lcd_is_on && !in_vblank;

if (access_restricted && value != 0x00) {
    static int restricted_access_count = 0;
    restricted_access_count++;
    
    if (restricted_access_count <= 50) {
        uint16_t pc = debug_current_pc;
        
        printf("[MMU-VRAM-ACCESS-RESTRICTED] Write #%d | Addr=0x%04X | Value=0x%02X | "
               "PC=0x%04X | LCD=ON, not in VBLANK | Access should be restricted\n",
               restricted_access_count, addr, value, pc);
    }
}
// TODO: Implementar restricciones de acceso a VRAM cuando LCD=ON (excepto VBLANK)
// -------------------------------------------

4. Verificación de Cambios de Estado del LCD (PPU.cpp)

Se agregó código en PPU::step() para verificar cambios de estado del LCD durante la ejecución. El código detecta cuando el LCD se apaga o se enciende, y cuando se apaga, verifica el estado de VRAM (cuántos bytes no-cero tiene).

// --- Step 0353: Verificación de Cambios de Estado del LCD ---
static bool last_lcd_state_step0353 = false;
static int lcd_state_change_count = 0;

bool current_lcd_state = lcd_enabled;

if (current_lcd_state != last_lcd_state_step0353) {
    lcd_state_change_count++;
    
    printf("[PPU-LCD-STATE-CHANGE] Change #%d | Frame %llu | LY: %d | "
           "LCD changed from %s to %s\n",
           lcd_state_change_count,
           static_cast(frame_counter_),
           ly_,
           last_lcd_state_step0353 ? "ON" : "OFF",
           current_lcd_state ? "ON" : "OFF");
    
    // Si el LCD se apaga, verificar el estado de VRAM
    if (!current_lcd_state) {
        int non_zero_bytes = 0;
        for (uint16_t addr = 0x8000; addr < 0x9800; addr++) {
            uint8_t byte = mmu_->read(addr);
            if (byte != 0x00) {
                non_zero_bytes++;
            }
        }
        
        printf("[PPU-LCD-STATE-CHANGE] LCD OFF | VRAM non-zero bytes: %d/6144 (%.2f%%)\n",
               non_zero_bytes, (non_zero_bytes * 100.0) / 6144);
    }
    
    last_lcd_state_step0353 = current_lcd_state;
}
// -------------------------------------------

Archivos Afectados

  • src/core/cpp/MMU.cpp - Agregado monitoreo desde el inicio, verificación de estado inicial de VRAM, y verificación de restricciones de acceso
  • src/core/cpp/MMU.hpp - Agregada declaración de check_initial_vram_state()
  • src/core/cpp/PPU.cpp - Agregada verificación de cambios de estado del LCD

Tests y Verificación

Se ejecutaron pruebas con las 5 ROMs en paralelo (~2.5 minutos total) para analizar el comportamiento de VRAM:

  • ROMs probadas: pkmn.gb, tetris.gb, mario.gbc, pkmn-amarillo.gb, Oro.gbc
  • Comando ejecutado: timeout 150 python3 main.py roms/<rom>.gb 2>&1 | tee logs/test_<rom>_step0353.log
  • Análisis de logs: Se usaron comandos grep y head para extraer información específica sin saturar el contexto

Resultados Clave

  • Monitoreo desde el inicio: ✅ Inicializado correctamente en todas las ROMs
  • Escrituras no-cero: ❌ 0 escrituras no-cero en todas las ROMs (0% de escrituras no-cero)
  • Estado inicial de VRAM: ✅ Los juegos SÍ tienen datos iniciales (92-98% de bytes no-cero)
  • Restricciones de acceso: ⚠️ No se detectaron accesos restringidos (no hay escrituras no-cero cuando LCD=ON)
  • Cambios de estado del LCD: ✅ El LCD se apaga y se enciende, pero cuando se apaga, VRAM tiene solo 40 bytes no-cero (0.65%)

Ejemplo de Logs

[MMU-VRAM-MONITOR-INIT] Monitoreo de VRAM inicializado desde el inicio
[MMU-VRAM-INITIAL-STATE] VRAM initial state | Non-zero bytes: 5867/6144 (95.49%) | Complete tiles: 374/384 (97.40%)
[MMU-VRAM-INITIAL-STATE] ✅ VRAM tiene datos iniciales (posiblemente desde ROM)
[MMU-VRAM-WRITE-FROM-START-STATS] Total writes=1000 | Non-zero writes=0 | Non-zero ratio=0.00%
[PPU-LCD-STATE-CHANGE] Change #1 | Frame 0 | LY: 0 | LCD changed from OFF to ON
[PPU-LCD-STATE-CHANGE] Change #2 | Frame 1 | LY: 148 | LCD changed from ON to OFF
[PPU-LCD-STATE-CHANGE] LCD OFF | VRAM non-zero bytes: 40/6144 (0.65%)

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • Restricciones de acceso a VRAM: En hardware real, el acceso a VRAM está restringido cuando el LCD está encendido (excepto durante VBLANK). Esto previene corrupción de datos cuando la PPU está leyendo VRAM durante el renderizado.
  • Estado inicial de VRAM: Los juegos pueden tener datos iniciales en VRAM desde la ROM o desde la inicialización. Estos datos pueden ser tiles de prueba, tiles del logo, o tiles básicos del juego.
  • Problema identificado: Los juegos tienen datos iniciales en VRAM (92-98% de bytes no-cero), pero todas las escrituras durante la ejecución son ceros (0% de escrituras no-cero). Esto sugiere que los datos iniciales se están borrando antes de que el juego pueda cargar tiles nuevos.
  • Timing del LCD: El LCD se apaga y se enciende durante la ejecución, pero cuando se apaga, VRAM ya tiene solo 40 bytes no-cero (0.65%), sugiriendo que los datos iniciales se borraron antes de que el LCD se apagara.

Lo que Falta Confirmar

  • ¿Por qué se borran los datos iniciales? Necesitamos investigar si hay escrituras de ceros que borran los datos iniciales antes de que el juego pueda cargar tiles nuevos.
  • ¿Cuándo se borran los datos iniciales? Necesitamos verificar si las escrituras de ceros ocurren durante la inicialización o durante la ejecución.
  • ¿Los juegos esperan que VRAM esté vacía? Algunos juegos pueden esperar que VRAM esté vacía al inicio y cargar tiles desde cero. Necesitamos verificar si los juegos intentan cargar tiles pero fallan por alguna razón.
  • ¿Hay restricciones de acceso implementadas? Actualmente no implementamos restricciones de acceso a VRAM cuando LCD=ON. Necesitamos verificar si esto es necesario o si el problema es otro.

Hipótesis y Suposiciones

Hipótesis principal: Los datos iniciales en VRAM se están borrando (escribiendo 0x00) antes de que el juego pueda cargar tiles nuevos. Esto podría ocurrir durante la inicialización del juego o durante la ejecución cuando el LCD está encendido. Si los juegos intentan cargar tiles cuando el LCD está apagado, pero los datos iniciales ya se borraron, VRAM quedará vacía y no se mostrará nada en pantalla.

Suposición: Los juegos pueden estar escribiendo ceros a VRAM durante la inicialización para limpiar VRAM antes de cargar tiles nuevos. Si esto ocurre antes de que empecemos a monitorear, no lo detectaríamos. Sin embargo, el monitoreo desde el inicio debería capturar estas escrituras.

Próximos Pasos

  • [ ] Step 0354: Investigar por qué se borran los datos iniciales de VRAM. Verificar si hay escrituras de ceros que ocurren antes de que el juego pueda cargar tiles nuevos.
  • [ ] Step 0355: Implementar restricciones de acceso a VRAM cuando LCD=ON (excepto VBLANK) si es necesario.
  • [ ] Step 0356: Verificar si los juegos intentan cargar tiles pero fallan por alguna razón. Investigar si hay problemas con el timing o con el acceso a VRAM.