GBA Affine Backgrounds: Fixed-Point Matrices and Scanline Reference Accumulators
Implement Game Boy Advance affine backgrounds correctly: 8.8 transform steps, signed reference points, per-scanline accumulators, pivots, and wrap behavior.
The Game Boy Advance does not rotate an affine background by transforming each tile or moving a finished bitmap. Its display engine walks through source coordinates using four signed fixed-point increments and two reference-point accumulators. For every output pixel it advances a source X/Y position; at each scanline it advances the line origin by a different pair of increments. The visible transform is therefore a stream of address-generation steps, not an abstract host-side matrix operation that can be applied after rendering.
That distinction matters for emulator accuracy. The parameters are written to memory-mapped registers, the source origin is a signed fixed-point value, the hardware maintains internal scanline state, and out-of-range samples may wrap or become transparent depending on the background mode and control bit. A renderer that gets the matrix direction right but misses the reference origin or its update timing can still draw the wrong map region.
Which backgrounds can use the affine path?
Only BG2 and BG3 use the classic affine background registers. In video mode 1, BG2 may be affine while BG3 is disabled; in mode 2, both BG2 and BG3 may be affine. BG2 also provides the rotation/scaling path for bitmap modes 3-5. The regular text backgrounds have their own horizontal and vertical offset registers; those HOFS and VOFS registers are ignored for affine and bitmap backgrounds, which instead use the reference-point registers.
For BG2, the matrix registers are BG2PA, BG2PB, BG2PC, and BG2PD at I/O addresses 0x04000020-0x04000026; BG2X and BG2Y are 32-bit write registers at 0x04000028 and 0x0400002C. BG3 has the corresponding set at 0x04000030-0x0400003C. The X/Y fields occupy 28 meaningful bits: eight fractional bits, nineteen magnitude/integer bits, and a sign bit. The unused upper four bits of a 32-bit write are ignored by the hardware model in GBATEK. Keep a software shadow of these write-only registers if later code needs the current state.
Classic affine tile maps also differ from text maps: the affine map is a flat array of one-byte tile indices, uses 256-color tiles, and can be 16×16, 32×32, 64×64, or 128×128 tiles. There are no per-entry palette-bank or tile-flip fields as in a regular text map. A renderer that reuses its text-map decoder will interpret map bytes and tile attributes incorrectly even if its transform math is perfect.
Interpret the transform as source-coordinate increments
The four matrix entries are signed 8.8 fixed-point numbers. The integer value 0x0100 represents 1.0 source pixels per step; 0x0080 represents 0.5; and 0x0200 represents 2.0. The hardware maps display coordinates into background source coordinates, which is the inverse direction from the intuitive “move the source into screen space” operation many graphics APIs describe. This inverse mapping is why a positive source-space scale can make an image appear smaller on screen.
Let (u, v) be the source coordinate of the upper-left output pixel, in units of 1/256 pixel. For each visible output pixel, the source-coordinate accumulators advance as:
u += PA v += PC
After each scanline, the origin for the next line advances as:
u_line += PB v_line += PD
Equivalently, ignoring fixed-point shifts and boundary details, the sampled source position is:
source_x(x, y) = reference_x + PA*x + PB*y
source_y(x, y) = reference_y + PC*x + PD*y
The column increments PA/PC describe movement within one horizontal output row. The row increments PB/PD describe how the source origin changes from one output row to the next. Do not swap them because a library names them dmx/dmy, and do not update X and Y with the same pair. A pure horizontal scroll with identity scale has PA=0x0100, PD=0x0100, PB=PC=0; rotation introduces cross-axis terms in PB and PC.
The actual display unit uses fixed-width arithmetic. Keep intermediate accumulators at least 32 bits, preserve signed interpretation, and apply hardware-compatible truncation when choosing the source texel. Avoid converting every increment to floating point and rounding at each step: repeated rounding can accumulate visible drift along a long scanline or down a frame. In an emulator, keep the subpixel fractional bits in the running state and discard them only when deriving the sampled texel address.
The reference point is the source location for the screen origin
BG2X/BG2Y and BG3X/BG3Y specify the background coordinate mapped to the top-left of the display surface. They do not specify a world point that should automatically land at the center of the screen. This is the main pivot trap: loading a desired map point directly into the reference registers puts that point at screen (0,0), not at an arbitrary on-screen rotation center.
To keep a chosen source point p0=(px, py) fixed at a chosen screen point q0=(qx, qy), calculate the reference origin as:
reference_x = px - (PA*qx + PB*qy) / 256
reference_y = py - (PC*qx + PD*qy) / 256
When p0 and q0 are represented in the same fixed-point units, do not divide the products until the final representation step; the formula above writes /256 to expose the 8.8 scale. A common implementation error is to multiply by a matrix derived for source-to-screen coordinates, then load it into the GBA’s screen-to-source registers as-is. Derive the inverse transform you need, or use a library routine whose coordinate convention is explicit.
Scaling also changes where a point appears unless the reference origin is corrected. For a centered zoom, compute the transformed source location of the chosen screen center and adjust the reference to compensate. Test identity, 2× magnification, and 1/2× magnification with a marked cross at the intended anchor. A checkerboard alone may make a half-pixel offset look like a harmless scale artifact; a single colored anchor pixel exposes it immediately.
Internal scanline accumulators and mid-frame writes
During vertical blank, the GBA copies the programmed X/Y reference registers into internal accumulators for the next frame’s first visible line. After each scanline, those internal values advance by PB and PD. This is why an emulator should keep separate programmed-register state and rendering-accumulator state. Reconstructing every line from a possibly updated reference register can erase the hardware’s frame/scanline behavior.
Writing BGxX or BGxY outside VBlank has a visible side effect: GBATEK documents that the newly written value is immediately copied into the corresponding internal reference for the current scanline. It therefore becomes the origin for the current line, not necessarily the next frame’s top line. Software can use this to create raster effects, but it also means a write during an HBlank-sensitive sequence must be modeled at the correct event boundary. A frame-based renderer that applies all register changes only at VBlank will miss such effects.
The reference registers are write-only in the hardware map. Software normally tracks the programmed values in ordinary RAM and writes them as a group during VBlank. That strategy avoids accidental tearing and preserves the full reference value despite the register’s unreadable status. For raster effects, intentionally schedule mid-frame writes and retain both the last programmed value and current internal accumulator in the emulator state.
Map overflow and transparent samples
In affine tile modes, BGxCNT controls the background size and the area-overflow behavior. When the transform samples outside the selected map, the engine either wraps the coordinate around the map or treats the pixel as transparent, according to the overflow/wrap bit. This decision belongs to the affine background fetch path, before normal layer composition. A transparent outside sample should allow lower-priority backgrounds or the backdrop to show through; it is not equivalent to wrapping into tile zero unless wrapping is enabled.
The bitmap modes are a separate case. Although BG2 uses the affine parameters in modes 3-5, the classic tile-map overflow flag does not make bitmap coordinates wrap. Samples outside the bitmap are transparent. Treating the tile-mode wrap bit as universal can create a repeated bitmap edge that real hardware does not show.
For tile backgrounds, transform fixed-point source coordinates first, then apply the chosen map size and wrap/clipping policy. When wrapping is active, wrap in pixel/map space at the documented background dimensions before converting to tile index and tile-local coordinates. Applying % tile_count to a signed tile index without normalizing negative coordinates can produce language-dependent results; implement explicit signed wrap for negative values. When wrapping is off, detect out-of-range samples before reading tile or palette memory.
A stable transform setup
This C-like example uses an identity matrix and sets a source reference point at (32, 16) pixels for BG2. It assumes a valid affine map and tile data are already in VRAM, a supported video mode is selected, BG2 is enabled, and the program updates the registers while the display is in VBlank.
#include <stdint.h>
#define REG16(addr) (*(volatile uint16_t *)(addr))
#define REG32(addr) (*(volatile uint32_t *)(addr))
#define BG2PA REG16(0x04000020)
#define BG2PB REG16(0x04000022)
#define BG2PC REG16(0x04000024)
#define BG2PD REG16(0x04000026)
#define BG2X REG32(0x04000028)
#define BG2Y REG32(0x0400002C)
static uint32_t encode_bg_ref_q8(int32_t coordinate_q8)
{
/* Caller must keep the value in the signed 28-bit representable range. */
return ((uint32_t)coordinate_q8) & 0x0FFFFFFFu;
}
static void set_bg2_identity_at(int32_t source_x_q8, int32_t source_y_q8)
{
BG2PA = 0x0100; /* 1.0 source pixels per screen-pixel step */
BG2PB = 0x0000;
BG2PC = 0x0000;
BG2PD = 0x0100;
/* X/Y use signed 8-fraction-bit reference coordinates. */
BG2X = encode_bg_ref_q8(source_x_q8);
BG2Y = encode_bg_ref_q8(source_y_q8);
}
/* Example: set_bg2_identity_at(32 * 256, 16 * 256); */
The arguments and helper use Q8 register units, not integer pixels; validate that each coordinate fits the signed 28-bit range before encoding. The snippet is meant to make the register layout and units visible; do not copy it as a complete driver without the target SDK’s fixed-point types and VBlank synchronization.
Emulator conformance tests
Begin with the identity matrix and a coordinate-labeled tile map. Check that the source point at the reference register appears at the output’s top-left, then set PA or PD to 0x0200 and verify that output advances two source pixels for each screen pixel along the selected axis. Use PB and PC separately to confirm that the origin shifts between scanlines. These tests isolate matrix direction and coefficient routing before any rotation math is involved.
Next add fractional increments such as 0x0080 and negative steps. Verify that two 0.5 steps sample adjacent source pixels only after the retained fraction crosses a whole-pixel boundary. Test signed values near zero and the 28-bit reference sign boundary, including high unused bits on writes. Use guard regions around map data so an incorrect sign-extension or wrap operation becomes a detected invalid fetch instead of a plausible neighboring tile.
For pivot tests, render a unique marked source point, apply scale and rotation around a non-central anchor, and confirm the mark remains at the requested screen coordinate. Compare the software equation against the emulator’s per-pixel accumulator. This catches both matrix transposition and reference-offset errors.
Finally test VBlank reload, scanline increments, and a mid-frame reference write as separate event traces. Record the programmed X/Y values, internal accumulators before and after each line, active map size, wrap bit, and output sample. Repeat in tile-affine mode and bitmap mode to verify their different boundary rules. A correct static screenshot is not enough to validate register timing; a one-line raster test is the fastest way to expose a frame-accumulator bug.
The GBA affine engine is best implemented as fixed-point source-coordinate stepping with frame and scanline state. Treat the matrix as screen-to-source increments, use X/Y as the origin mapped to the screen’s top-left, preserve subpixel and signed bits, and apply the right map-boundary rule for the selected mode. With those decisions explicit, affine scrolling, rotation, zoom, and raster effects become deterministic hardware behavior rather than a best-effort image transform.
Related:
- Game Boy Advance Windows and Blending: Layer Masks, Priority, and Pixel Rules
- SNES Mode 7: Affine Transforms, Pivot Registers, and Raster Effects
Sources: