Skip to content
RetrogamingDeep Dive Published Updated 7 min readViews unavailable

Game Boy OAM DMA: Copy Timing, Bus Conflicts, and Emulator Accuracy

Trace Game Boy OAM DMA from the FF46 register to sprite memory, and understand why CPU bus restrictions and timing belong in the emulated hardware model.

On Game Boy hardware, object attribute memory (OAM) stores the descriptors the LCD controller uses for sprites. Software can initiate an OAM DMA transfer by writing a source-page value to the DMA register at FF46. The hardware copies the 160-byte range beginning at that page boundary into OAM at FE00-FE9F. This is not equivalent to a normal CPU loop: the transfer has its own timing and changes what the CPU can access while it is in progress.

For an emulator, the key design question is not merely how to copy 160 bytes but how to model the bus-visible consequences. During DMA, the CPU cannot freely access OAM, and access to other address regions depends on the machine model and bus being used. Games exploit timing and ordering; a copy routine that produces the final bytes but ignores access restrictions can pass simple screenshots while failing software that polls status, updates memory mid-transfer, or depends on instruction timing.

Separate the register write from the transfer engine

Treat the FF46 write as a hardware event that schedules the transfer, rather than as an immediate host-language memcpy. The emulator should represent source-page selection, current byte or transfer phase, destination progress, and the CPU-access rules for the active hardware mode. Keep the PPU’s OAM ownership distinct from CPU access so rendering and DMA do not silently overwrite each other’s state.

The Game Boy Color adds speed-mode details: CPU and DMA timing do not scale identically to the LCD controller. Avoid translating a count of CPU instructions directly into a fixed number of host frames. Advance the transfer against emulated clock units and verify the exact model-specific rules against trusted hardware documentation and test ROMs.

Test edge cases, not only the final sprite list

Build small tests that start DMA from different source pages, trigger it at different scanline/PPU phases, access OAM during transfer, and vary normal versus double-speed operation on compatible hardware. Compare memory traces, interrupt timing, and rendered output against known-good emulator tests or hardware captures. When a title has missing or scrambled sprites, isolate DMA state from sprite evaluation limits and rendering order before changing the PPU.

Pan Docs is a community-maintained technical reference, not a manufacturer programming manual. Cross-check subtle timing claims with emulator test suites and source implementations, and label unresolved revision-specific behavior rather than presenting a guess as universal Game Boy behavior.

What the register value means

FF46 is both a source-page selector and the start trigger. If software writes 0xC0, the source range begins at 0xC000; the DMA unit reads the 160 bytes from 0xC000 through 0xC09F and copies them to 0xFE00 through 0xFE9F. The low byte of the source address is not programmable through FF46. The documented source-page range is 0x00 through 0xDF; do not silently treat every high byte in the 16-bit address space as a valid source.

OAM has 40 entries of four bytes each, so the transfer covers 160 bytes total. The data is the hardware’s object attribute table: Y and X positions, tile index, and attributes such as palette, flip, and priority. DMA copies bytes; it does not choose sprites for a scanline or render them. A title with missing objects may therefore involve the transfer, OAM contents, scanline selection, or pixel composition. Keep those stages observable separately.

Pan Docs gives the transfer as 160 machine cycles, equivalent to 640 dots at normal speed and 320 dots at CGB double speed. These are emulated-clock quantities, not a host delay. A correct emulator advances the DMA engine alongside CPU and PPU timing, including the different relationship between CPU double speed and the LCD dot clock. Modeling it as a synchronous copy followed by a pause can create the right final OAM but the wrong intermediate bus behavior and interrupt or instruction ordering.

Bus access is model-dependent

During OAM DMA on an original DMG, the CPU is restricted to high RAM (0xFF80 through 0xFFFE) while the transfer runs. This is why real programs use a short routine copied to HRAM to start the transfer and wait for it to finish. If an emulator blocks all CPU accesses indiscriminately, however, it will also break the waiting routine. If it leaves all memory accessible, software can read or execute from areas that the original bus arbitration made unavailable.

CGB behavior should not be collapsed into the DMG rule. The cartridge bus and WRAM bus allow some CPU accesses while DMA reads from the other bus. Which area remains accessible depends on the transfer source. Pan Docs also documents PPU-visible consequences when DMA overlaps OAM search or pixel transfer: sprite evaluation can observe unavailable or changing OAM data. There are revision-specific details and PPU-mode edge cases, so an emulator should follow targeted tests and documented model behavior rather than a blanket rule copied from one hardware revision.

Keep at least three pieces of state explicit in an emulator: the source page and current transfer position, the scheduled timing of the next byte or transfer step, and the memory-access restrictions active for the selected hardware model. A state save taken during DMA must preserve enough of this state to resume consistently. Otherwise a save/load test may pass at frame boundaries but diverge when loading in the middle of a transfer.

Build a timing-oriented test matrix

A robust test suite should distinguish final-copy correctness from bus and PPU timing. Start with source pages in cartridge ROM and WRAM whose bytes are deliberately distinct, then verify all 160 destination bytes and boundary values at offsets 0 and 159. Test writes to FF46 during active transfer only according to documented behavior and test-ROM evidence; do not invent restart or queue semantics if the reference material does not establish them.

Test dimension Useful observation Failure it can expose
Source page and byte pattern First, last, and alternating destination bytes Off-by-one range or incorrect source-page decode
DMG CPU execution HRAM routine completes while restricted regions are unavailable Immediate memcpy or overly broad CPU lockout
CGB source bus Access from the other bus follows the documented model Applying DMG restrictions to every model
Normal and double speed DMA duration is measured in dots and CPU cycles separately Scaling CPU cycles as if LCD timing also doubled
PPU mode at start OAM search/render output reflects overlap at tested phases Correct final bytes but incorrect sprite timing
Mid-transfer save state Reload resumes with matching data and remaining duration DMA state omitted from snapshots

Use known hardware tests such as the Pan Docs-referenced test ROM ecosystem when available, and compare more than screenshots. A screenshot is insensitive to a short CPU bus window that changes one game-state read. Compare the CPU’s observable memory reads, DMA completion point, PPU mode transitions, and OAM bytes. When a test only says pass/fail, preserve the exact ROM revision and emulator commit so later maintainers can rerun it.

For real hardware capture, record console family and revision, cartridge or source page, display state, clock mode, and how the transfer was triggered. A result from a DMG cannot be assumed to validate CGB double-speed behavior. If a test fails only during a visual scanline boundary, first isolate DMA/PPU contention from the separate per-line object-selection limit; changing sprite limits to conceal an OAM timing defect only moves the symptom.

Keep the implementation testable and honest

An event-driven DMA implementation is easier to inspect than a one-shot copy. Give the transfer a start time and a documented completion time in the emulated clock domain, advance progress at the hardware’s granularity, and make bus ownership queries depend on both model and phase. The exact internal granularity can be an implementation choice, but it must preserve every externally observable point relevant to software and tests. Avoid host sleeps or frame-based approximations: host scheduling is unrelated to a Game Boy’s DMA bus.

When hardware documentation leaves a revision-specific corner unresolved, encode the best-supported behavior for the named model, add a test documenting the evidence, and state the uncertainty in the emulator’s notes. Cycle accuracy is not a license to claim certainty beyond the measurements. The practical goal is a model that passes reproducible timing tests and explains which assumptions remain open.

Related:

Sources:

Comments