11 August 2026: the sprite-slot cluster takes shape, and an honest progress number
Five body routines of the sprite-slot init cluster are matched, then the triple-slot dispatch. In between, the progress number gets a real denominator: matched code against the game's code, not against the whole 4 MB ROM. 2,377 new bytes in 7 functions (sessions 29 and 30).
A word on yesterday's correction
Session 27's notes made it sound as if routines like $CF2D and $D299 went through a "midlayer" around $47xx before calling the slot allocators. Disassembling them showed they call Sub_E97A directly; the $47xx block is a separate, unrelated region. I had sketched a call graph from cross-reference data without checking the bytes at each address. Corrections like this are cheap, because make diff enforces the bytes and a wrong description of who calls what can't corrupt the output. The discipline is to check the disassembly before writing prose about call graphs.
The template, confirmed
Yesterday's scoping described a template: gate → same-frame check → allocate slots → tile-data pointer → DMA loop → OAM write → return. Matching five routines showed it is real. The routines differ in:
- how many slots: one (
Sub_E952), two (Sub_E97A) or three (Sub_E9AA) - whether the animation gate runs:
Sub_CBDCcallsSub_CB3A;Sub_CDC8doesn't - tile count and stride: 16 tiles ×
$28, 16 tiles ×$50, or 32 tiles ×$50 - the starting Y offset in the tile data:
$00for the first pass,$20for the second - which OAM staging buffer:
$7F:4802+slotor$7F:4BC2+slot - what goes into
$1B00,X:INC,INCtwice, or set to$80 - the return carry:
SECon first-pass success,CLCon OAM-only or second-pass
Once you see the template, each routine is a checklist. The differences are often a single byte: a multiply constant, a Y start, an OAM base address.
Sub_CBDC: single slot, gated, 16 tiles
Sub_CBDC ($CBDC–$CDC7, 492 bytes) calls Sub_CB3A first, unless $1780,X == $03, in which case it reads the frame byte straight from $1301,X. If the gate passes and the frame is new, it claims one VRAM slot with Sub_E952, runs a 16-pass DMA loop with multiplier $28 (40 bytes per frame), writes four tile entries to $7F:4802+slot, and ends with CLC.
One detail: after STA $D9 (the palette-and-priority value), A still holds that value, and the first ORA $0C00,Y uses it directly. My first draft had an extra LDA $D9; the diff caught it.
Sub_CDC8: OAM only
Sub_CDC8 ($CDC8–$CEF4, 301 bytes) is the simplest: no gate, no slot allocation, no DMA loop. It computes the tile-data pointer from $0F01,X × $28 + $1380,X, loads the OAM slot index from $1700,X, and writes four Y entries (with sign extension into the high byte) and four X entries (entity base plus 2-step increments) to $7F:4BC2+slot. Then $1B00,X = $80 and CLC. It updates screen positions for entities whose tiles are already loaded, a light pass after the heavy init routines.
Sub_CEF5: the three-way dispatch
Sub_CEF5 ($CEF5–$D123, 559 bytes; scoping had guessed ~660 for $CEF5–$D289) loads tile-table pointers in its header ($CEF5–$CF20) and checks $1780,X:
$1780,X == $03: a full 32-tile dual-slot init inline ($CF21–$D123). It allocates two slots withSub_E97A, runs 32 passes (multiplier$50), writes eight OAM entries starting at tile-data offset Y=$40, increments$1B00,Xtwice (at$D11Aand$D11D) and returnsCLC. The double increment records that both halves loaded in one call.- otherwise, check
$1B00,X & $7F. Zero (first pass):BRL $0369toSub_D28A. Non-zero (second pass or later):BRL $03EFtoSub_D30D.
Both BRLs are tail calls; the target's RTS returns to Sub_CEF5's caller. They need raw offsets because asar's BRL takes a literal displacement when the target is in another org block; see asar Quirks.
Sub_D28A and Sub_D30D: a load split across two calls
Sub_D28A ($D28A–$D30C, 131 bytes) is the first-pass dual-slot routine. It calls Sub_CB3A, compares the frame byte with $0F01,X, allocates two slots with Sub_E97A, and runs a 16-pass tile loop from Y=0 with multiplier $50 to VRAM base $0D80,X. On success it increments $1B00,X and returns SEC.
Sub_D30D ($D30D–$D4F6, 490 bytes; scoping had guessed ~569) is the second pass. No gate, no allocation: it reuses what Sub_D28A set up on the previous call. It uses VRAM base + $0200 for the second slot, starts the tile loop at Y=$20 (the second half of the 32-tile frame), and writes eight OAM entries from tile-data offset Y=$40. Then it increments $1B00,X and returns CLC.
So Sub_CEF5 does the load in two calls (corrected later: not one per frame; the animation queue calls again at once while carry is set and the blanking period has time left, so both can run in the same frame). First call ($1B00,X & $7F == 0): Sub_D28A loads tiles 0–15 and returns SEC. Second call (== 1): Sub_D30D loads tiles 16–31 and returns CLC. The pass counter in $1B00,X tracks which half is pending. Sub_D30D is much longer because it has the eight-entry OAM block that Sub_D28A leaves to it.
One 8-tile frame in two calls. Pass 0 returns carry set, "call me again"; the animation queue calls again while it has time, so pass 1 can run in the same blanking period or a later one.
The denominator problem
I had been quoting progress like "0.38% of the ROM". That is true and misleading: most of a 4 MB SNES RPG is character sprites, tilesets, enemy graphics, maps, battle backgrounds, music samples and compressed audio, not code.
So I scanned the dscotton reference disassembly. Each bank file pulls raw asset data in with incbin. Summing those ranges and subtracting from each bank's 65,536 bytes gives the code per bank:
- ROM total: 4,194,304 bytes
- Asset data (incbin): ~3,835,026 bytes (91.4%)
- 65816 code: ~359,278 bytes (8.6%)
Against 359,278 instead of 4,194,304, the 16,460 matched bytes are 4.6%, not 0.38%. Bank $C0 has 61,779 bytes of code, so 15,989 matched there is 25.9%, a quarter of the bank.
The same 16,460 matched bytes against two totals. Against the whole ROM they barely show; against the code they are 4.6%.
The breakdown also shows which banks are worth targeting:
$C1and$CBare 97% and 94% code, nearly as dense as$C0: the obvious next big targets.$C2is 84% code.$CA,$CCand$FDmix significant code with data.$C4,$C5,$C8,$C9,$D0and most of$D2–$EFare pure asset banks, 0% code. No point disassembling them.
The full per-bank density table is on the ROM Map page.
Triple-slot dispatch: Sub_D4F7 and Sub_D608
The two "triple-slot stubs" from yesterday's table were estimates. Disassembling at $C0:D546 showed more structure. The code at $D4F7 is a dispatch routine, the triple-slot counterpart of Sub_CEF5. It:
- sets up graphics-bank parameters (
$CF,$D2,$CD,$D8) - bails at once for type 3 sprites, like the rest of the cluster
- reads
$1B00,X & $7F, the pass counter - checks whether
$0D00,Xequals$68, a sprite-size flag that picks between two load strategies
The paths:
- Pass 0, not
$68: falls through toSub_D546in the same body: two 8-tile DMA loops, to the WRAM slot base and base +$0200. - Pass 0,
$68:BRLtoSub_D608, one 16-tile loop to the WRAM base. - Pass 1, not
$68:BRLtoSub_D68B(unmatched). - Pass 1,
$68:BRLtoSub_D738(unmatched). - Pass 2+: two more
BRLtargets, unmatched.
Sub_D546 is reached by a short BRA, not a JSR, so it is an internal label. It is written as part of Sub_D4F7's body (273 bytes in all), with Sub_D546 kept as a global label for tracking. Sub_D608 is a real routine (131 bytes): animation gate, triple-slot allocator, 16-tile DMA, pass counter increment, SEC.
Both assembled cleanly, with a full 4,194,304-byte match on the first try.
Where the cluster stands
| Routine | Range | Bytes | Status |
|---|---|---|---|
Sub_CB3A |
$CB3A–$CBDB | 162 | Matched — animation-frame gate |
Sub_CBDC |
$CBDC–$CDC7 | 492 | Matched — single-slot, gated, 16-tile |
Sub_CDC8 |
$CDC8–$CEF4 | 301 | Matched — single-slot, OAM-only |
Sub_CEF5 |
$CEF5–$D123 | 559 | Matched — three-way dispatch |
Sub_D28A |
$D28A–$D30C | 131 | Matched — dual-slot first pass |
Sub_D30D |
$D30D–$D4F6 | 490 | Matched — dual-slot second pass |
Sub_D4F7 (incl. Sub_D546) |
$D4F7–$D607 | 273 | Matched — triple-slot dispatch |
Sub_D608 |
$D608–$D68A | 131 | Matched — triple-slot pass 0, $68 |
Sub_D68B, Sub_D738, Sub_D7E5, Sub_DA69 |
— | — | Identified (pass 1 and pass 2+) |
Pass 1 and pass 2+ load tiles for the second and third slots of a triple-slot sprite in separate calls. They should look like Sub_D28A/Sub_D30D, writing to WRAM base + $100 and base + $200 in turn.
The day in numbers
| Session | Routine | Address | Bytes | Running total quoted |
|---|---|---|---|---|
| 29 | Sub_CBDC |
$C0:CBDC–$CDC7 |
492 | |
| 29 | Sub_CDC8 |
$C0:CDC8–$CEF4 |
301 | |
| 29 | Sub_CEF5 |
$C0:CEF5–$D123 |
559 | |
| 29 | Sub_D28A |
$C0:D28A–$D30C |
131 | |
| 29 | Sub_D30D |
$C0:D30D–$D4F6 |
490 | 16,056 (80 functions) |
| 30 | Sub_D4F7 (incl. Sub_D546) |
$C0:D4F7–$D607 |
273 | |
| 30 | Sub_D608 |
$C0:D608–$D68A |
131 | 16,460 (82) |
| Total | 2,377 |
End of day: 16,460 / 359,278 bytes of game code (4.6%); bank $C0 15,989 / 61,779 (25.9%).
Next
The four remaining triple-slot routines: Sub_D68B, Sub_D738, Sub_D7E5 and Sub_DA69.