⚠️ Clean-Room / Educational

This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.

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

Date:2026-01-02 StepID:0444 State: VERIFIED

Summary

End-to-end validation of OAM DMA (0xFF46): creation of clean-room tests that verify that DMA correctly copies 160 bytes from source to OAM, and that the PPU consumes real OAM (sprites appear when OAM has data). Added 2 main tests that freeze the hardware contract: (1) DMA copy correct, (2) Sprites render (at least 1 sprite visible). Optionally, added OAM metrics to headless tool for future diagnosis.

Hardware Concept

DMA (Direct Memory Access) OAM Transfer (0xFF46): The Game Boy includes a DMA mechanism that allows data to be copied to the OAM (Object Attribute Memory) without direct intervention from the CPU. Write a valueXXin0xFF46initiates a transfer that copies 160 bytes from the addressXX00until0xFE00-0xFE9F(OAM).

On real hardware, the transfer takes approximately 160 microseconds (640 T-cycles), and during this time the CPU can only access HRAM (0xFF80-0xFFFE). However, for this Step we focus on thedata correctness(that the copy is correct) before the exact timing.

OAM (Object Attribute Memory): The OAM contains data for up to 40 sprites. Each sprite occupies 4 bytes:

  • Byte 0: Y position (0-255, offset 16 for display)
  • Byte 1: X position (0-255, offset 8 for display)
  • Byte 2: Tile ID (0-255, tile index in VRAM)
  • Byte 3: Flags (palette, flip X/Y, priority, etc.)

Sprite Rendering: The PPU reads OAM during OAM Search mode (Mode 2) to determine which sprites are visible in the current scanline. If OAM has valid data (Y/X within range, tile data present in VRAM), the sprite should appear in the framebuffer.

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

Implementation

Previous Audit: Verified that DMA OAM (0xFF46) already exists insrc/core/cpp/MMU.cpplines 967-1004. The current implementation:

  • Detect writing to0xFF46
  • Calculatesource_base = value<< 8
  • Runs loop that copies 160 bytes fromsource_addrto OAM (0xFE00-0xFE9F)
  • Useread(source_addr)to read (correct: can be ROM/VRAM/WRAM)
  • Write directly tomemory_[0xFE00 + i](correct: special bypass according to Pan Docs)
  • Model: instantaneous (no delay/timing)

Conclusion: Correct basic implementation. We need to validate it with clean-room tests.

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

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

Test 1:test_dma_oam_copies_160_bytes():

  • Prepare incremental pattern in WRAM (0xC000-0xC09F): 0x00, 0x01, 0x02, ..., 0x9F
  • Verify that OAM is clean (0xFE00-0xFE9F = 0)
  • Activate DMA: write source page (0xC0) to 0xFF46
  • Verify that DMA copied correctly:mem[0xFE00+i] == pattern[i]fori=0..0x9F
  • Verify that source has not changed (DMA is read-only on source)

Test 2:test_dma_oam_from_different_source():

  • Validate that DMA works from different sources (alternative WRAM 0xD000)
  • Different pattern:0xAA+(i & 0x0F)
  • Verify correct copy

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

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

Test:test_single_sprite_visible():

  • Load tile data for sprite (Tile ID 0x00, address 0x8000): checkerboard pattern (0xAA/0x55 alternating)
  • Set OAM entry for sprite: y=36 (scanline 20), x=38 (column 30), tile_id=0x00, flags=0x00
  • Activate LCD and sprites: LCDC=0x83 (LCD on, sprites on, BG on)
  • Run 3 full frames (70224 T-cycles each)
  • Check framebuffer: look for non-white pixels in the sprite's bounding box (scanlines 20-27, columns 30-37)
  • Validation: there must be at least 10 non-white pixels in the bounding box

Metric used: "bounding box nonwhite" instead of "exact image" (allows transparencies and palette variations)

Phase C - OAM Metrics in Headless Tool (Optional)

Archive: tools/rom_smoke_0442.py(modified)

Added method:_sample_oam_nonzero():

  • Samples every 4th byte in OAM (0xFE00-0xFE9F): 40 samples
  • Non-zero byte count
  • Estimate total by multiplying by 4

Updated_collect_metrics(): Includes fieldoam_nonzeroin metrics dictionary

Updated_print_summary(): Show nonzero OAM statistics (min, max, average)

Updated periodic dump: Sampleoam_nzin frame output

Design Decisions

  • Correctness > Timing: Focus on validating that DMA copies data correctly before exact timing (640 T-cycles). Fine timing can be added in dedicated step if necessary.
  • Small tests: Both tests are< 100 líneas, sin PNG, sin pygame. Ejecutan rápido y son fáciles de mantener.
  • Bounding box nonwhite: Instead of validating the exact sprite image, we validate that there are non-white pixels in the expected bounding box. This is more robust and allows for palette/transparency variations.
  • Optional OAM Metrics: Added only if it costs little (< 30 líneas), útil para diagnóstico futuro pero no crítico para este Step.

Affected Files

  • tests/test_dma_oam_copy_0444.py(new) - Clean-room test that validates DMA copies 160 bytes correctly
  • tests/test_sprite_visible_0444.py(new) - Clean-room test that validates visible sprite in framebuffer
  • tools/rom_smoke_0442.py(modified) - Added OAM metrics for future diagnostics

Tests and Verification

Command executed: pytest tests/test_dma_oam_copy_0444.py -v

Result: 2 passed in 0.24s

Command executed: pytest tests/test_sprite_visible_0444.py -v

Result: 2 passed in 0.45s

Command executed: pytest -q

Result: 537 passed(full suite)

C++ Compiled Module Validation: ✅ Build successful, test_build.py passes

Key Test Code

def test_dma_oam_copies_160_bytes():
    """Validates that DMA correctly copies 160 bytes from WRAM to OAM."""
    # Prepare pattern in 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)
    
    # Enable DMA: write source page (0xC0) to 0xFF46
    dma_source_page = 0xC0
    mmu.write(0xFF46, dma_source_page)
    
    # Verify that DMA copied correctly
    for i in range(160):
        expected = pattern[i]
        current = mmu.read(0xFE00 + i)
        assert current == expected, f"DMA copy failed on byte {i}"
def test_single_sprite_visible():
    """Validates that a visible sprite appears in the framebuffer."""
    # Configure OAM entry for 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
    
    # Run 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 not visible: only {nonwhite_count} non-white pixels"

Sources consulted

Educational Integrity

What I Understand Now

  • DMA OAM Correctness: DMA copies 160 bytes from 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: The PPU consumes real OAM during OAM Search (Mode 2). If OAM has valid data (Y/X within range, tile data present), the sprite appears in the framebuffer. Test clean-room confirms which sprites appear when OAM has data.
  • Hardware Contracts Frozen: Tests freeze hardware contract: (1) DMA copy correct, (2) Sprites render. This makes it possible to detect future regressions.

What remains to be confirmed

  • DMA Timing: Exact DMA timing (640 T-cycles) and memory access blocking during DMA (except HRAM). This can be added in dedicated step if needed for specific games.
  • Sprite Priority: Sprite priority (OBP flags) and rendering order. Current tests use flags=0x00 (normal priority).
  • Sprite Flip: Flip X/Y of sprites (flags bits). Current tests do not validate flip.

Hypotheses and Assumptions

Instant DMA Model: We assume that DMA is instantaneous (without delay) for this Step. On real hardware, DMA takes 640 T-cycles and blocks memory access (except HRAM). If specific games (e.g. Pokémon) require exact timing, it can be added in a dedicated step.

Bounding Box Nonwhite: We assume that validating "at least 10 non-white pixels in bounding box" is enough to confirm that sprite is visible. This allows palette/transparency variations without requiring exact image.

Next Steps

  • [ ] If Pokémon or other games are still weird after this Step, attack fine timing of DMA/locks in dedicated step
  • [ ] Validate sprite priority and flip X/Y with additional tests if necessary
  • [ ] Use headless tool OAM metrics to diagnose sprite issues in real ROMs