⚠️ 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 Tile Data Addressing 0x8800 (Signed) + Tests Clean-Room

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

Resumen

Corrección crítica del bug en el direccionamiento de tile data en modo 0x8800 (signed addressing, LCDC bit4=0). El problema causaba pantallas planas en ROMs que usan este modo (como Pokémon y Tetris). El fix corrige la base incorrecta (0x8800 → 0x9000) y elimina la suma errónea de 128 en el cálculo del offset. Tests clean-room añadidos para validar ambos modos (8000 unsigned y 8800 signed).

Concepto de Hardware

El Game Boy tiene dos modos de direccionamiento para los datos de tiles (tile data) según el bit 4 del registro LCDC (LCDC bit4):

  • Modo 0x8000 (unsigned, LCDC bit4=1): Tile data en 0x8000-0x8FFF, tile_id es uint8_t (0-255), dirección = 0x8000 + tile_id * 16
  • Modo 0x8800 (signed, LCDC bit4=0): Tile data en 0x8800-0x97FF, tile_id es int8_t (-128 a 127), dirección = 0x9000 + int8(tile_id) * 16

Punto crítico del modo signed: Aunque el modo se llama "0x8800", la base real es 0x9000. El tile_id 0x00 apunta a 0x9000, el tile_id 0x80 (-128) apunta a 0x8800, y el tile_id 0xFF (-1) apunta a 0x8FF0.

El bug: El código tenía dos errores:

  1. Base incorrecta: Usaba 0x8800 en lugar de 0x9000 para el modo signed
  2. Cálculo incorrecto: Sumaba 128 al tile_id signed antes de multiplicar, lo que causaba un offset incorrecto

Referencia: Pan Docs - LCD Control Register (LCDC), bit 4: BG & Window Tile Data Select.

Implementación

El fix se aplicó en tres lugares de PPU.cpp donde se calculaba incorrectamente el direccionamiento signed:

Componentes modificados

  • src/core/cpp/PPU.cpp - Corregido base y cálculo signed (líneas 1769, 1784, 2712, 2867)
  • tools/rom_smoke_0442.py - Añadido logging de modo tile data (LCDC bit4) para diagnóstico
  • tests/test_bg_tile_data_addressing_0463.py - Tests clean-room nuevos para ambos modos

Cambios aplicados

1. Corrección de base (línea 1769):

// ANTES:
uint16_t data_base = unsigned_addressing ? 0x8000 : 0x8800;

// DESPUÉS:
uint16_t data_base = unsigned_addressing ? 0x8000 : 0x9000;  // Step 0463: Fix signed base

2. Corrección de cálculo signed (líneas 1784, 2712, 2867):

// ANTES:
tile_addr = data_base + ((signed_id + 128) * 16);

// DESPUÉS:
tile_addr = data_base + (static_cast<uint16_t>(signed_id) * 16);  // Step 0463: Fix signed calculation

3. Logging añadido en rom_smoke_0442.py:

# Derivar modo de tile data
bg_tile_data_mode = "8000(unsigned)" if (lcdc & 0x10) else "8800(signed)"
bg_tilemap_base = 0x9C00 if (lcdc & 0x08) else 0x9800
win_tilemap_base = 0x9C00 if (lcdc & 0x40) else 0x9800

# Imprimir en frames loggeados
print(f"LCDC=0x{lcdc:02X} | TileDataMode={bg_tile_data_mode} | "
      f"BGTilemap=0x{bg_tilemap_base:04X} | WinTilemap=0x{win_tilemap_base:04X} | "
      f"SCX={scx} SCY={scy} LY={ly}")

Archivos Afectados

  • src/core/cpp/PPU.cpp - Corregido direccionamiento signed (4 lugares)
  • tools/rom_smoke_0442.py - Añadido logging de modo tile data
  • tests/test_bg_tile_data_addressing_0463.py - Tests clean-room nuevos (3 tests)

Tests y Verificación

Validación realizada:

  • Tests unitarios clean-room: 3 tests pasando (modo 8000 unsigned, modo 8800 signed, modo signed extremo con tile_id 0x80)
  • Evidencia headless: Ejecución de 4 ROMs (Pokémon, Tetris, Tetris DX, Mario) con logging de modo tile data
  • Resultados headless:
    • Pokémon: Usa modo 8800(signed) - confirmado
    • Tetris: Usa modo 8800(signed) - confirmado
    • Tetris DX: Usa modo 8000(unsigned)
    • Mario: Usa modo 8000(unsigned)

Comando ejecutado: pytest -q tests/test_bg_tile_data_addressing_0463.py

Resultado: 3 passed in 0.42s

Código del Test:

def test_tile_data_addressing_8800_signed(self):
    """Caso 2: Modo 8800 (signed addressing, LCDC bit4=0)."""
    # Configurar LCDC bit4=0 (signed)
    self.mmu.write(0xFF40, 0x81)  # Bit4=0 → signed, base 0x9000
    
    # Escribir tile patrón en 0x9000 (tile_id 0x00 en signed mode)
    for line in range(8):
        self.mmu.write(0x9000 + (line * 2), 0x55)
        self.mmu.write(0x9000 + (line * 2) + 1, 0x33)
    
    # Tilemap[0] = 0x00 (apunta al tile en 0x9000 en signed mode)
    self.mmu.write(0x9800, 0x00)
    
    # Correr 1 frame y verificar que el tile se puede leer
    cycles_per_frame = 70224
    for _ in range(cycles_per_frame):
        cycles = self.cpu.step()
        self.timer.step(cycles)
        self.ppu.step(cycles)
    
    # Verificar que el tile en 0x9000 se puede leer
    tile_byte1 = self.mmu.read(0x9000)
    tile_byte2 = self.mmu.read(0x9001)
    
    assert tile_byte1 == 0x55
    assert tile_byte2 == 0x33

Validación Nativa: Validación de módulo compilado C++ con cálculo correcto de direcciones.

Fuentes Consultadas

  • Pan Docs: LCD Control Register (LCDC), bit 4: BG & Window Tile Data Select
  • Pan Docs: Memory Map, VRAM Tile Data (0x8000-0x97FF)

Integridad Educativa

Lo que Entiendo Ahora

  • Modo signed addressing: Aunque se llama "modo 0x8800", la base real es 0x9000. El tile_id se interpreta como int8_t, y el cálculo correcto es: addr = 0x9000 + int8(tile_id) * 16
  • Rango de direcciones: En modo signed, tile_id 0x00 → 0x9000, tile_id 0x80 (-128) → 0x8800, tile_id 0xFF (-1) → 0x8FF0
  • Bug común: Sumar 128 al tile_id signed es incorrecto. El casting a int8_t ya maneja correctamente la interpretación signed del byte.

Lo que Falta Confirmar

  • Verificación visual: Si el fix resuelve las pantallas planas en Pokémon y Tetris (requiere ejecución UI)
  • Impacto en otras ROMs: Verificar si hay otras ROMs afectadas por este bug

Hipótesis y Suposiciones

Hipótesis confirmada: Las ROMs problemáticas (Pokémon, Tetris) usan LCDC bit4=0 (modo signed), por lo que el bug en el direccionamiento signed causaba que leyeran tiles incorrectos o vacíos, resultando en pantallas planas.

Próximos Pasos

  • [ ] Verificación visual con grid UI para confirmar que el fix resuelve las pantallas planas
  • [ ] Si siguen planas con bit4=1 → investigar tilemap base (bit3/bit6) + window enable (bit5) o VRAM bank/attrs CGB
  • [ ] Validar que el fix no rompe ROMs que usan modo unsigned