Skip to content
RetrogamingDeep Dive Published Updated 8 min readViews unavailable

Game Boy Advance Affine OBJ: OAM Matrix Slots, Fixed-Point Mapping, and Double Size

Decode GBA affine OBJ state across OAM attributes and interleaved PA/PB/PC/PD slots, then map pixels with signed 8.8 coefficients.

Game Boy Advance affine objects reuse ordinary OBJ attributes and tile data, but their transform coefficients live in the otherwise unused halfword after each OAM entry. Four signed 8.8 values form a shared matrix; each affine OBJ selects one of 32 matrix groups through bits in its second attribute. Rendering then scans a screen-space bounding rectangle and uses the matrix to find a source texel. The bounding rectangle, source dimensions, reference point, and matrix are separate inputs.

That layout creates bugs that are easy to misdiagnose as bad sprite art. A correct coefficient array at the wrong OAM stride produces an unrelated transform. A valid matrix assigned to the wrong index looks like a tile corruption. A regular OBJ and an affine OBJ also interpret some Attribute 1 bits differently: in affine mode, the matrix number occupies positions used for horizontal and vertical flip flags in regular mode. Emulator code must decode the attribute mode before interpreting those bits.

OAM stores entries and matrix parameters together

The GBA has 128 OAM entries, each occupying eight bytes. The first three halfwords hold OBJ Attribute 0, Attribute 1, and Attribute 2. The fourth halfword is not an independent per-object setting; it is one element in the shared affine-parameter table. Every four such parameter halfwords, spaced one OAM-entry apart, make a matrix group in the order PA, PB, PC, PD.

For matrix group zero, the coefficients occupy OAM offsets 0x06, 0x0E, 0x16, and 0x1E. Group one begins at 0x26, 0x2E, 0x36, and 0x3E. The pattern continues for 32 groups. This interleaving lets the hardware use attribute slots from multiple OBJ entries as matrix storage. It also means software that initializes all OAM entries as an array of three halfwords and assumes a contiguous four-element matrix will write the wrong memory.

Attribute 0 bit 8 enables affine processing. With affine processing enabled, bit 9 selects the double-size bounding mode; without affine mode, bit 9 disables the object. Attribute 1 bits 9 through 13 select the matrix number in affine mode, while the normal non-affine mode uses its flip fields instead. Attribute 1’s size bits and Attribute 0’s shape bits still determine the source object’s dimensions. Decode this combination into explicit fields before building the renderer’s per-line sprite state.

The matrix coefficients are signed 16-bit fixed-point values with eight fractional bits. 0x0100 represents 1.0, 0x0080 represents 0.5, and 0x0200 represents 2.0. The four values are not four independent per-axis settings: they define a 2-by-2 matrix. Off-diagonal terms represent rotation or shear, while diagonal terms participate in scaling. Fixed-point truncation and signed arithmetic are observable at pixel boundaries, so use an explicitly signed type and arithmetic with a defined rounding policy.

Rendering maps destination pixels back to source texels

The PPU has to decide which source texel supplies each pixel in the transformed screen rectangle. A useful mathematical representation is:

source_delta = [[PA, PB], [PC, PD]] * screen_delta / 256

In this scan-conversion direction, the P matrix maps a destination-space offset to a source-space offset. It should not be confused with the forward matrix a game developer might use to describe where source pixels ought to move. If code starts from a desired forward transform, it generally needs the corresponding inverse coefficients for the PPU’s sampling path. Tonc’s worked examples derive those coefficients and discuss the center-relative anchor; mGBA’s software renderer likewise accumulates matrix terms while traversing output pixels.

The transform origin is centered on the object rather than the upper-left source texel. The renderer must therefore subtract the bounding-box center from the destination coordinate, apply the signed fixed-point matrix, and add the source-image center. The exact integer center convention matters for even dimensions because there is no pixel exactly at the geometric half-pixel point. A one-pixel discrepancy can appear after rotating or mirroring a sprite even though the matrix is mathematically correct.

Double-size mode changes the screen-space canvas, not the source bitmap’s width and height. It doubles the bounding-box dimensions so rotated or sheared corners have room to appear. The renderer still samples the original source dimensions and must treat coordinates outside them as transparent or clipped according to the PPU behavior. Applying the matrix to a doubled source texture instead changes tile addressing and causes characteristic duplicated or displaced art.

Double-size also changes the screen-space center reference because the destination canvas has grown. A sprite may shift by half its original size when the mode is toggled if software does not compensate its position. A useful test rotates a square object through quarter turns with and without double-size, then compares its center and visible bounds. Do not “fix” this by moving the sprite anchor for every matrix; separate OAM X/Y position from the PPU’s center and bounding-box arithmetic.

Matrix selection is shared state, not object-local state

An affine OBJ reads the matrix group named in its Attribute 1 index. Several objects may select the same group and therefore share all four coefficients. Updating that group can affect every visible OBJ that references it. This is a hardware feature, not necessarily a bug: games can rotate a multipart character or several repeated objects using one matrix. But it also means an OAM writer must avoid overwriting matrix words while it updates neighboring object attributes.

Represent the OAM image as a byte-accurate block or expose separate helpers that preserve the fourth halfword slots. An API that writes three attribute halfwords should not zero the matrix parameter at offset six as a side effect. Conversely, writing a matrix should not clobber the next OBJ’s first attribute. The GBA video renderer in mGBA resolves the matrix index and loads a, b, c, and d from the shared OAM matrix array at render time, illustrating why a detached per-sprite copy can become stale.

Save states need the full OAM byte array, including matrix slots, as well as any renderer state already latched for the current scanline. If an emulator snapshots only its logical list of OBJ records, it may lose the aliased coefficient halfwords. If it snapshots both a logical matrix cache and OAM bytes, define which representation is authoritative and invalidate the other on writes. Mid-frame save-state tests should include one affine OBJ and another OBJ that selects the same matrix.

A narrow coefficient and slot fixture

The following Python helpers demonstrate two independent facts: matrix words are separated by eight bytes in OAM, and the coefficients are interpreted as signed 8.8 values. They do not implement OAM bus timing, per-scanline evaluation, pixel clipping, object priority, tile fetch, or all hardware rounding behavior.

OAM_BASE = 0x07000000
OAM_ENTRY_BYTES = 8
MATRIX_COUNT = 32


def affine_matrix_offsets(index):
    if not 0 <= index < MATRIX_COUNT:
        raise ValueError("GBA has 32 OBJ affine parameter groups")
    first = OAM_BASE + 6 + index * 4 * OAM_ENTRY_BYTES
    return first, first + 8, first + 16, first + 24


def signed_8_8(raw):
    if not 0 <= raw <= 0xFFFF:
        raise ValueError("coefficient is a 16-bit OAM halfword")
    value = raw - 0x10000 if raw & 0x8000 else raw
    return value / 256.0


assert affine_matrix_offsets(0) == (0x07000006, 0x0700000E, 0x07000016, 0x0700001E)
assert affine_matrix_offsets(1) == (0x07000026, 0x0700002E, 0x07000036, 0x0700003E)
assert signed_8_8(0x0100) == 1.0
assert signed_8_8(0xFF00) == -1.0

For a renderer test, initialize a nearest-neighbor checkerboard, set the identity matrix (0x0100, 0, 0, 0x0100), and confirm that all in-bounds source coordinates are preserved. Then test half and double scale, a quarter-turn matrix, a shear, a negative coefficient, and a case whose transformed corners require double-size mode. Compare exact pixel coordinates rather than a filtered screenshot.

Diagnose missing or displaced affine objects

Start by logging the raw 8-byte OAM entry and all four selected matrix halfwords. Decode bit 8 before interpreting Attribute 1’s bits 9-13. Confirm the shape-size pair produces the expected source width and height, and confirm the chosen OBJ character mapping points to the intended tile data. If the matrix index or OAM stride is wrong, the renderer can still display a plausible rectangle, so a screenshot alone does not establish that attributes were decoded correctly.

Next inspect the source-coordinate accumulator for the first, center, and last pixels in the screen-space bounding box. Record the signed coefficient values, integer destination offset from the bounding center, 32-bit intermediate, shifted source coordinate, and clipping decision. Large matrices can overflow a narrow intermediate even when each coefficient is a valid signed halfword. Use a sufficiently wide signed accumulator, then verify its truncation against the console reference model.

If only a rotated corner is missing, compare ordinary and double-size bounding extents before changing the source tile. If the sprite appears to move when double-size is enabled, check the center convention. If multiple sprites transform unexpectedly together, inspect whether they intentionally share a matrix index or whether an OAM writer overwrote shared matrix storage. If the image is mirrored only in non-affine mode, verify that the regular horizontal/vertical flip bits are not being decoded as an affine matrix index.

Acceptance criteria

A production-quality GBA affine OBJ implementation parses the OAM mode before interpreting overlapping fields, preserves the interleaved matrix layout, reads signed 8.8 coefficients from the selected shared group, and samples source coordinates from a center-relative destination bounding box. It treats double-size as an expanded processing canvas, keeps OAM and matrix state coherent, and tests exact pixel outputs at clipping boundaries. That model prevents matrix layout, source-addressing, and geometry errors from being conflated with tile or palette bugs.

Related:

Sources:

Comments