WPAR: Why Metroid Prime's Geometry Exploded

While helping my friend vxpm debug Metroid Prime in Lazuli, his GameCube emulator, I traced the exploding geometry to WPAR, the CPU’s Write Pipe Address Register.

Writing that register has a side effect that’s easy to miss: it discards any bytes still waiting in the write-gather pipe. Without the reset, leftover bytes can shift the next array and turn small coordinates into enormous triangles.

The Metroid Prime comparisons below reproduce that behavior by deliberately disabling the WPAR reset in Gecko:

The same station flyby with the WPAR reset implemented: the sky and station are unobscured.
Metroid Prime reproduction in Gecko with the WPAR reset disabled: bright triangles cover the sky above the station.
Disabling the WPAR reset in Gecko reproduces the triangles above the station.

In this reproduction, a vertex’s Y coordinate went from roughly 0.18 to minus 3.2 quadrillion. That’ll stretch a triangle quite a bit :^)

A bit of gathering

The GameCube’s CPU, Gekko, has a write-gather pipe that combines small stores into 32-byte bursts. A byte-sized command and a 4-byte value can join the same stream, which waits until a complete burst is ready to transfer.

Games normally write to the port at 0xCC008000, whose physical address, 0x0C008000, is selected by WPAR. Repeated stores append bytes to the pipe even though the CPU keeps writing to the same address.

The processor interface, or PI, then writes each burst into RAM at its FIFO write pointer and advances that pointer. Normally the graphics processor, GX, reads commands from this FIFO: a queue that preserves their order.

There are 2 addresses involved here: the port the CPU writes to and the destination in RAM selected by PI. Software can redirect the destination to another buffer and disconnect GX’s FIFO so it won’t consume those bytes as graphics commands. PI continues writing the bursts to RAM.

Metroid Prime uses this to build arrays of positions and normals, the surface directions used for lighting:

Writing the same address again

The WPAR description includes the reset rule:

A mtspr to WPAR invalidates the data in the buffer and sets the gather address.

mtspr is the PowerPC instruction for writing a special-purpose register. Writing WPAR discards pending bytes even if the address doesn’t change, while bytes already transferred to RAM stay there.

Suppose 1 padding byte remains in the pipe when software rewrites WPAR and redirects PI to a new buffer. The next 32 bytes should form a burst of their own, but without the reset, the old byte takes the first slot. Only 31 new bytes fit and the last byte stays behind.

The example below uses 8 floats as its new payload. Step through the writes, then change how much padding was left over:

Old paddingNew float dataEmpty

Reset missing

1 old byte waiting.

Reset implemented

1 old byte waiting.

With 0 pending bytes, there’s nothing to discard and both versions work. With 4, the floats still start on 4-byte boundaries but occupy the wrong array entries. Other amounts can split individual floats across the boundaries where GX expects to read them.

Skinning through the pipe

Animated models start with vertices in a reference pose. Skinning transforms those vertices according to the current pose of the model’s bones and updates their normals to match.

The Prime decompilation’s CSkinnedModel::Calculate shows how the game writes those results. With allocation, cache management and optional morphing omitted, the sequence looks like this:

pipe = GXRedirectWriteGatherPipe(verts);

BuildAccumulatedTransforms(pose, layout);
BuildPoints(pipe);
// Pad the position region to a multiple of 32 bytes.
BuildNormals(pipe);

GXRestoreWriteGatherPipe();

The CPU calculates the transformed vertices and writes the results through the pipe. There’s a nice detail in CVirtualBone::BuildPoints: its small-count path writes X, Y and Z through the same pointer, without incrementing it. Each store appends another coordinate because that pointer refers to the write-gather port.

The SDK’s redirect routine finishes the previous graphics writes, waits for outstanding transfers and rewrites WPAR. It then disconnects the FIFOs, saves the old PI write pointer and points PI at the workspace supplied by the game.

WPAR still contains 0x0C008000, but writing it empties the pipe before PI’s destination changes.

The game writes positions first, pads that region to a multiple of 32 bytes and writes the normals after it. Later, GX reads these arrays using base addresses and a stride, the number of bytes between entries. If old padding lands at the start of the workspace, the new data won’t line up with those addresses.

Why add 31 zeros?

GXRestoreWriteGatherPipe writes 31 zero bytes before restoring the graphics FIFO.

A partial burst can hold between 1 and 31 useful bytes. Adding 31 zeros guarantees that all the useful bytes leave in a complete burst, though some padding may remain in the pipe. WPAR is then rewritten to discard that remainder before the destination changes again.

For example, 12 useful bytes need 20 zeros to complete a burst. The other 11 zeros stay in the pipe until the WPAR write discards them:

Useful dataZero paddingEmpty slot
Partial burst · not transferred yet
Tail left in the gather pipe

Try 1 useful byte: all 31 zeros fit in the burst and there’s nothing left to discard. Try 0: the useful data already ended on a burst boundary, so all 31 zeros remain pending and get discarded.

The order matters because padding transfers the useful tail and resetting discards the unused remainder. Resetting first would lose data, whereas keeping the padding would shift the next buffer’s contents.

Back to Metroid Prime

Reproducing this in Gecko, the affected skinning pass had 23 zero bytes pending when the game redirected the pipe. Without the WPAR reset, those bytes became the start of the new workspace:

ResetByte offsetBytes
Implemented03E A0 F9 08 3E 39 E0 1A
Missing0-2200 … 00 (23 zeros)
Missing233E A0 F9 08 3E 39 E0 1A

All 4’224 bytes of the corrected workspace match the broken workspace when shifted by 23 bytes. The calculations produced the same data, but Gecko stored it in the wrong places.

GX still reads from the original array base with a stride of 12 bytes: 3 4-byte floats per position. At array index 12, that gives these coordinates, rounded for readability:

AxisReset implementedReset missing
X0.1060610.1060610.009569700.00956970
Y0.1837040.1837043.21470×1015-3.21470 \times 10^{15}
Z008.68805×10448.68805 \times 10^{-44}

The broken Y coordinate comes from the bytes D9 36 BC 00. A 32-bit float uses 1 bit for its sign, 8 for its exponent and 23 for its fraction. For a normal finite value, those fields give:

(1)s(1+f223)2e127\displaystyle (-1)^s \left(1 + \frac{f}{2^{23}}\right) 2^{e-127}

Here, ss is the sign bit and ee and ff are the unsigned integers stored in the exponent and fraction fields. The exponent is stored with a bias of 127, which we subtract to get the power of 2.

For D9 36 BC 00, the sign is negative and the stored exponent is 178, giving 2178127=2512^{178-127} = 2^{51}. Multiplying by that is what makes the coordinate so enormous. The inspector below shows the calculation using the captured bytes from the Gecko reproduction:

Bytes at 0x804c0050

D9 36 BC 00-3.214697e+15
Sign · 1 bitNegative
Exponent · 8 bits178
Fraction · 23 bits3'587'072
(1+3’587’072223)2513.214697×1015\begin{aligned} &-\left(1 + \frac{\text{3'587'072}}{2^{23}}\right) 2^{51} \\ &\approx -3.214697\times 10^{15} \end{aligned}

4 bytes starting at offset 4 decode to -3.214697e+15.

Move the window 1 byte left. 3D D9 36 BC decodes to 0.10606143 because the sign and exponent now come from different bits.

That exposes a float from the intended stream, but it isn’t the corrected Y coordinate. The whole stream moved by 23 bytes, so this nearby aligned value belongs to an earlier part of it. Y at the correct array offset is 0.18370357, as shown in the table.

The huge coordinate is already present before projection. Connecting that displaced vertex to the others stretches its triangles far beyond the model, producing the large shapes over the station. The same shift also corrupts the normal array used for lighting.

Implementing the fix

Gecko’s fix adds explicit WPAR handling in both the interpreter and the runtime function used by the CPU JIT:

921 => {
    sys.gekko.spr.wpar = val & !1;
    sys.cp.gather_pos = 0;
}

921 is WPAR’s register number and gather_pos is the number of queued bytes. Setting it to 0 makes the pipe empty; the next stores overwrite the old bytes, so there’s no need to zero the buffer itself.

val & !1 clears the lowest bit of the supplied value. That bit is BNE, the read-only buffer status bit, so software can’t set it by writing WPAR. The mask handles the register value, whereas gather_pos = 0 performs the reset.

The same commit also fixes the RAM writes during redirection. Previously, Gecko only wrote bursts to RAM when the CPU and GPU FIFOs were linked, but PI must keep writing while they’re disconnected. Only the notification to GX depends on the link:

write the complete burst to RAM
advance the PI write pointer

if the GPU FIFO is linked:
    update its write pointer and available byte count

retain only the incomplete tail for the next burst

Switching off only the WPAR reset produces the glitching on the left:

Watch the oversized triangles sweep across the view on the left.

The vertex-skip bug added an unwanted corner and z-freeze changed which surface won the depth test. This time, the coordinates were already wrong before GX got to them.

Turns out throwing data away was the missing feature :^)