How Sprite Init Works
A technical reference for how bank $C0 turns field objects into sprites, as recovered from
byte-exact matched code. This page describes the system as it works now that every routine in it
is matched and reviewed; older devlog posts describe it with earlier names and, in places, an
earlier and partly wrong reading (a "sprite descriptor table", "palette copy loops", "init pass 2"
markers). The headers in asm/bankC0/bankC0.asm are the reference.
Overview
Drawing an object takes two separate jobs:
- Building a frame puts the object's graphics in VRAM and writes its tile records. It is
expensive, so it is queued and spread over frames: the animation tick queues the object when
its frame changes, and
Field_ProcessAnimQueuebuilds queued frames while the frame still has time. - Appending to OAM happens for every visible object every frame:
Oam_BuildShadowvisits the objects in their draw buckets andSpr_AppendToOamcopies each one's tile records, placed at its current screen position, into the OAM shadow, which the NMI uploads.
Field_EndOfFrame
Vblank_UnkA810 per object: move, view test,
Obj_AnimTickAndQueue animation tick → join the frame-build queue
Obj_DrawUnlink / Obj_DrawLink re-bucket by screen Y
Field_ProcessAnimQueue while there is time left in the frame:
Obj_BuildSpriteFrameStep by size class:
Obj_BuildFrame4 / 8 / 12 copy graphics → SprBuf chunk → VRAM queue,
write SprTileSrc records, INC Obj_State
BRL Oam_BuildShadow every object in every draw bucket:
Spr_AppendToOam
Spr_PrepareTiles Obj_State → Spr_LoadN or Spr_PlaceN
copy SprTile X/Y/Tile/Attr → OAM shadow range, high-table bits too
NmiHandler VramQ_Flush (the queued graphics), Oam_UploadShadow
Size classes
Obj_SprSize ($1201, per object) bits 0–1 give the object's size class: how many sprite
tiles (OAM entries, one SprTile record each) it uses. A frame's graphics are 8×8 tiles: a
4-tile object's frame copies 16 of them.
| Class | Sprite tiles | High-table bytes | Load | Place | Frame builder |
|---|---|---|---|---|---|
| 0 | 4 | 1 | Spr_Load4 |
Spr_Place4 |
Obj_BuildFrame4 (one pass) |
| 1 | 8 | 2 | Spr_Load8 |
Spr_Place8 |
Obj_BuildFrame8 (two passes) |
| 2 | 12 | 3 | Spr_Load12 |
Spr_Place12 |
Obj_BuildFrame12 (three passes) |
| 3 | 24 | 6 | — | Spr_Place24 |
Spr_LoadLargeObj (one go, at set-up) |
Tile records
Each object owns a run of 8-byte records in bank $7F, starting at its Obj_TileRecOfs:
| Struct | Base | Fields |
|---|---|---|
SprTile |
$7F:4BC0 |
X, Y (OAM position after adding the object's base and clamping), OfsX (16-bit), OfsY, Tile, Attr |
SprTileSrc |
$7F:4800 |
the same layout; the frame builders write OfsX, OfsY, Tile, Attr here |
The packed OAM high-table bytes of each object (the 9th X bit and the size bit of each sprite
tile) are kept in Obj_OamHiA/B/C ($7F:4F00, $7F:4B40, $7F:4F80, two bytes per object each).
Obj_State and Spr_PrepareTiles
Obj_State ($1B00, per object) counts the frame builder's finished passes in bits 0–6;
$80 means the SprTile records are up to date. Spr_PrepareTiles ($C0:B701) reads it before
each append:
Obj_State |
Action |
|---|---|
$00 |
Nothing to draw (C=1) |
$01–$7F, below the class's threshold |
Frame not finished yet (C=1); the threshold is 2 passes for class 1, 3 for class 2, none for class 0 |
at or above the threshold ($01–$7F or $81–$FF) |
Spr_LoadN: copy SprTileSrc into SprTile and place it; free the object's SprBuf chunks (SprBuf_Free1/2/3); Obj_State = $80 |
$80, or $81… below the threshold |
Spr_PlaceN: re-place the existing records at the object's position |
Class 3 always goes to Spr_Place24 by BRL, whose carry becomes the result.
Placing. The Spr_Place* and Spr_Load* routines set each record's X = base X + OfsX
(packing bit 8 into the high-table byte with Spr_PackHiBits4 in the 24-tile case) and Y = base
Y + OfsY, then clamp Y to Oam_HiddenY ($E0, off screen) in one of three ways chosen by the
base Y's high byte and bit 7. The variants differ in their branch orderings from routine to
routine; each header spells out its own.
Building a frame
Obj_AnimTickAndQueue ($C0:C98A) counts an object's animation timer down; when it reaches 0
the object joins the frame-build queue (ObjQ_Head / ObjQ_Tail, linked through
Obj_QueueNext) and only then advances to its next frame, whose duration it reads from bank
$E4. Field_ProcessAnimQueue ($C0:CA76) takes the queue head while the V counter is at line
240 or later, or below ObjQ_ScanlineLimit, and runs Obj_BuildSpriteFrameStep ($C0:CAD9);
C=1 from the builder means "call again" (another pass). It waits while an earlier build's VRAM
upload is still pending.
A builder (Obj_BuildFrame4, for example, $C0:CBDC):
- looks the frame up (
Obj_AnimFrameLookup: the frame number for the object's facing, row and column, from bank$E4), and stops if it is the frame already built (Obj_LastFrame); - takes WRAM tile buffer chunks:
SprBuf_Alloc1/2/3give the object 1, 2 or 3 adjacent$200-byte chunks of the buffer at$7F:3800(owners inSprBuf_Owner,$0BC0;$80= free); - copies the frame's 8×8 graphics tiles into the chunk with
Spr_CopyTileor, for a tile whose bit 14 is set,Spr_CopyTileFlipped(mirrored left to right); the source bank picks one of the unrolled copiers (Spr_CopyTileD2/D3/D4, bank$D5, orSpr_CopyTile7Fwhen the graphics are already in WRAM); - queues the chunk for VRAM at the object's
Obj_VramTile(theVramQ_*queue, which the NMI flushes withVramQ_Flush); - writes the
SprTileSrcrecords (offsets from the frame record, tile numbers fromObj_VramTile, attributes from the object's palette and priority) and incrementsObj_State.
The 8- and 12-tile builders split this over passes (Obj_BuildFrame8Pass0/1,
Obj_BuildFrame12Pass0/1/2 and their Alt variants for the alternate tile layout $68). Once
Obj_State reaches the class's threshold, the next Spr_PrepareTiles loads the records and frees
the chunks.
Obj_BuildFrameLayout ($C0:CB0A) and Obj_FrameLayout4/8/12 rewrite only the records, for
objects whose graphics are already in VRAM (used after a battle). Spr_LoadLargeObj ($C0:E12A)
loads a 24-tile object in one go: 96 graphics tiles into the whole buffer, one DMA to VRAM word
$0400, and 24 records.
The OAM shadow
Oam_BuildShadow ($C0:B271) rebuilds the shadow (low table at $0700, high table at $0900)
every frame. It is split into three ranges, each with a low-table and a high-table write pointer;
an object's Obj_OamFlags ($0F80) bits 2–3 choose its range. Objects are visited bucket by
bucket from Obj_DrawBucket ($0E00, 64 buckets, last first), following Obj_DrawNext chains;
Obj_DrawLink puts an object in the bucket for its screen Y.
Spr_AppendToOam ($C0:B309) runs Spr_PrepareTiles, then copies the object's packed
high-table bytes to its range's high-table pointer and aims the WRAM port (WMADD) at the
range's low-table pointer, writing X, Y, Tile, Attr of each record through WMDATA
with DP pointed at $2100. After the last object, every entry between a range's new end and last
frame's end is parked at Y = $E0. NmiHandler uploads the shadow (Oam_UploadShadow, $220
bytes, DMA channel 7).
Quirks kept as in the original
Spr_Place4andSpr_Load4also clampSprTile[4], the record after a 4-tile object's last one, on one path (harmless, per the header, since each object's records are placed again before they are copied).- On one path
Spr_Place24leaves the carry of the last tile's Y add, so a 24-tile object can be skipped for a frame. Obj_BuildFrame12returns C=1 for fixed-frame (mode 3) objects without building anything, soField_ProcessAnimQueuekeeps retrying them until its scanline window closes.SprBuf_FreeAllclears all eightSprBuf_Ownerentries, though only entries 0–3 are used.
The routines
| Routine | Address | Role |
|---|---|---|
Oam_BuildShadow |
$C0:B271 |
Rebuild the OAM shadow, bucket by bucket |
Spr_AppendToOam |
$C0:B309 |
Append one object's sprite tiles to the shadow |
Spr_PrepareTiles |
$C0:B701 |
Bring the object's SprTile records up to date |
Spr_Place4 / Spr_Load4 |
$C0:B788 / $C0:B8CA |
Class 0 place / load |
Spr_Place8 / Spr_Load8 |
$C0:BA65 / $C0:BCDC |
Class 1 |
Spr_Place12 / Spr_Load12 |
$C0:BFF2 / $C0:C2BF |
Class 2 |
Spr_PackHiBits4 / Spr_Place24 |
$C0:C6E7 / $C0:C73A |
Class 3 |
Obj_AnimTickAndQueue |
$C0:C98A |
Animation tick, frame-build queue |
Field_ProcessAnimQueue |
$C0:CA76 |
Build queued frames while there is time |
Obj_BuildSpriteFrameStep |
$C0:CAD9 |
One build step, by class |
Obj_BuildFrameLayout, Obj_AnimFrameLookup, Obj_BuildFrame4 / 8 / 12 (+ passes), Obj_FrameLayout4 / 8 / 12 |
$C0:CB0A–$C0:E129 |
Frame builders and layout rewriters |
Spr_LoadLargeObj |
$C0:E12A |
24-tile object, in one go |
Spr_CopyTile* |
$C0:E534–$C0:E934 |
Graphics tile copiers |
SprBuf_Alloc1/2/3, SprBuf_Free1/2/3, SprBuf_FreeAll, SprBuf_FreeObj |
$C0:E935–$C0:EA62 |
Tile buffer chunks |
VramQ_Flush |
$C0:B1B2 |
Upload the queued graphics (from the NMI) |