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
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_addra 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]parai=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 correctamentetests/test_sprite_visible_0444.py(nuevo) - Test clean-room que valida sprite visible en framebuffertools/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
- Pan Docs: DMA Transfer
- Pan Docs: OAM (Object Attribute Memory)
- Pan Docs: Sprite Rendering
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