Skip to content
RetrogamingDeep Dive Published Updated 7 min readViews unavailable

SNES HDMA Raster Effects: Tables, Line Counters, and Safe Updates

Understand SNES horizontal-blank DMA table entries, transfer patterns, channel state, and the VBlank discipline needed for deterministic raster effects.

The Super Nintendo Entertainment System’s horizontal-blank DMA, usually called HDMA, can update selected memory-mapped registers at scanline boundaries without requiring the CPU to execute a precisely timed loop for every line. A channel reads a compact table from the A bus and transfers data to or from a B-bus register according to a configured pattern. This makes effects such as per-line scrolling, color gradients, and layer-window changes practical, but the mechanism is stateful: channel registers, table position, line counter, transfer mode, and frame timing all interact.

The most common failure is not an incorrect color value. It is enabling a channel after its state should have been initialized, editing the active table, or treating each table entry as an independent one-line command. Those mistakes can create a malformed frame whose symptoms look like random PPU register corruption. Design the table and its lifetime as deliberately as the register values it contains.

HDMA is a per-channel state machine

The SNES exposes eight DMA channels, and the channel register sets are shared between general-purpose DMA and HDMA. The HDMA enable register selects which channels participate. Each selected channel has a source/table address, a B-bus destination address, a transfer pattern, and state used to track the current table position and remaining scanlines. Indirect HDMA additionally uses an indirect data address; the table can then hold pointers instead of every output value inline.

HDMA performs a small transfer in the horizontal blanking interval. It does not suspend the CPU for an entire arbitrary-length copy in the same way as starting a normal DMA transfer. Normal DMA and HDMA do share hardware resources and timing, however, and the SNESdev documentation notes interactions that make careless concurrency unsafe. Build a timing plan for any frame that combines large DMA work and active HDMA.

Interpret the line-count byte correctly

Each table entry starts with a line-count/repeat byte. A zero terminates processing for that channel for the rest of the frame. Values in the lower range describe a nonrepeating entry: the configured pattern is written once, then the controller waits the encoded number of scanlines before processing the next entry. Values with the high bit set describe repeating entries: the pattern is written on successive scanlines for the specified run. The value is therefore both a count and a behavior flag, not a plain delay.

The first table entry is processed at the end of scanline 0. That origin is a frequent source of off-by-one errors: a table count is not automatically the same as the visible line number on which the next value appears. When a new entry is meant to affect visible line N, calculate its predecessor’s duration from the documented end-of-line behavior, then verify with a test pattern that assigns a distinct color to each region.

An illustrative direct-mode layout is:

line_count, data_for_selected_register_pattern
line_count, data_for_selected_register_pattern
0                         ; end of this channel's table

The number of data bytes following an entry depends on the channel’s transfer pattern. A one-register pattern carries one byte per write; multi-register patterns write a sequence to consecutive or repeated B-bus addresses according to the mode. Do not generate a byte array without also deriving the mode’s expected transfer width.

Match transfer patterns to register semantics

The DMAP transfer-pattern field controls how B-bus addresses advance and how many bytes the pattern moves. This matters for PPU registers that expect paired or repeated writes. For example, a scroll position may use a two-write register latch, while palette and VRAM ports have their own address/data sequencing. A transfer can have the right number of bytes yet still be wrong if it targets the wrong port order or omits a required latch write.

Before implementing a visual effect, create a small table mapping each output byte to its intended B-bus address and the PPU’s write semantics. The SNESdev register tables and HDMA examples identify suggested patterns and warn about write-twice/read-twice registers. For more complex effects, test the transfer in isolation before combining it with scrolling, windows, color math, or other DMA channels.

Direct and indirect tables solve different problems

In direct mode, the table contains the actual byte or bytes to write. It is easy to inspect and suitable when the table is modest. In indirect mode, each table entry points to a separate data stream, allowing large or shared value arrays without embedding every value beside the line-control bytes. Indirect mode brings another piece of state to validate: the bank component of the indirect address is configured separately, and the automatic low-word behavior does not remove that responsibility.

Choose the representation from memory layout and update needs, not only from source-code convenience. A dynamic per-line gradient may be easier to build in a buffer and double-buffer than to edit in place. A small fixed effect can remain in ROM. An indirect table can make the control program compact while data is generated elsewhere, but the data buffer still needs correct placement and lifetime.

Initialize and update during VBlank

The controller initializes active HDMA channel state around the beginning of scanline 0. The safe operational rule is to configure the table and channel registers, and enable channels, during vertical blank while the screen is active. Enabling a channel too late can leave table position and state inconsistent; changing a table while the channel is consuming it can produce torn or mixed-frame values. The documented recommendation is to modify active HDMA state only during VBlank and to double-buffer larger dynamic tables.

A reliable frame pipeline is:

  1. During the visible frame, render using the current immutable HDMA table.
  2. Build the next frame’s values in an inactive buffer.
  3. During VBlank, finish the buffer, update source/channel state, and select the next table.
  4. Enable or retain the intended channel set before the next scanline-0 initialization point.
  5. Do not write the active table or its live channel registers during display time.

This is a lifecycle outline, not a complete 65C816 routine. Exact instruction scheduling, interrupt structure, and register setup must follow the target program’s memory map and assembler. If the table is small, updating it in VBlank may be simpler than maintaining two copies; if the table is large or its construction can exceed blanking time, prepare it earlier and swap buffers during VBlank.

Budget channels and blanking work together

HDMA is not free. Each channel consumes part of a constrained blanking interval, with data transfer width and timing affecting how much remains for other channels and CPU work. Avoid assuming that eight enabled channels with multi-byte patterns will always fit comfortably. Count the pattern bytes and active lines, consider direct versus indirect fetches, and measure the complete frame on the intended hardware or a validated emulator.

Normal DMA introduces a separate concern. The SNESdev DMA reference describes a revision-specific risk when a normal DMA completes at the moment HDMA begins; it recommends restricting DMA to VBlank, disabling HDMA during the operation, or using a proven fixed-size/aligned schedule. This is an example of why a timing-safe design should state its assumptions about hardware revision, active channels, and transfer duration instead of treating DMA as an isolated copy primitive.

Build tests that expose table errors

Start with one channel writing a harmless, clearly visible register such as a background-layer enable or a controlled scroll value. Use short table segments with visibly different outputs. Test the zero terminator, a single nonrepeating entry, a repeating entry, and transitions between entries. Confirm which visible line receives each value, and trace the channel’s table address and line counter at scanline boundaries.

Then test buffer switching under load. Intentionally vary the next frame’s table values so stale data is obvious. Verify that no frame contains a mixture of old and new values. Add other channels one at a time, then add normal DMA and CPU work. When a glitch appears, log HDMA enable state, channel transfer mode, table address, current line counter, indirect address if present, and the exact scanline where the result changes.

SNESdev Wiki is a community-maintained reverse-engineering and development reference rather than an original Nintendo programming manual. Its register descriptions and examples are unusually useful, but revision-sensitive behavior should be described with that qualifier and corroborated by hardware tests or trusted emulator test suites before relying on it for exact timing guarantees.

Related:

Sources:

Comments