Game Boy Color VRAM DMA: Modeling GDMA and HBlank Transfers
Implement CGB VRAM DMA accurately by separating general-purpose and HBlank transfers, preserving bank state, and testing FF51-FF55 timing and status.
The Game Boy Color adds a hardware path for copying data into video RAM without asking the CPU to move every byte. The interface is exposed through FF51-FF55, commonly called HDMA1 through HDMA5. Despite the name, it supports two distinct modes: general-purpose DMA (GDMA), which transfers a programmed block in one operation, and HBlank DMA (HDMA), which transfers a 16-byte block during eligible horizontal blanking periods. An emulator that treats both as an immediate memory copy may produce correct final VRAM and still get CPU timing, video access, and status reads wrong.
These registers are CGB features, not generic DMG hardware. A physical CGB running a monochrome-compatible cartridge may be in compatibility mode, where CGB-only behavior is unavailable. Check the active emulated model and cartridge CGB flag before exposing the registers or enabling either mode.
Decode the register contract before scheduling work
FF51 and FF52 specify the source address, and FF53 and FF54 specify the VRAM destination. The low four address bits are ignored, so both ends are aligned to 16 bytes. Documented source regions include ROM and cartridge RAM at 0000-7FF0 or A000-DFF0; do not silently model unverified source regions as ordinary RAM. The destination is in 8000-9FF0. Only the documented destination address bits are significant, and the destination VRAM bank comes from the CGB’s VBK register.
Writing FF55 starts a transfer. Bits 0-6 encode the number of 16-byte blocks minus one, giving a range from one block through 128 blocks (16 through 2048 bytes). On write, bit 7 selects the mode: clear for GDMA, set for HBlank DMA. FF55 is also a status register when read, but its bit 7 meaning is counterintuitive: a set bit means no HBlank transfer is active, and a clear bit means one is active. A completed GDMA transfer is documented as reading back FF.
For example, copying 256 bytes from source 4000 to destination 8000 in CGB HBlank mode requires 16 blocks, so the programmed FF55 value is 80h OR (16 - 1), or 8Fh. The addresses and selected banks must remain valid for the whole operation:
DEF rVBK EQU $FF4F
DEF rHDMA1 EQU $FF51
DEF rHDMA2 EQU $FF52
DEF rHDMA3 EQU $FF53
DEF rHDMA4 EQU $FF54
DEF rHDMA5 EQU $FF55
; CGB mode only. Source ROM bank and destination VRAM bank are
; assumed to be selected and kept stable until all 16 blocks finish.
ld a, 1
ldh [rVBK], a ; Select VRAM bank 1 for the destination.
ld a, $40 ; Source $4000.
ldh [rHDMA1], a
xor a
ldh [rHDMA2], a
ldh [rHDMA3], a ; Destination $8000.
ldh [rHDMA4], a
ld a, $8F ; HBlank mode, 16 blocks: 256 bytes total.
ldh [rHDMA5], a
The example demonstrates the register programming shape, not a complete game routine. The code must execute in CGB mode, the source must refer to the intended mapped ROM/RAM bank, and the destination must fit the intended VRAM region. If a program switches a bank while the transfer is active, later blocks can read from or write to a different backing region than the one used by the first block.
GDMA is one CPU-stalling transfer
With bit 7 clear on the FF55 write, the entire requested transfer runs as one operation and the CPU remains stopped until it completes. This makes GDMA useful for a bounded copy when the program can afford the pause and the display timing allows it. It is not equivalent to a background copy that the emulator can finish whenever convenient.
GDMA blindly attempts its transfer even if the LCD controller is currently using VRAM. Software should schedule it with the display disabled, during VBlank, or for a sufficiently short block during HBlank, as appropriate to the workload and hardware reference. Starting a large transfer during active rendering can disturb the displayed frame. Model the CPU stall and the transfer’s effect on the video subsystem together; do not copy bytes instantly and then advance the emulated CPU as if nothing happened.
HBlank DMA is a resumable sequence of block windows
With bit 7 set, the engine transfers one 16-byte block during each eligible HBlank on visible lines. The CPU is stopped for the individual block transfer, then can execute in the spaces between blocks. Current Pan Docs states that no block is transferred during VBlank; the sequence resumes at visible line 0. If the program halts the CPU, HBlank DMA also pauses until CPU execution resumes. Those details matter to emulators that model HALT wake-up and PPU line progression.
Do not start HBlank DMA while STAT reports Mode 0 (HBlank). Starting it inside the window that should schedule the first block is an explicitly documented hazard. A robust game-side routine arms the transfer outside Mode 0 and lets the PPU reach the next eligible blanking interval. The destination VRAM bank and any banked source selection must not change before the transfer has completed.
HBlank mode’s FF55 readback supports progress and cancellation handling. While active, bit 7 reads clear and the lower bits report the remaining block count minus one. Software can terminate an active HBlank transfer by writing FF55 with bit 7 clear; after termination, bit 7 reads set and the lower bits retain the remaining-count encoding. A stopped transfer does not automatically rewrite HDMA1-4 to FF. Treat this as a state transition in an emulator, not a one-time start flag followed by a hidden memcpy.
Pan Docs also warns that a transfer stops prematurely if the destination overflows the allowed range, and explicitly marks the resulting register state as still under investigation. Avoid inventing a precise readback rule for this edge case. Keep the limitation visible in tests and implementation notes until a hardware-verified result is available.
Structure an emulator implementation as a state machine
Represent the transfer state explicitly: source and destination pointers, current block progress, selected mode, active or stopped status, remaining blocks, and the bank mappings captured or required by the transfer. Schedule bytes or transfer phases against the emulated machine clock and PPU mode transitions. Keep the VRAM bus-availability rules separate from the DMA engine so CPU reads and writes during a block cannot accidentally bypass the same ownership rules the renderer observes.
For GDMA, advance the DMA and CPU stall together according to the emulator’s cycle model. For HBlank DMA, trigger a block only at the correct PPU boundary, pause between blocks, and resume across visible lines rather than treating VBlank as another HBlank. Integrate interrupt and HALT behavior with the CPU scheduler; if an interrupt can wake a halted CPU, the DMA’s paused state must follow the hardware’s resulting execution state. Preserve the FF55 status semantics on start, completion, cancellation, and reset.
The transfer source may be banked cartridge ROM/RAM, while the destination is selected VRAM. A well-isolated memory-bus API helps prevent the DMA engine from using the CPU’s currently visible mapping by accident. At the same time, do not assume that the exact rules for every unusual source address or model revision are fully documented. Implement the documented address range first, test it, and record unresolved hardware questions rather than generalizing from one emulator’s internal behavior.
Validate with targeted ROMs and regression captures
Build a compact test matrix rather than relying on a game that happens to exercise the feature. Include GDMA and HBlank DMA at minimum and maximum block counts, non-aligned address writes, source-bank stability, both VRAM banks, completion status, cancellation with a nonzero remainder, and a transfer armed before versus during HBlank. Test visible-line progression across the last visible line into VBlank and back to line 0. Add HALT/resume cases and LCD-off behavior only when you have a hardware-backed expected result for the target model.
Use test ROMs whose expected behavior is documented and tied to specific hardware models. The Mooneye Test Suite distinguishes console families and SoC revisions; a pass on one CGB revision is not automatically proof for every handheld or a CGB-compatible GBA. Compare state and timing traces at FF51-FF55, PPU mode transitions, CPU stall periods, and destination VRAM bytes. Keep visual output captures too, because a timing error may appear only as a line-specific tile corruption or raster artifact.
Cross-check implementation references such as SameBoy as an additional diagnostic, not as the hardware specification. Emulator implementations encode their own tested model assumptions and may intentionally support approximations. When behavior is unresolved in the hardware documentation, mark the expected result as unknown and avoid turning one implementation’s choice into a universal fact.
The production-grade model is a clocked DMA engine with explicit mode-specific scheduling and readable status, covered by model-aware tests. A final VRAM comparison is necessary, but it is not enough: the CPU’s lost cycles and the PPU’s access window are part of the feature contract.
Related:
- Game Boy OAM DMA: Copy Timing, Bus Conflicts, and Emulator Accuracy
- Game Boy STAT and LYC Interrupts: Building Reliable Scanline Effects
Sources: