12 August 2026: the sprite-init system closes, and a first look at bank C1
The triple-slot routines, the dispatcher layer above the sprite-slot cluster and the animation queue that drives it are all matched, so the sprite system from $C98A to $E129 is one continuous run of matched code. Then a scouting pass over bank $C1, the battle engine. 3,509 new bytes in 10 functions (sessions 31 to 33).
The last triple-slot routines
Yesterday left the pass-1 and pass-2+ tails of the triple-slot dispatch unmatched: four routines at $C0:D68B–$DD27, 1,693 bytes. Sub_D4F7 dispatches on a 2×2 grid: pass counter (1 vs 2+) × $0D00,X type ($68 vs not), each cell a BRL to its own routine.
Pass 1: Sub_D68B and Sub_D738. These are close to Sub_D608 (pass 0, $68), shifted in WRAM. Sub_D68B (not $68) loads WRAM slots at base + $0100 and base + $0300; Sub_D738 ($68) uses base + $0200 and base + $0400. Each runs two 8-tile DMA loops. They differ from the pass-2+ routines in two ways:
- They reload
LDX $6Dat the top. The pass-2+ routines don't; they inherit X fromSub_D4F7, which still holds the entity slot. The raw bytes at$D7E5settled it:C2 20 BD 80 0D, noA6 6D. - They end with
SEP #$30(A and X/Y both 8-bit before theINC/RTS). Pass 2+ usesSEP #$10: X/Y go 8-bit but A stays 16-bit for the OAM staging that follows.
Pass 1 exits INC $1B00,X; SEC; RTS: SEC means "not done, call me again".
Pass 2+: Sub_D7E5 and Sub_DA69. These also write the full OAM staging buffer: 12 Y entries with sign extension, the X entries, then attribute bytes. The sign-extension pattern:
LDA [$D3],Y
STA $7F4BC2,X
BPL .pos
LDA #$FF
BRA .done
.pos:
LDA #$00
.done:
STA $7F4BC3,X ; sign-extension byte
That pair of stores repeats 6 times, then come the X positions. Here the $68 difference shows: Sub_D7E5 (not $68) writes 8 X positions, then a CLC; ADC #$10 gap, then 4 more; Sub_DA69 ($68) writes 4, the gap, then 8. Different slot geometry for different sprite widths. The ADC #$10 is 69 10 (2 bytes) because A is 8-bit after SEP #$20, and asar's REP/SEP tracking got it right.
Pass 2+ exits INC $1B00,X; SEP #$10; CLC; RTS: CLC means "done". make diff: 4,194,304 bytes identical, once the SEP encoding and the LDX $6D asymmetry were right.
Sprite-Slot Init Cluster: $CB3A–$DD27, 4,232 bytes, 12 routines — COMPLETE
Sub_CB3A $CB3A–$CBDB 162 B Animation-frame gate
Sub_CBDC $CBDC–$CDC7 492 B Single-slot, gated 16-tile init
Sub_CDC8 $CDC8–$CEF4 301 B Single-slot OAM-only update
Sub_CEF5 $CEF5–$D123 559 B Dual-slot 3-way dispatch
Sub_D28A $D28A–$D30C 131 B Dual-slot pass-0
Sub_D30D $D30D–$D4F6 490 B Dual-slot pass-1+
Sub_D4F7 $D4F7–$D607 273 B Triple-slot 4-way dispatch
Sub_D608 $D608–$D68A 131 B Triple-slot pass-0, $68
Sub_D68B $D68B–$D737 173 B Triple-slot pass-1, non-$68
Sub_D738 $D738–$D7E4 173 B Triple-slot pass-1, $68
Sub_D7E5 $D7E5–$DA68 644 B Triple-slot pass-2+, non-$68
Sub_DA69 $DA69–$DD27 703 B Triple-slot pass-2+, $68
Four sessions earlier I had scoped this cluster at "12+ routines, 4,000+ bytes". It is done.
The dispatcher layer
Scouting callers turned up the main sprite-init dispatcher at $CB0A–$CB30 and two more BRL targets: $D124–$D289 (~358 bytes, which I guessed was a single-slot OAM-only variant) and $DD28–$E129 (~1,026 bytes, a quad-slot or extended variant). All three are matched:
Sub_CB0A($CB0A–$CB39, 48 bytes): the top-level sprite-type dispatcherSub_D124($D124–$D289, 358 bytes): an 8-slot OAM-staging initializer (type 1)Sub_DD28($DD28–$E129, 1,026 bytes): a 12-slot OAM-staging initializer (type 2)
The whole sprite-init system
Sessions 28–32 matched 15 routines spanning $CB0A–$E129 (5,664 bytes). As a system:
Every visible entity (character, NPC, enemy) is a "sprite slot", and each slot uses one, two or three hardware OAM entries depending on how many tile groups its art needs. When an entity appears, the game must (1) upload its tile graphics from ROM to VRAM and (2) describe the OAM layout: which tiles, where, in what priority and palette.
The VRAM-DMA cluster ($CB3A–$DD27, 12 routines, sessions 28–31) moves tile data:
Sub_CB3A, the animation-frame gate: if the frame hasn't changed since the last init, returnCLC(skip).- Allocate sprite slot(s) from the free list via
Sub_E952/E97A/E9AA. - DMA loop: for each 16×16 tile entry, branch on a flag bit to
Sub_E687(normal tile copy) orSub_E534(described in this session's notes as a flip-transformed copy), and send it to VRAM. - Increment the pass counter (
$1B00,X) and returnSEC(call again for the next pass).
It varies along two axes: slot count (single Sub_CBDC; dual Sub_CEF5/D28A/D30D; triple Sub_D4F7/D608/D68B/D738/D7E5/DA69) and layout, where $0D00,X == $68 picks a "left-heavy" 8+4 or "right-heavy" 4+8 X-position pattern.
The OAM-staging dispatcher layer ($CB0A–$CB39 + $D124–$D289 + $DD28–$E129, sessions 28/32) does no DMA. Sub_CDC8 (matched yesterday) was the first hint. Sub_CB0A reads bits 0–1 of $1201,X (the entity's sprite type) and tail-calls one of three routines with BRL:
| Value | Target | What it does |
|---|---|---|
| 0 | Sub_CDC8 | 1-group OAM staging |
| 1 | Sub_D124 | 8-group OAM staging |
| 2 | Sub_DD28 | 12-group OAM staging |
| other | CLC + RTS | no-op |
First it validates the entity and bails if the sprite state flag has bit 7 set, if $1A81,X is zero or negative, or if $0F00,X is zero. All four early exits branch backward to one shared RTS at $CB11 instead of using a second RTS later. You only notice from the raw offsets, which are all negative and all hit the same byte.
Sub_D124 and Sub_DD28 run later in the frame, after the tiles are uploaded, and refresh the OAM staging buffer from precomputed animation data:
Sub_D124multiplies the frame number by$50(80), reads 8 tile-index/attribute pairs fromY=$40, and writes 8 OAM groups at$7F:4BC2–$7F:4BFC(stride$08). It reads the OAM buffer index from WRAM at$001700,X(a long access to bank$00, not the ROM table at$C01700).Sub_DD28does the same for 12 groups ($7F:4BC2–$7F:4C1C), frame ×$78(120), starting atY=$60. If$0D00,X == $68it takes an alternate layout with the X gap after the 4th slot instead of the 8th. Both paths read the index from the ROM table at$C01700,X(absolute mode).
Throughout, each tile-index byte is followed by $00 or $FF (bit 7 of the index), the "tile number high bit" for 512-tile sprite banks.
The animation queue: the last gap
One gap was left next to the sprite system: $C0:C98A–$CB09, 384 bytes between the six-tile renderer (ending $C0:C989) and Sub_CB0A. It holds three routines, the animation clock behind everything above.
Obj_AnimTickAndQueue ($C0:C98A, 236 bytes) is the per-frame animation tick, called once per sprite slot. It reads the countdown at $1601,X, decrements it, and returns until it hits zero. Then it arbitrates focus slots: dp $76, $77 and $78 hold the primary focus, secondary focus and a tertiary mode flag. The sprite type in $1100,X decides the path: type 0 competes for $76, types 1 and 2 for $77. A winner increments its column counter $1681,X. Then it builds an address into a bank $E4 frame table: the row (from $1680,X or $1781,X by mode) × 4, plus the slot's base pointer from $1580,X, plus the column. A non-zero byte there becomes the new timer. A zero byte (end of row) falls back to the row base for the timer and adjusts or zeroes the column counter depending on whether the slot's loop counter has run out. Each sprite slot animates at its own pace.
Field_ProcessAnimQueue ($C0:CA76, 99 bytes) is the VBlank-time partner, called once per frame from the VBlank handler. It bails if $09A0 is non-zero (this stops it running twice). Then it latches the scanline ($2137 SLHV, then $213D OPVCT) and checks it against a threshold in $6B: if $6B ≤ V < 240 it exits, too close to the active display. Otherwise it takes the focus chain ($76, $77) and calls Obj_BuildSpriteFrameStep for the current slot. If the step reports "done" (carry clear), the slot leaves the chain ($1081,X set to $80) and the next one loads. It re-latches the scanline on every pass, so if VBlank ends mid-loop it stops. Re-reading the hardware counter, instead of counting cycles in software, is how an SNES VBlank task stays time-safe.
When Field_ProcessAnimQueue may work, by scanline. It re-reads the V counter before every step and stops once the beam reaches the limit in dp $6B, until line 240.
Obj_BuildSpriteFrameStep ($C0:CAD9, 49 bytes) is called by Field_ProcessAnimQueue and by the map-load pass at $C0:B109. It checks $1100,X, $1A81,X and $0F00,X in turn (carry clear if any fails), reads bits 0–1 of $1201,X, and tail-calls with BRL:
- type 0 →
Sub_CBDC(single-slot frame builder; this session's notes call the other two 8-slot and 12-slot builders) - type 1 →
Sub_CEF5 - type 2 →
Sub_D4F7
All three targets are already matched; they return carry set when a frame was produced. With this, $C98A to $E129 is contiguous matched code.
The whole sprite-init system as a call tree. The six triple-slot passes are picked by pass number and by whether $0D00,X is $68; Sub_CB0A is the separate layout-only path.
Where bank C0 stands
With the dispatcher layer, bank $C0 reached 19,114 bytes (30.9% of 61,779), and with the animation queue 19,498 (31.6%). The biggest uncharted blocks:
$59D9–$B191: ~22,425 bytes, betweenSub_597D(ends$59D8) andSub_B192. Likely more entity management, game-mode setup or map traversal.$2E1E–$595B: ~11,070 bytes, afterClearRAMDMAand before the entity dispatchers.
Smaller gaps remain near the E cluster ($EA42–$EC5F, ~542 bytes), probably close relatives of matched code.
Scouting bank C1
Bank $C1 is 63,904 bytes of code with only 1,632 bytes of incbin data: 97% code, the densest bank in the ROM. The dscotton reference names 481 routines in it, so almost nothing is uncharted. It is one integrated system.
The field engine enters it through one JSL $C10000 (Battle_Main), a JMP.W to a bootstrap that sets up the battle NMI/IRQ handlers, starts the battle main loop, and chains to banks $CF, $CC and $CD for the frame-by-frame battle tick. Bank $C1 is the coordination layer: battle menu, battler panels, stat bars, number display, enemy spatial queries and battle-phase sequencing.
It opens with a six-stub JMP vector table ($C10000–$C10018) and a bootstrap/dispatch block ($C10019–$C10088). Those 137 bytes mix data (the incbin jump table for the service dispatcher) and JML stubs, which makes them messier to verify. Right after, at $C10089, is a tight cluster of math leaf routines:
- Battle_Mul8 (29 bytes): 8×8 hardware multiply via
$4202/$4203/$4216. Input$AD/$AE, output$AF. - Battle_MulAccum (48 bytes): multiply-accumulate on the same hardware, into a 32-bit result at
$A9/$AA. Sets DB=0 for the register access. - Battle_Divide (54 bytes): 16÷8 hardware divide via
$4204–$4206/$4214/$4216. Input$B1/$B2/$B3, quotient$B5, remainder$B7. - Shift helpers (18 bytes, six overlapping stubs):
Battle_ShiftLeft7,ShiftLeft4andShiftLeft3fall through into each other;Battle_ShiftRight6toShiftRight3do the same.
They touch only the hardware math registers and a few direct-page temporaries, with no WRAM layout to understand: the cleanest possible way to open a new bank. After them comes BattleMsg_FormatNumberDigits at $C1:011F (decimal formatting for damage numbers), then the BattleUI and BattleMenu code.
The day in numbers
| Session | Routine | Address | Bytes | Running total quoted |
|---|---|---|---|---|
| 31 | Sub_D68B |
$C0:D68B–$D737 |
173 | |
| 31 | Sub_D738 |
$C0:D738–$D7E4 |
173 | |
| 31 | Sub_D7E5 |
$C0:D7E5–$DA68 |
644 | |
| 31 | Sub_DA69 |
$C0:DA69–$DD27 |
703 | 18,153 (86 functions) |
| 32 | Sub_CB0A |
$C0:CB0A–$CB39 |
48 | |
| 32 | Sub_D124 |
$C0:D124–$D289 |
358 | |
| 32 | Sub_DD28 |
$C0:DD28–$E129 |
1,026 | 19,585 (89) |
| 33 | Obj_AnimTickAndQueue |
$C0:C98A |
236 | |
| 33 | Field_ProcessAnimQueue |
$C0:CA76 |
99 | |
| 33 | Obj_BuildSpriteFrameStep |
$C0:CAD9 |
49 | |
| Total | 3,509 |
Session totals: 1,693, 1,432 and 384 bytes. After session 31 the quoted figures were 28.6% of bank $C0 and 5.1% of all game code; after session 32, bank $C0 went from 17,682 bytes (28.6%) to 19,114 (30.9%) and all code from 18,153 (5.1%) to 19,585 (5.5%).
Next
Bank $C0 still has the large $2E1E–$595B and $59D9–$B191 regions, but the sprite system is fully closed. The next arc is bank $C1: start at $C1:0089 with Battle_Mul8, open the file, and work through the named routines in order.