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

DMA/OAM Correctness + Sprite Visible Test + Freeze Hardware Contracts

Fecha: 2026-01-02 Step ID: 0444 Estado: VERIFIED

Resumen

Validación end-to-end de OAM DMA (0xFF46): creación de tests clean-room que verifican que DMA copia correctamente 160 bytes desde source a OAM, y que el PPU consume OAM real (sprites aparecen cuando OAM tiene datos). Añadidos 2 tests principales que congelan el contrato hardware: (1) DMA copia correcta, (2) Sprites renderizan (al menos 1 sprite visible). Opcionalmente, añadidas métricas OAM a herramienta headless para diagnóstico futuro.

Concepto de Hardware

DMA (Direct Memory Access) OAM Transfer (0xFF46): La Game Boy incluye un mecanismo de DMA que permite copiar datos a la OAM (Object Attribute Memory) sin intervención directa de la CPU. Escribir un valor XX en 0xFF46 inicia una transferencia que copia 160 bytes desde la dirección XX00 hasta 0xFE00-0xFE9F (OAM).

En hardware real, la transferencia tarda aproximadamente 160 microsegundos (640 T-cycles), y durante este tiempo la CPU solo puede acceder a HRAM (0xFF80-0xFFFE). Sin embargo, para este Step nos enfocamos en la correctness de datos (que la copia sea correcta) antes que el timing exacto.

OAM (Object Attribute Memory): La OAM contiene los datos de hasta 40 sprites. Cada sprite ocupa 4 bytes:

  • Byte 0: Y position (0-255, offset 16 para pantalla)
  • Byte 1: X position (0-255, offset 8 para pantalla)
  • Byte 2: Tile ID (0-255, índice del tile en VRAM)
  • Byte 3: Flags (palette, flip X/Y, priority, etc.)

Sprite Rendering: El PPU lee OAM durante el modo OAM Search (Mode 2) para determinar qué sprites están visibles en el scanline actual. Si OAM tiene datos válidos (Y/X dentro de rango, tile data presente en VRAM), el sprite debe aparecer en el framebuffer.

Fuente: Pan Docs - "DMA Transfer", "OAM (Object Attribute Memory)", "Sprite Rendering"

Implementación

Auditoría Previa: Se verificó que DMA OAM (0xFF46) ya existe en src/core/cpp/MMU.cpp líneas 967-1004. La implementación actual:

  • Detecta escritura a 0xFF46
  • Calcula source_base = value << 8
  • Ejecuta loop que copia 160 bytes desde source_addr a OAM (0xFE00-0xFE9F)
  • Usa read(source_addr) para leer (correcto: puede ser ROM/VRAM/WRAM)
  • Escribe directamente a memory_[0xFE00 + i] (correcto: bypass especial según Pan Docs)
  • Modelo: instantáneo (sin delay/timing)

Conclusión: Implementación básica correcta. Necesitamos validarla con tests clean-room.

Fase A - Test Clean-Room: "DMA Copies 160 Bytes"

Archivo: tests/test_dma_oam_copy_0444.py (< 80 líneas)

Test 1: test_dma_oam_copies_160_bytes():

  • Prepara patrón incremental en WRAM (0xC000-0xC09F): 0x00, 0x01, 0x02, ..., 0x9F
  • Verifica que OAM está limpio (0xFE00-0xFE9F = 0)
  • Activa DMA: escribe source page (0xC0) a 0xFF46
  • Verifica que DMA copió correctamente: mem[0xFE00+i] == pattern[i] para i=0..0x9F
  • Verifica que source no cambió (DMA es read-only en source)

Test 2: test_dma_oam_from_different_source():

  • Valida que DMA funciona desde diferentes sources (WRAM alternativo 0xD000)
  • Patrón diferente: 0xAA + (i & 0x0F)
  • Verifica copia correcta

Fase B - Test Clean-Room: "Single Sprite Visible"

Archivo: tests/test_sprite_visible_0444.py (< 100 líneas)

Test: test_single_sprite_visible():

  • Carga tile data para sprite (Tile ID 0x00, dirección 0x8000): patrón checkerboard (0xAA/0x55 alternado)
  • Configura OAM entry para sprite: y=36 (scanline 20), x=38 (columna 30), tile_id=0x00, flags=0x00
  • Activa LCD y sprites: LCDC=0x83 (LCD on, sprites on, BG on)
  • Ejecuta 3 frames completos (70224 T-cycles cada uno)
  • Verifica framebuffer: busca píxeles non-white en bounding box del sprite (scanlines 20-27, columnas 30-37)
  • Validación: debe haber al menos 10 píxeles non-white en el bounding box

Métrica usada: "bounding box nonwhite" en lugar de "imagen exacta" (permite transparencias y variaciones de paleta)

Fase C - Métricas OAM en Herramienta Headless (Opcional)

Archivo: tools/rom_smoke_0442.py (modificado)

Método añadido: _sample_oam_nonzero():

  • Muestrea cada 4º byte en OAM (0xFE00-0xFE9F): 40 muestras
  • Cuenta bytes non-zero
  • Estima total multiplicando por 4

Actualizado _collect_metrics(): Incluye campo oam_nonzero en diccionario de métricas

Actualizado _print_summary(): Muestra estadísticas de OAM nonzero (mín, máx, promedio)

Actualizado dump periódico: Muestra oam_nz en output de frames

Decisiones de Diseño

  • Correctness > Timing: Enfoque en validar que DMA copia datos correctamente antes que timing exacto (640 T-cycles). Timing fino se puede añadir en step dedicado si es necesario.
  • Tests pequeños: Ambos tests son < 100 líneas, sin PNG, sin pygame. Ejecutan rápido y son fáciles de mantener.
  • Bounding box nonwhite: En lugar de validar imagen exacta del sprite, validamos que hay píxeles non-white en el bounding box esperado. Esto es más robusto y permite variaciones de paleta/transparencias.
  • Métricas OAM opcionales: Añadidas solo si cuesta poco (< 30 líneas), útil para diagnóstico futuro pero no crítico para este Step.

Archivos Afectados

  • tests/test_dma_oam_copy_0444.py (nuevo) - Test clean-room que valida DMA copia 160 bytes correctamente
  • tests/test_sprite_visible_0444.py (nuevo) - Test clean-room que valida sprite visible en framebuffer
  • tools/rom_smoke_0442.py (modificado) - Añadidas métricas OAM para diagnóstico futuro

Tests y Verificación

Comando ejecutado: pytest tests/test_dma_oam_copy_0444.py -v

Resultado: 2 passed in 0.24s

Comando ejecutado: pytest tests/test_sprite_visible_0444.py -v

Resultado: 2 passed in 0.45s

Comando ejecutado: pytest -q

Resultado: 537 passed (suite completa)

Validación de módulo compilado C++: ✅ Compilación exitosa, test_build.py pasa

Código del Test Clave

def test_dma_oam_copies_160_bytes():
    """Valida que DMA copia 160 bytes correctamente desde WRAM a OAM."""
    # Preparar patrón en WRAM (0xC000-0xC09F)
    source_base = 0xC000
    pattern = [i & 0xFF for i in range(160)]
    
    for i, byte_value in enumerate(pattern):
        mmu.write(source_base + i, byte_value)
    
    # Activar DMA: escribir source page (0xC0) a 0xFF46
    dma_source_page = 0xC0
    mmu.write(0xFF46, dma_source_page)
    
    # Verificar que DMA copió correctamente
    for i in range(160):
        expected = pattern[i]
        actual = mmu.read(0xFE00 + i)
        assert actual == expected, f"DMA copy falló en byte {i}"
def test_single_sprite_visible():
    """Valida que un sprite visible aparece en el framebuffer."""
    # Configurar OAM entry para sprite
    oam_addr = 0xFE00
    mmu.write(oam_addr + 0, 16 + 20)  # y = 36
    mmu.write(oam_addr + 1, 8 + 30)   # x = 38
    mmu.write(oam_addr + 2, 0x00)     # tile_id = 0
    mmu.write(oam_addr + 3, 0x00)     # flags = 0
    
    # Ejecutar 3 frames
    for frame in range(3):
        frame_cycles = 0
        while frame_cycles < CYCLES_PER_FRAME:
            cycles = cpu.step()
            ppu.step(cycles)
            timer.step(cycles)
            frame_cycles += cycles
    
    # Verificar framebuffer: píxeles non-white en bounding box
    framebuffer = ppu.get_framebuffer_rgb()
    nonwhite_count = 0
    for y in range(20, 28):
        for x in range(30, 38):
            idx = (y * 160 + x) * 3
            r, g, b = framebuffer[idx], framebuffer[idx+1], framebuffer[idx+2]
            if r < 200 or g < 200 or b < 200:
                nonwhite_count += 1
    
    assert nonwhite_count >= 10, f"Sprite no visible: solo {nonwhite_count} píxeles non-white"

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • DMA OAM Correctness: DMA copia 160 bytes desde source (value << 8) a OAM (0xFE00-0xFE9F). La implementación actual es instantánea (sin timing), pero la correctness de datos es correcta. Tests clean-room confirman que la copia funciona correctamente.
  • Sprite Rendering: El PPU consume OAM real durante OAM Search (Mode 2). Si OAM tiene datos válidos (Y/X dentro de rango, tile data presente), el sprite aparece en el framebuffer. Test clean-room confirma que sprites aparecen cuando OAM tiene datos.
  • Hardware Contracts Frozen: Los tests congelan el contrato hardware: (1) DMA copia correcta, (2) Sprites renderizan. Esto permite detectar regresiones futuras.

Lo que Falta Confirmar

  • Timing DMA: Timing exacto de DMA (640 T-cycles) y bloqueo de acceso a memoria durante DMA (excepto HRAM). Esto se puede añadir en step dedicado si es necesario para juegos específicos.
  • Sprite Priority: Prioridad de sprites (OBP flags) y orden de renderizado. Tests actuales usan flags=0x00 (prioridad normal).
  • Sprite Flip: Flip X/Y de sprites (flags bits). Tests actuales no validan flip.

Hipótesis y Suposiciones

Modelo DMA Instantáneo: Asumimos que DMA es instantánea (sin delay) para este Step. En hardware real, DMA tarda 640 T-cycles y bloquea acceso a memoria (excepto HRAM). Si juegos específicos (ej: Pokémon) requieren timing exacto, se puede añadir en step dedicado.

Bounding Box Nonwhite: Asumimos que validar "al menos 10 píxeles non-white en bounding box" es suficiente para confirmar que sprite es visible. Esto permite variaciones de paleta/transparencias sin requerir imagen exacta.

Próximos Pasos

  • [ ] Si Pokémon u otros juegos siguen raros después de este Step, atacar timing fino de DMA/locks en step dedicado
  • [ ] Validar sprite priority y flip X/Y con tests adicionales si es necesario
  • [ ] Usar métricas OAM de herramienta headless para diagnosticar problemas de sprites en ROMs reales