Donald was having a particularly bad day in Disney’s Donald Duck: Goin’ Quackers. In Gecko, my GameCube and Wii emulator, his idle animation in the laboratory started out normally, but then his feet stretched out and his body folded into itself! When I let it run a little longer, the triangles grew large enough to cover the whole room…

Despite how it looks, the graphics code wasn’t to blame. A memory transfer was ignoring an alignment rule, so the game read the wrong animation data long before the graphics processor ever saw a vertex. Let’s look into this!
A mesh is made of triangles that share their corners, or vertices. If some of those vertices end up far from where they belong, the triangles still connect the same corners, so a small surface can turn into an enormous spike. That’s what I usually call a vertex explosion.
The first question was where the bad positions came from. Goin’ Quackers animates Donald on the CPU: it calculates a transform for each bone, uses those transforms to move the model’s vertices and writes the results into RAM. GX, the GameCube’s graphics processor, then reads those positions and draws the triangles.
The positions were already wrong in RAM, so GX was faithfully drawing broken geometry. Switching from Gecko’s JIT to the CPU interpreter produced the same deformation, which pointed at something both execution paths had in common rather than a bug in translated code.
Here’s Donald’s model. Switch between the poses and rotate him to see how far the damage goes. The viewer uses simple colors in place of the game’s textures and lighting.
Drag to rotate Donald, or focus the model and use the arrow keys.
From the vertex writes, I worked back through the bone matrices to the animation keyframes. A keyframe describes a pose at a particular time and the game interpolates between 2 of them to produce the poses in between. Donald’s animation was feeding that calculation values which no longer belonged together… Turns out the keyframe records had arrived through ARAM!
Besides its main RAM, the GameCube has a separate 16 MiB memory called ARAM. It’s mostly used for audio, but games are free to store other data there too. Goin’ Quackers keeps animation data in ARAM and brings small pieces of it into main RAM with DMA.
ARAM DMA only works in 32-byte blocks. Its main RAM address, ARAM address and transfer length are all masked with 0x03ff_ffe0, which keeps bits 5 through 25 and clears the lowest 5 bits. The transfer direction is bit 31 of the length register. Dolphin’s DSP register implementation applies the same masks when these registers are written.
That matters because the game’s animation records are 24 bytes long, so most of them don’t start on a 32-byte boundary. The game handles this itself: it requests 64 bytes around the record and picks the 24 bytes it needs out of that buffer afterward.
For a record at 0x7888, the intended sequence looks like this:
0x7880.The game deliberately keeps the original, unaligned address around for that last step. The problem was that Gecko used it as the DMA start address too!
Without the mask, Gecko started copying at 0x7888. The callback then skipped another 8 bytes, so the record it extracted actually started at 0x7890.
More generally, let be the aligned block address and the record’s offset within that block, so the record starts at . With aligned DMA, the callback’s extraction lands exactly there:
Gecko’s DMA had already started at the record, so the same extraction overshot it:
Every record was shifted by bytes. Try changing the offset below, especially to 0:
Request 0x7888 · transfer 64 bytes · keep 24 bytes
DMA starts at 0x7880
Then the callback skips 8 bytes.
0x7888-0x789f
DMA starts at 0x7888
Then the callback skips 8 bytes.
0x7890-0x78a7
Each cell labels a source byte's offset from 0x7880, not its contents.
An aligned record comes out correctly either way, which is why this didn’t break every frame of every animation. Since the records are 24 bytes long, consecutive records sit at different positions within their blocks. For example, starting at offset 16 the offsets go 16, 8, 0, 24 and then repeat, so 1 in every 4 records happens to work even without the mask.
At offsets 8 and 16, the extracted bytes straddle 2 records. At offset 24, the shift skips an entire record and selects the next one instead. The fields can still look plausible, but the game is no longer reading the keyframe it asked for.
To see how shifted bytes can turn into a stretched foot, here’s an example using the game’s interpolation rules.
Take an animation time of 33.54 ticks, a current keyframe at 25 and a next keyframe at 0, so the next keyframe supposedly comes before the current one. The game calculates the interpolation factor like this:
Here is the animation clock and are the 2 keyframe times, all in ticks. Substituting these values gives:
A factor between 0 and 1 blends between the 2 keyframes, whereas this negative factor extrapolates beyond the first one. That alone doesn’t explain an exploding model, though, because exact slerp between valid unit quaternions preserves their length even when extrapolating. The mismatch in the rotation data is what matters here.
The game stores bone rotations as quaternions (I hate them with a passion). A quaternion has 4 components and the matrix formula the game uses assumes its length is 1. To blend 2 of them, the game uses spherical interpolation, or slerp. For unit quaternions separated by an angle in quaternion space, the result is:
with the weights:
For this animation path, the game reads a stored angle rather than calculating from the quaternions. It looks up the sines in a table with 4’096 entries per full turn. Suppose the record supplies 113 units, where each unit is radians, giving about 0.17334 radians. If the 2 unit quaternions have a dot product of 0.70769, their actual separation is radians. The stored angle doesn’t match the rotations it is supposed to describe.
The table lookup adds a small but important detail to the formula above. With a stored angle , the game computes , where truncation discards the fractional part toward zero. Here that gives . The sine indices are then and , with negative indices wrapping around the table:
These differ slightly from evaluating the continuous slerp formula directly. More importantly, they don’t preserve the length of these mismatched inputs. For unit quaternions, the squared length of the blend is:
Substituting the weights and dot product gives instead of 1.
The routine that turns a quaternion into a matrix doesn’t normalize it first, so a nonunit result can stretch the bone as well as rotating it. Each bone’s transform is then combined with its parent’s, allowing the damage to propagate down the hierarchy.
The last step is skinning, where each vertex’s position is blended from the transforms of the bones it’s attached to. Even when those weights add up to 1, a blend of damaged transforms can still put the vertex far from where it belongs. The CPU writes the resulting position to RAM and GX draws exactly what it’s given!
Every one of these calculations did exactly what it was supposed to. The mistake happened several steps earlier, when the bytes were copied.
The fix applies the address and length masks whenever the game writes the ARAM DMA registers, while preserving the direction bit in the length register:
const ARAM_DMA_MASK: u32 = 0x03ff_ffe0;
address = written_address & ARAM_DMA_MASK;
control = written_control & (0x8000_0000 | ARAM_DMA_MASK);
Loading a savestate applies the same masks too, because an older build can have saved unmasked register values. Writing the lower half of the length register starts the transfer as before, whereas writing only its upper half doesn’t.
All that math made my brain explode, but hey, Donald gets to keep his feet :^)