A battle frame, end to end
One frame of a fight is built in four moves: the battle code decides what changed, every body and effect is turned into GPU packets, the packets are linked into an ordering table by depth, and the GPU walks that table from the far end to the near one. Each move has its own reference page. This page follows a single frame through all four, because most questions about a battle picture ("why is this flash a frame late", "why does this frame have no dither") are questions about which move did the work and in what order.
At a glance
- Decides
- The action state machine
FUN_801E295C, then the per-body anim tickFUN_80047430(action state machine, actor rendering) - Lands casts
- The cast tick
FUN_801E09F8, called from the draw, after every body's tick (action helpers) - Emits
- The per-body draw
FUN_80048A08and the per-primitive dispatchFUN_80043390through the GTE (renderer) - Sorts
- One ordering table for 3D primitives and screen-space packets alike, walked far bucket first (renderer internals)
- Colours
- Texel, palette from VRAM,
texel * colour / 128, blend, then the 4x4 dither into 15 bits (shading) - Paces
- The frame step
DAT_1F800393: one pass spans one to four vsyncs (frame step)
The frame in one picture
Read it top to bottom. The top band is the tick: everything that changes state. The middle band is the draw: nothing here decides anything about the fight except the cast tick, which is the one piece of game logic that runs from the draw. The bottom band is the GPU's walk of what the draw linked.
1. The tick decides
The master frame driver FUN_80016444 runs every list's tick pass before any of its draw passes. For a battle that means, in this order:
- The action state machine
FUN_801E295Cruns one state of the acting seat's turn and drains its timers by the frame step. - The separation pass pushes overlapping bodies apart, the stage colour pass moves the ambient ramp, and the camera frames the shot the state asked for.
- The anim tick
FUN_80047430runs once for each body: it advances the clip cursor by(step * rate * clip_rate) >> 1, applies the clip's root motion, walks the clip's hit events, and only then commits a clip that was staged.
Two consequences are worth holding on to. The commits sit after the advance and a commit zeroes the cursor, so a clip the state machine stages this pass starts the next pass on its first keyframe. And the camera frames ahead of the anim tick, so the shot is built from the pose the previous pass left on screen.
A pass is one call with the step folded in, not step one-vsync calls: retail tests each counter and range once a pass, so a threshold the pass's first vsync would have crossed does not stop its second.
2. The cast tick runs from the draw
The cast tick FUN_801E09F8 has one caller, and it is not the tick pass. It is the battle draw tick FUN_800480D8 (jal 0x801E09F8 at 0x80048130), in a preamble the frame's first drawn body runs behind the latch ctx+0x272. That preamble also holds the effect-pool walker FUN_801E0080, the damage popup and the effect-node sweep. So the routine that counts outstanding effects, flies projectiles and lands them runs after every body has ticked.
That placement sets when each part of a landing is seen:
| What the cast tick does on pass N | When it shows or is read |
|---|---|
| Draws or stores something itself: flight ribbons, the victim's tint, the landing cue | Pass N |
| Recounts the census (bodies still mid-animation, live spell children, the sole-survivor latches) | The state machine reads it on pass N+1 |
| Stages the victim's reaction clip | Commits on that seat's anim tick in pass N+1, so the reaction cue rings a frame after the landing cue |
| Links a new node: the landing flash's fade actor, a list's effect children | First ticks on pass N+1's walk and first shows there |
The port keeps the same seat: World::tick_battle_cast_drive runs at the tail of the anim pass, and the nodes it links are handed on at the head of the next pass (World::release_draw_spawns). A cast-sequence film shows the difference directly: Astral Wave's recording brightens one frame after the landing pass.
3. The draw emits packets
Every battle body, whether a monster, a party member or a part of a summoned creature, goes through FUN_80048A08 once a rendered frame. It composes the body's matrix under the camera's, runs the pose decoder FUN_8004998C to turn the clip cursor into bone matrices, applies the render scale, and hands each object of the mesh to a leaf. The usual leaf is the per-primitive dispatch FUN_80043390: for each triangle or quad it projects the corners (RTPT), rejects back faces (NCLIP), averages the depth (AVSZ) and runs the primitive's colour op.
The colour op is not lighting in the usual sense. A battle primitive carries a colour word baked into the mesh; the tint pass FUN_8004A908 has already written the body's own colour word and blend weight from its distance to the camera and its status, and the depth cue pulls the result toward the far colour. The only primitives that take a light source are a mesh's light-source rows. See shading for the whole chain.
The pose decoder does one more job: it stores every body's position pair from the pose it just decoded. The next pass's state machine and camera read those, which is how the draw closes a pass.
4. The ordering table sorts
Nothing is drawn as it is emitted. Each packet is linked into an ordering table, an array of buckets indexed by depth, and the GPU later walks the array from the far end to the near end. There is no depth buffer: a nearer bucket paints over a farther one, and inside one bucket the packet linked last is drawn first.
3D primitives and screen-space packets share the one table. A plain battle frame's table reads, from far to near:
The port has no ordering table at run time for 3D geometry by default: it depth-tests. PSX mode draws the field's primitives at their retail table links, and the screen-space pass (screen_prim::order_primitives) reproduces the link-and-walk exactly, descending bucket and last-in first-out inside a bucket, for both hosts.
5. The GPU colours, and sometimes dithers
For each pixel of a textured primitive the GPU reads a texel index, looks it up in a palette that lives in VRAM (not in the texture's file), multiplies by the primitive's colour as texel * colour / 128, blends with the framebuffer when the primitive and the palette entry both ask for it, and writes 15 bits. With dithering on, a 4x4 ordered offset is added before each channel is cut to 5 bits; that is the fine cross-hatch on a gradient.
Retail boots with dithering on and the frame-begin driver FUN_80016B6C copies the setting into the draw environment every frame. But the enable is a bit of the draw mode, and two battle draws link a draw-mode packet of their own:
- A cast's armed wash goes through the fade quad
FUN_80024EE4in the farthest bucket, and that routine linksSetDrawMode(FUN_80059010) with the dither argument clear ahead of its quad. Everything behind the next packet that sets the bit is undithered. - The ground grid
FUN_80026CE4brackets itself with two draw-mode packets: dither on ahead of the grid, then the frame's own setting for the rest of the table.
So a frame that arms the wash over a stage whose grid does not draw has no packet to set the bit again. Ozma's arms 16 to 21 show no 4x4 pattern at all in a retail recording, where the arms before them show the full one; the stacked additive fans are where it is visible, half a 5-bit step a layer. The port carries this as one flag a frame, World::frame_dither, and both hosts gate PSX mode's dither on it. A packet that moves the bit part of the way down the table is not modelled.
A cast module can link a draw-mode packet too, and every one a battle reaches passes the dither argument clear except the ground grid, the letterbox handler and one resident routine. The full list of call sites is in the function reference.
What paces it
At the end of the frame the driver FUN_80016B6C puts the frame's cost, an hblank count, into a sixteen-entry ring and bins the ring's maximum into the next frame step: 1, 2, 3 or 4 vsyncs. The pass above then runs once with that step folded into every counter. A busy cast (many additive sheets, a full party) costs more, the step rises, and the same clip plays over fewer, longer passes. This is why a recording is compared pass for pass on the game's own clock rather than vsync for vsync. The ring, the bins and what the port does for a console's cost are on battle internals.
No battle routine waits on its own: no battle overlay calls VSync or DrawSync, and nothing in a battle writes the forced step or the step floor. A frame that runs past its step does so on what it costs. On the emulator the films are recorded on, the longest frames are the CD stop watch: while a voice clip plays, the clip player polls the drive from the CD interrupt back to back, and a frame's work runs in the third of the CPU the polls leave. Tri-Somersault's three voice lines and Element Strike's burning arms run eight to eleven vsyncs a frame that way. What a frame draws counts too: each after-image ghost re-runs the body's whole object draw, so the two ghosts of a Super Art's slow motion lift a full party's frame some 200 hblanks, over the rung into step 4. The port prices a party member’s ghost by the triangles its seat draws, so those passes run step 4 as retail’s do, the pass that leaves the slow motion runs four vsyncs, and the flinch cursor the juggle counter reads lands where retail’s does: all ten Tri-Somersault hits roll retail’s numbers.
How the port is held to it
- The order of the tick is pinned by a counter the camera and the anim tick both touch: the close-up accumulator gains
8 * stepin the state machine's framing prologue and a clip commit zeroes it. Retail save states hold the pair of values only one ordering produces. - The commit sites of the anim tick run against retail's own routine in the retail kernel differential.
- The cast tick's seat shows in a cast-sequence film: the landing cue, the reaction cue and the flash each fall on the pass the table above gives.
- The picture is scored by the retail comparison corpus in PSX mode at the native 320x240, where the dither pattern is part of the image.