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

Step 0399: Mejorar Métricas: Diversidad de Tile IDs y Estado Jugable

📋 Resumen Ejecutivo

El Step 0398 reveló que la métrica "tilemap 100%" era engañosa: el tilemap estaba lleno de bytes no-cero, pero todos eran tile ID 0x00 (sin diversidad). Este step mejora las métricas de detección para incluir diversidad de tile IDs y estado jugable.

Lección Aprendida del Step 0398

Problema: Contar bytes != 0x00 puede ser engañoso si todos los valores son iguales. Un tilemap "lleno" no implica estado jugable si todos los tiles son el mismo ID.

Solución: Verificar diversidad (contar tile IDs únicos) y combinar múltiples métricas para determinar estado jugable.

Resultados Clave

  • Zelda DX: Tilemap 100% pero solo 1 tile ID único (todos 0x00) → gameplay_state=NO
  • Tetris DX: 69-256 tile IDs únicosgameplay_state=YES desde Frame 720 ✅
  • Sin regresiones: Detección correcta en ROMs que funcionaban antes ✅

🔧 Concepto de Hardware

Por Qué la Diversidad de Tile IDs es Importante

Según Pan Docs - Tile Maps, el tilemap (0x9800-0x9BFF o 0x9C00-0x9FFF) es un array de 32×32 bytes (1024 tiles) donde cada byte es un Tile ID (0-255) que referencia un tile en VRAM.

Métricas Engañosas vs Correctas

Métrica Problema Ejemplo Engañoso
Bytes != 0x00 No verifica si todos son el mismo valor 1024 bytes con valor 0x00 → "100% lleno" pero sin diversidad
Tile IDs únicos ✅ Mide diversidad real 1024 bytes con valor 0x00 → 1 tile ID único → estado de inicialización

Estado Jugable vs Estado de Inicialización

Un juego en estado jugable tiene:

  1. TileData con datos: ≥200 bytes no-cero en 0x8000-0x97FF (tiles cargados desde ROM)
  2. Diversidad de tilemap: ≥10 tile IDs únicos (no solo inicialización a 0x00)
  3. Tiles completos: ≥10 tiles con ≥8 bytes no-cero (tiles reales, no bytes sueltos)

Un juego en estado de inicialización puede tener tilemap "lleno" pero sin diversidad:

  • Todos los tiles apuntan a 0x00 (limpieza inicial del tilemap)
  • VRAM vacía (tiles aún no cargados desde ROM)
  • Ejemplo: Zelda DX en Frame 1-1200 del Step 0398

Referencia: Pan Docs - Background & Tiles

La diversidad de tiles es esencial para renderizado real. Un juego que carga su pantalla principal típicamente usa 50-256 tile IDs únicos para representar:

  • Fondo estático (ej: cielo, pasto, paredes)
  • Elementos interactivos (ej: puertas, items)
  • Texto y UI (ej: barra de vida, puntos)
  • Personajes y sprites (referenciados por tilemap en algunos juegos)

⚙️ Implementación

1. Helper: count_unique_tile_ids_in_tilemap()

Archivos: src/core/cpp/PPU.hpp, src/core/cpp/PPU.cpp

Objetivo: Contar cuántos tile IDs únicos hay en el tilemap (diversidad).

Implementación:

int PPU::count_unique_tile_ids_in_tilemap() const {
    if (mmu_ == nullptr) {
        return 0;
    }
    
    uint8_t lcdc = mmu_->read(IO_LCDC);
    // Tilemap activo según LCDC bit 3
    uint16_t vram_offset = (lcdc & 0x08) ? 0x1C00 : 0x1800;  // 0x9C00 o 0x9800
    
    // Usar array de booleanos para rastrear tile IDs únicos (0-255)
    bool tile_ids_seen[256] = {false};
    int unique_count = 0;
    
    // Leer tilemap completo (32×32 = 1024 bytes)
    for (uint16_t offset = 0; offset < 0x0400; offset++) {
        uint8_t tile_id = mmu_->read_vram_bank(0, vram_offset + offset);
        if (!tile_ids_seen[tile_id]) {
            tile_ids_seen[tile_id] = true;
            unique_count++;
        }
    }
    
    return unique_count;
}

Concepto: A diferencia de contar bytes != 0x00, esto cuenta cuántos tile IDs diferentes hay. Un tilemap con todos tiles = 0x00 tiene diversidad = 1 (solo un ID único).

2. Helper: is_gameplay_state()

Archivos: src/core/cpp/PPU.hpp, src/core/cpp/PPU.cpp

Objetivo: Determinar si el juego está en estado jugable basado en métricas combinadas.

Implementación:

bool PPU::is_gameplay_state() const {
    // Verificar TileData
    int tiledata_nonzero = count_vram_nonzero_bank0_tiledata();
    if (tiledata_nonzero < 200) {
        return false;  // VRAM vacía o casi vacía
    }
    
    // Verificar diversidad de tilemap
    int unique_tile_ids = count_unique_tile_ids_in_tilemap();
    if (unique_tile_ids < 10) {
        return false;  // Tilemap sin diversidad (estado de inicialización)
    }
    
    // Verificar tiles completos
    int complete_tiles = count_complete_nonempty_tiles();
    if (complete_tiles < 10) {
        return false;  // Pocos tiles completos (datos incompletos)
    }
    
    return true;  // Todas las métricas cumplen → estado jugable
}

Concepto: Combina tres métricas independientes. Si solo se cumplen 1-2 criterios, probablemente es estado de inicialización o transición.

3. Actualización de vram_has_tiles_ con Criterio de Diversidad

Archivo: src/core/cpp/PPU.cpp (función render_scanline(), LY=0)

Cambio: Incluir criterio de diversidad en detección de VRAM.

Lógica anterior (Step 0397):

// Solo verificaba bytes no-cero O tiles completos
vram_has_tiles_ = (tiledata_nonzero >= 200) || (complete_tiles >= 10);

Lógica mejorada (Step 0399):

// Verificar diversidad de tile IDs en tilemap
int unique_tile_ids = count_unique_tile_ids_in_tilemap();

// Triple criterio mejorado:
// 1. TileData tiene datos (>= 200 bytes) Y tiles completos (>= 10)
// 2. Tilemap tiene diversidad (>= 5 tile IDs únicos)
bool has_tiles_data = (tiledata_nonzero >= 200) || (complete_tiles >= 10);
bool has_tilemap_diversity = (unique_tile_ids >= 5);

bool old_vram_has_tiles = vram_has_tiles_;
vram_has_tiles_ = has_tiles_data && has_tilemap_diversity;

// Log cuando cambia el estado (máx 10 cambios)
if (vram_has_tiles_ != old_vram_has_tiles) {
    printf("[VRAM-STATE-CHANGE] Frame %llu | has_tiles: %d -> %d | "
           "TileData: %d/6144 (%.1f%%) | Complete: %d | Unique IDs: %d\n",
           frame_counter_ + 1, old_vram_has_tiles ? 1 : 0, vram_has_tiles_ ? 1 : 0,
           tiledata_nonzero, (tiledata_nonzero * 100.0 / 6144),
           complete_tiles, unique_tile_ids);
}

Concepto: Ahora requiere ambos criterios: datos en VRAM Y diversidad en tilemap. Esto previene falsos positivos (tilemap "lleno" sin datos reales).

4. Actualización de Métricas Periódicas [VRAM-REGIONS]

Archivo: src/core/cpp/PPU.cpp (función render_scanline(), cada 120 frames)

Cambio: Incluir diversidad de tile IDs y estado jugable en logs periódicos.

Log anterior (Step 0397):

[VRAM-REGIONS] Frame 1080 | tiledata_nonzero=... | tilemap_nonzero=... | 
               complete_tiles=... | vbk=... | vram_is_empty=... | vram_has_tiles=...

Log mejorado (Step 0399):

[VRAM-REGIONS] Frame 1080 | tiledata_nonzero=0/6144 (0.0%) | 
               tilemap_nonzero=2048/2048 (100.0%) | unique_tile_ids=1/256 | 
               complete_tiles=0/384 (0.0%) | vbk=0 | gameplay_state=NO

Concepto: El campo unique_tile_ids revela inmediatamente si hay diversidad. El campo gameplay_state resume el resultado de la combinación de métricas.

✅ Tests y Verificación

Compilación

Comando ejecutado:

cd /media/fabini/8CD1-4C30/ViboyColor
python3 setup.py build_ext --inplace

Resultado: ✅ Compilación exitosa sin errores

Prueba Extendida: Zelda DX (60 segundos)

Comando ejecutado:

timeout 60s python3 main.py roms/Oro.gbc > logs/step0399_zelda_dx_extended.log 2>&1

Resultados (métricas cada 120 frames):

[VRAM-REGIONS] Frame 120 | tiledata_nonzero=0/6144 (0.0%) | tilemap_nonzero=2048/2048 (100.0%) | unique_tile_ids=1/256 | complete_tiles=0/384 (0.0%) | vbk=0 | gameplay_state=NO
[VRAM-REGIONS] Frame 240 | tiledata_nonzero=0/6144 (0.0%) | tilemap_nonzero=2048/2048 (100.0%) | unique_tile_ids=1/256 | complete_tiles=0/384 (0.0%) | vbk=0 | gameplay_state=NO
...
[VRAM-REGIONS] Frame 1200 | tiledata_nonzero=0/6144 (0.0%) | tilemap_nonzero=2048/2048 (100.0%) | unique_tile_ids=1/256 | complete_tiles=0/384 (0.0%) | vbk=0 | gameplay_state=NO

Análisis:

  • ✅ Tilemap 100% pero solo 1 tile ID único (todos 0x00)
  • gameplay_state=NO correctamente detectado durante 1200 frames
  • ✅ Confirma que Zelda DX está en estado de inicialización, no jugable

Prueba de Regresión: Tetris DX (30 segundos)

Comando ejecutado:

timeout 30s python3 main.py roms/tetris_dx.gbc > logs/step0399_tetris_dx.log 2>&1

Resultados clave:

[VRAM-STATE-CHANGE] Frame 678 | has_tiles: 0 -> 1 | TileData: 2938/6144 (47.8%) | Complete: 221 | Unique IDs: 69
[VRAM-REGIONS] Frame 720 | tiledata_nonzero=1416/6144 (23.0%) | tilemap_nonzero=259/2048 (12.6%) | unique_tile_ids=256/256 | complete_tiles=98/384 (25.5%) | vbk=0 | gameplay_state=YES
[VRAM-STATE-CHANGE] Frame 735 | has_tiles: 1 -> 0 | TileData: 0/6144 (0.0%) | Complete: 0 | Unique IDs: 1
[VRAM-STATE-CHANGE] Frame 745 | has_tiles: 0 -> 1 | TileData: 3479/6144 (56.6%) | Complete: 253 | Unique IDs: 185
[VRAM-REGIONS] Frame 840 | tiledata_nonzero=3479/6144 (56.6%) | tilemap_nonzero=2012/2048 (98.2%) | unique_tile_ids=185/256 | complete_tiles=253/384 (65.9%) | vbk=0 | gameplay_state=YES
[VRAM-REGIONS] Frame 960 | tiledata_nonzero=3479/6144 (56.6%) | tilemap_nonzero=2012/2048 (98.2%) | unique_tile_ids=185/256 | complete_tiles=253/384 (65.9%) | vbk=0 | gameplay_state=YES

Análisis:

  • ✅ Frame 678: Transición has_tiles: 0 -> 1 con 69 tile IDs únicos
  • ✅ Frame 720: gameplay_state=YES con 256 tile IDs únicos (diversidad máxima)
  • ✅ Frame 735-745: Transición temporal (posible screen clear durante modo menú)
  • ✅ Frame 840+: gameplay_state=YES estable con 185 tile IDs únicos
  • ✅ No hay regresiones: detección correcta de estado jugable

Prueba de Regresión: Pokemon Red (30 segundos)

Comando ejecutado:

timeout 30s python3 main.py roms/pkmn.gb > logs/step0399_pokemon_red.log 2>&1

Resultado: ✅ Sin cambios de estado detectados (comportamiento esperado, no alcanza estado jugable en 30s o no tiene transiciones significativas)

Validación Nativa

Módulo C++ compilado y funcionando correctamente

Los nuevos helpers count_unique_tile_ids_in_tilemap() y is_gameplay_state() se ejecutan nativamente en C++ sin overhead de Python.

📊 Resultados y Conclusiones

Comparación de Métricas: Step 0397 vs Step 0399

ROM Frame Step 0397 (tilemap_nonzero) Step 0399 (unique_tile_ids) gameplay_state
Zelda DX 1080 100.0% (engañoso) 1/256 (solo 0x00) NO
Tetris DX 720 12.6% (correcto) 256/256 (máxima diversidad) YES
Tetris DX 840+ 98.2% (correcto) 185/256 (buena diversidad) YES

Mejoras Logradas

  1. Métrica de Diversidad: unique_tile_ids detecta correctamente estado de inicialización (Zelda DX: 1/256)
  2. Estado Jugable: gameplay_state resume combinación de métricas (TileData + diversidad + tiles completos)
  3. Sin Falsos Positivos: Zelda DX ya no reporta "tilemap 100%" como si fuera jugable
  4. Sin Regresiones: Tetris DX sigue detectando estado jugable correctamente (Frame 720, 256 IDs únicos)

Lecciones Aprendidas

Métricas simples pueden ser engañosas:

  • ❌ Contar bytes != 0x00 no garantiza diversidad
  • ✅ Contar valores únicos revela el verdadero estado

Estado jugable requiere múltiples criterios:

  • TileData con datos (tiles cargados desde ROM)
  • Tilemap con diversidad (no solo inicialización a 0x00)
  • Tiles completos (no solo bytes sueltos)

Próximos Pasos

Con las métricas mejoradas, ahora podemos:

  1. Detectar con precisión cuándo un juego alcanza estado jugable
  2. Identificar transiciones de estado (ej: menú → gameplay)
  3. Investigar por qué Zelda DX no carga tiles desde ROM (posible problema de emulación o timing)

📁 Archivos Modificados

  • src/core/cpp/PPU.hpp - Declaraciones de helpers count_unique_tile_ids_in_tilemap() y is_gameplay_state()
  • src/core/cpp/PPU.cpp - Implementación de helpers y actualización de lógica de detección VRAM
  • logs/step0399_zelda_dx_extended.log - Log extendido de Zelda DX (60 segundos)
  • logs/step0399_tetris_dx.log - Log de regresión Tetris DX (30 segundos)
  • logs/step0399_pokemon_red.log - Log de regresión Pokemon Red (30 segundos)