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

Fix BG Tilemap Base + Scroll Diagnostics + Fix

Fecha: 2026-01-03 Step ID: 0464 Estado: VERIFIED

Resumen

Diagnóstico y corrección del problema de pantallas blancas/patrones que se desplazan, causado por selección incorrecta de BG tilemap base (LCDC bit3) y/o aplicación incorrecta de scroll (SCX/SCY). Se añadió instrumentación para diagnosticar qué tilemap tiene datos y cuál se está usando, se corrigió el uso de MMU::read_vram() para leer tilemap (en lugar de read() directo), y se crearon tests clean-room para validar la selección de tilemap base y scroll.

Concepto de Hardware

El Game Boy tiene dos tilemaps de fondo (BG tilemap) que pueden seleccionarse según el bit 3 del registro LCDC (LCDC bit3):

  • LCDC bit3=0: BG tilemap base en 0x9800-0x9BFF (32x32 tiles = 1024 bytes)
  • LCDC bit3=1: BG tilemap base en 0x9C00-0x9FFF (32x32 tiles = 1024 bytes)

Scroll (SCX/SCY): Los registros SCX (0xFF43) y SCY (0xFF42) controlan el desplazamiento del fondo. El scroll se aplica con wrap 0..255, es decir:

  • map_x = (x + scx) & 0xFF
  • map_y = (ly + scy) & 0xFF

El problema: Si el emulador usa el tilemap incorrecto o lee incorrectamente el tilemap (usando read() en lugar de read_vram()), puede renderizar tiles incorrectos o vacíos, resultando en pantallas blancas o patrones que se desplazan incorrectamente.

Referencia: Pan Docs - LCD Control Register (LCDC), bit 3: BG Tile Map Display Select. Pan Docs - Scroll Registers (SCX/SCY).

Implementación

El fix se implementó en cuatro fases:

Fase A: Instrumentación Mínima

Se añadió logging para diagnosticar qué tilemap tiene datos y cuál se está usando:

  • tools/rom_smoke_0442.py: Añadido conteo de nonzero bytes en ambos tilemaps (0x9800 y 0x9C00) y muestra de 16 tile IDs desde el base actual
  • src/core/cpp/PPU.cpp: Añadido log [PPU-TILEMAP-DIAG] que muestra LCDC, bg_map_base, tilemap_nz_9800, tilemap_nz_9C00, y tile_ids_sample (primeros 5 frames + cada 120)
  • src/viboy.py: Añadido log [ENV] al arranque para evidencia de kill-switches OFF

Fase B: Fix Core

Se corrigió el uso de MMU::read_vram() para leer tilemap en lugar de read() directo:

// ANTES (línea 2748):
uint8_t tile_id = mmu_->read(tile_map_addr);

// DESPUÉS:
uint16_t tile_map_offset = tile_map_addr - 0x8000;
uint8_t tile_id = 0x00;
if (tile_map_offset < 0x2000) {  // Rango válido VRAM
    tile_id = mmu_->read_vram(tile_map_offset);
}

También se corrigió el uso en la verificación inmediata de tilemap (línea 2255).

Fase C: Tests Clean-Room

Se crearon tests en tests/test_bg_tilemap_base_and_scroll_0464.py:

  • test_tilemap_base_select_9800: Verifica que con LCDC bit3=0 se usa tilemap 0x9800
  • test_tilemap_base_select_9C00: Verifica que con LCDC bit3=1 se usa tilemap 0x9C00
  • test_scx_scroll: Verifica que SCX se aplica correctamente (placeholder para verificación de framebuffer)

Fase D: Validación Real

La validación real con grid UI se realizará en un step posterior. Los logs de diagnóstico permitirán identificar si el problema está en el tilemap base, scroll, o en otros componentes (Window, OBJ, CGB attrs).

Archivos Afectados

  • src/core/cpp/PPU.cpp - Añadido logging [PPU-TILEMAP-DIAG] y corregido uso de read_vram() para leer tilemap (líneas 2171-2221, 2748, 2255)
  • tools/rom_smoke_0442.py - Añadido conteo de nonzero bytes en tilemaps y muestra de tile IDs (líneas 374-409, 512-516)
  • src/viboy.py - Añadido log [ENV] al arranque para evidencia de kill-switches (líneas 689-704)
  • tests/test_bg_tilemap_base_and_scroll_0464.py - Tests clean-room nuevos (3 tests)

Tests y Verificación

Comando ejecutado: pytest tests/test_bg_tilemap_base_and_scroll_0464.py -v

Resultado: ✅ 3/3 tests pasan

Código del Test:

def test_tilemap_base_select_9800(self):
    """Test 1: tilemap base select (0x9800 vs 0x9C00) - Caso 0x9800."""
    # Escribir tile 0 en 0x8000 (patrón 0x55/0x33)
    for line in range(8):
        self.mmu.write(0x8000 + (line * 2), 0x55)
        self.mmu.write(0x8000 + (line * 2) + 1, 0x33)
    
    # Poner en 0x9800: tile IDs = 0
    for i in range(32 * 32):
        self.mmu.write(0x9800 + i, 0x00)
    
    # Poner en 0x9C00: tile IDs = 1
    for i in range(32 * 32):
        self.mmu.write(0x9C00 + i, 0x01)
    
    # Setear LCDC bit3=0 (tilemap base 0x9800)
    self.mmu.write(0xFF40, 0x91)  # Bit3=0 → 0x9800
    
    # Correr 1 frame y verificar que se lee tile 0

Validación Nativa: Tests validan módulo compilado C++ (viboy_core).

Fuentes Consultadas

  • Pan Docs: LCD Control Register (LCDC), bit 3: BG Tile Map Display Select
  • Pan Docs: Scroll Registers (SCX/SCY)
  • Pan Docs: Memory Map, VRAM (0x8000-0x9FFF)

Integridad Educativa

Lo que Entiendo Ahora

  • BG Tilemap Base Select: LCDC bit3 controla qué tilemap se usa (0x9800 vs 0x9C00). Es crítico usar el tilemap correcto según el bit.
  • Scroll (SCX/SCY): Se aplica con wrap 0..255. El cálculo de map_x y map_y debe incluir el scroll correctamente.
  • Lectura de VRAM: Debe usarse read_vram() para leer desde VRAM (0x8000-0x9FFF), no read() directo, para respetar las restricciones de acceso durante el modo PPU.

Lo que Falta Confirmar

  • Validación Visual: Verificar con grid UI que el fix mejora el renderizado de ROMs problemáticas (Pokémon, Tetris).
  • Diagnóstico Automático: Si LCDC bit3=1 y tilemap_nz_9C00 >> tilemap_nz_9800 pero el renderer está usando 0x9800 → BUG confirmado.

Hipótesis y Suposiciones

Si el problema persiste después del fix, puede estar en Window enable (LCDC bit5), VRAM bank/attrs CGB, o en otros componentes (OBJ, paletas).

Próximos Pasos

  • [ ] Validación real con grid UI para verificar que el fix mejora el renderizado
  • [ ] Análisis de logs [PPU-TILEMAP-DIAG] para identificar qué tilemap tiene datos en ROMs problemáticas
  • [ ] Si el problema persiste, investigar Window enable (LCDC bit5) y VRAM bank/attrs CGB