Declare dScMgBase_c slot 35 (Virtual8C) — the 18-35 range is complete - #2112
Conversation
The last of the eighteen. With this the minigame family emits its vtables
from source at full length: 32 of the 33 family tables now score VERIFIED
against the cartridge at 144 bytes / 36 slots, where before slot 18 every
one of them was an 18-slot prefix.
Slot 35 is a predicate on the scene's own spawn parameter. The base body
(ov004:0x020ad660, 20 bytes) returns `(param1 & 0xff) != 0` — fBase_c's
word at +0x08. dScMgAmida_c overrides it (ov006:0x020d1170, 24 bytes) to
ask `== 1`. Nothing else in the family overrides it, so 31 of the 32
tables carry the base's answer.
It has the strongest call-site evidence in the campaign. Thirteen dispatch
sites, all in ov006, all `mov r0,<this>; ldr r1,[r0]; ldr r1,[r1,#0x8c];
blx r1`, spread across four leaf classes' code regions — dScMgCoin_c (2),
dScMgPanel_c (4), dScMgSound_c (3), dScMgSnowball_c (4). Each is a class
asking the question of itself and branching on the answer to pick between
two variants of the same minigame. All 13 of 13 follow the call with
`cmp r0, #0`: the first slot in this range whose return value is seen to
be consumed rather than merely permitted. Arity is measured the same way —
r1 is the loaded pointer at every site and r2/r3 are never set up, so
`this` only.
The rename was keyed on the module-qualified symbol, not the address:
0x020ad660 is an overlay load base, so ov000, ov002, ov003, ov004 and
ov007 each have a different unrelated symbol there (ov003's is
dScTitle_c's D1). Keying on the address would have hit five files in four
unrelated overlays and every byte gate would still have passed.
dScMgAmida_c::Unk36 moves from 35 to 36, which is where the ROM puts it.
That closes the last DIFFERS in the family.
Verified: rombuild 11,088/11,088 reproducing, 106/106 modules exact,
100.000000%. romdata_check against the slot-34 baseline: verified symbols
465 -> 496, verified bytes 35,428 -> 39,900, partial 253 -> 223, differs
6 -> 5. Unlike slots 18-34 this is NOT +4/+124 per table — completing a
table moves the whole table, so 30 flipped PARTIAL -> VERIFIED and
_ZTV12dScMgAmida_c flipped DIFFERS -> VERIFIED.
One family table stays PARTIAL and it is not a missing virtual:
_ZTV12dScMgSlot3_c emits 144 bytes against a romExtent of 152. The eight
bytes past the end are {0, _ZTI12dScMgSlot1_c}, the header of the next
vtable in the image — dsd points a _ZTV symbol at slot 0, so those two
words belong to no symbol and its "extends to the next symbol" sizing
swallows them. A symbols.txt extent fix, with its own gates, not a
keystone change. Recorded in notes/dScMgBase_c-slots-18-35.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhhAeJwXBnfPp7B5DNjCwh
✅ PR validation — PassedCommitted merge passes; 2 contributor credit reassignment(s) noted, not a blocker. Full merge validation
Contributor credit moved (2)
Byte-verified means the range carries Warnings: contributor attribution changed, not a blocker (2 changed, 0 lost -- src/_ZN11dScMgBase_c9Virtual8CEv.c: tangosdev -> andrewboudreau; src/_ZN12dScMgAmida_c9Virtual8CEv.c: tangosdev -> andrewboudreau). Per-file link-check detailAll 192 changed file(s) compile to the ROM byte-for-byte with correct relocation targets.
The private worker commits a test merge, builds the stock ROM profile, compares every executable module, measures matched and source-built code, checks contributor lineage, and verifies affected relocations. The mod profile is opt-in and is not part of this merge gate. |
Self-review — slot 35, and the end of the 18-35 rangeReviewing my own work because no PR in this repo can be APPROVED — one shared account, so this is a written sign-off, not a GitHub approval. Findings are inline rather than linked, per the standing rule that a reviewer should not have to chase a second artifact. What landed
Two renames, both scoped by module-qualified symbol rather than by address: 0x020ad660 is an overlay load base. ov000, ov002, ov003, ov004 and ov007 each have a different, unrelated symbol at that address — ov003's is Call-site evidence — the strongest in the campaignThirteen dispatch sites in ov006, spread across four leaf classes that are not the declaring class: All 13 of 13 follow with Three representative uses: Verification
The measurement, and why it does not look like slots 18-34Every slot from 18 to 34 moved exactly 32 tables x 4 bytes = 128, split Completing a table does not add a word to it — it moves the whole table from PARTIAL to VERIFIED:
Thirty tables flipped PARTIAL to VERIFIED and Per-symbol (driven through 32 of 33 family tables are VERIFIED at 144 bytes / 36 slots with Finding 1 — the one remaining PARTIAL is a dsd config artifact, not a class-model gap. NOT fixed here, deliberately.The eight bytes past the end read This is the mirror image of the standing "a VERIFIED vtable is not a complete vtable" hazard — same cause, opposite direction: the symbol boundaries are dsd's guesses, not the ABI's. Why it is not in this PR: the fix is a one-line A reviewer who takes a PARTIAL vtable at face value here would go looking for two virtuals that do not exist. Finding 2 — two process traps this PR tripped over
Finding 3 — a claim I explicitly did not make
Likewise Prose re-countedFour files carried sentences the declaration invalidates, all updated to past tense with the new state stated positively:
The thirteen borrowed VerdictSigned off. Stack context: base is |
andrewboudreau
left a comment
There was a problem hiding this comment.
Reviewed against the cartridge. Approve on content — with one count correction and one lever disagreement.
I rebuilt this head in a wired worktree and independently re-derived every ROM claim in the PR body and the notes file. Almost all of it holds, and the return-type evidence here is the strongest the slot campaign has produced. Two things need changing before merge, neither of them a rebuild.
What I reproduced
rombuild.py -j16 --no-rom --data-json at b2ffb512ca64:
source-built functions: 11,088 reproducing: 11,088 mismatching: 0
module fidelity: 106/106 exact, 100.000000% of compared bytes
ROM data from source: 496 verified, 223 partial, 5 differ, 522 unnamed by config
Exactly the counters in the table. Premerge, both bases:
vs origin/cpp/minigame-slot34 @bdb2510ca600 merge tree 4d4dfb4c60c3 8/8 pass -> pass
vs origin/main @5e88bfb5ed0f merge tree fe50770a3b71 8/8 pass -> pass
Independently confirmed from the ROM image:
- Both bodies. ov004:
0x020ad660, 20 bytes:ldr r0,[r0,#8] / ands r1,r0,#0xff / movne r0,#1 / moveq r0,#0 / bx lr—(this[2] & 0xff) != 0. ov006:0x020d1170, 24 bytes:and r0,r0,#0xff / cmp r0,#1 / moveq r0,#1 / movne r0,#0—== 1. Both match the source exactly. - Thirteen dispatch sites, all in ov006, found with an 8-word lookahead window and break-on-redefinition (an adjacency scan finds only ten — worth knowing if anyone re-derives this).
- 13 of 13 followed by
cmp r0, #0. The return-type claim is fully earned. This really is the first slot in the range where the ROM is seen consuming the value rather than merely permitting one. - Class attribution. All 13 sit inside unnamed
func_ov006_*symbols, so I bracketed each with the nearest_ZNsymbol before and after inconfig/arm9/overlays/ov006/symbols.txt. Both brackets agree on all thirteen, and the split is exactly yours:dScMgCoin_c2,dScMgPanel_c4,dScMgSound_c3,dScMgSnowball_c4. - The five-overlay rename hazard, including ov000 — which I checked specifically because
overlays.yamlbases ov000 at0x020aa420and it looked like an error. It isn't: ov000 spans0x020ad660and carriesdata_ov000_020ad660 … ambiguousthere. The list is right and better-evidenced than it first reads. - The Slot3 residual, to the byte.
_ZTV12dScMgSlot3_c0x0213eaa8,_ZTV12dScMgSlot1_c0x0213eb40, gap exactly 152. ROM at0x0213eb38is{0x00000000, 0x0213e5a0}and0x0213e5a0is_ZTI12dScMgSlot1_c. Your reading of it is correct.
1. The "32 of 33" headline is inflated by one table — and the odd one out is hiding this PR's own slot
Measured at this head, from --data-json, deduped on (module, symbol, addr):
(bytes 144, romExtent 144, VERIFIED) x30
(bytes 148, romExtent 148, VERIFIED) _ZTV12dScMgAmida_c
(bytes 144, romExtent 80, VERIFIED) _ZTV14dScMgD3DBase_c <-- here
(bytes 144, romExtent 152, PARTIAL ) _ZTV12dScMgSlot3_c
(bytes 8, romExtent 8, VERIFIED) _ZTVN12dScMgSlot1_c9betIcon_cE (nested, excluded from the 33)
Thirty-two do score VERIFIED. But the notes say something stronger than that:
Thirty-two of the thirty-three family tables read exactly like that — 144 bytes, 36 slots, emitted length equal to the cartridge's extent — except
_ZTV12dScMgAmida_c, which is 148…
That sentence is true of thirty-one. _ZTV14dScMgD3DBase_c emits 144 against a config romExtent of 80, and it scores VERIFIED only because check_symbol's verdict test is len(linked) >= extent — an over-emitting table passes. Its extent is 80 because data_ov006_0213c67c sits inside the table and rom_data_index() sizes every data symbol as distance-to-the-next-symbol.
I dumped all 36 slots. Slots 20–35 are all valid ov006/ov00x code pointers, and the table genuinely ends at 0x0213c6bc with the standard {0, 0x0213c5c8} preamble at V−8:
slot 20 0x213c67c 020b2990 <<< data_ov006_0213c67c — the config extent stops here
slot 21 0x213c680 020b298c
...
slot 34 0x213c6b4 020ae3b4 <- #2110's Virtual88 base body
slot 35 0x213c6b8 020ad660 <- THIS PR's Virtual8C base body
Sixteen of its slots are never compared, including slot 34 from #2110 and slot 35 from this commit. That is the "a VERIFIED vtable is not a complete vtable" hazard you name by name two paragraphs later, occurring inside your own count. The delta table is unaffected — D3DBase contributed 0 to the +31 (30 flips + Amida), so the arithmetic is fine; it's the prose claim that overreaches.
Ask: restate as 31 tables read exactly like the sample; 32 score VERIFIED, one of them on a truncated config extent that hides 16 slots (_ZTV14dScMgD3DBase_c, extent 80 of a real 144); 1 remains PARTIAL. Prose only, in both the PR body and notes/dScMgBase_c-slots-18-35.md.
2. The Slot3 fix belongs in romdata_check.py, not in symbols.txt
That is a
config/arm9/overlays/ov006/symbols.txtchange with its own gates
I argued against exactly this on #2111 a few hours ago, so let me put the measurement here rather than ask you to go read it.
This is not a one-off. On origin/main there are 540 _ZTV symbols, 18 owned by source, 522 unowned, and the two-word mwcc preamble at V−8 belongs to no symbol in any of them. 48 of the 256 live PARTIAL records are partial solely because romExtent − padded_emitted == 8 with a _ZTV as the next symbol. _ZTV12dScMgSlot3_c is one of them — instance #523 of a tool defect, not a config error.
check_symbol already knows about the preamble: it applies preamble = OI.VTABLE_PREAMBLE if name.startswith("_ZTV") else 0 on the emitted side and corrects reloc addends by the same 8. Only rom_data_index() doesn't, and that is a one-line change there — it fixes all 522 at once, and the 18 already-owned tables plus this PR's own 30 flips are its positive control.
A symbols.txt row here would instead be a row the next intact-object promotion of dScMgSlot1_c has to delete again (a promoted TU must drop the V−8 row, and must not invent a name for it — that is settled anti-pattern #3 from #2061).
Ask: drop the "that is a symbols.txt change" sentence and point at rom_data_index() instead. I'll take the tool fix as a separate tools-only PR onto main — this PR gives it a second free positive control, so I'm not asking you to carry it.
Non-blocking
3. "r1 … at every one of the thirteen sites" is false at one site. Twelve load the pointer into r1; 0x020dd9cc loads it into r2:
0x20dd9c4 ldr r2, [r0] <- vptr
0x20dd9cc ldr r2, [r2, #0x8c] <- pointer in r2
0x20dd9d8 sub r7, r1, fp <- r1 written, but consumed here, before the call
0x20dd9dc blx r2
The arity conclusion survives — r1 there is a local computation dead before the blx, r3 is never set, and the other twelve are decisive on their own. Just don't say "every one of the thirteen". (Related: the gap between ldr and blx is 1 at ten sites, 2 at two, 4 at one, so "all the same shape" is a little loose too.)
4. 0x0211b9e0 selects a different table, not "a different field path". Both arms execute the identical add r0,r4,#0x5000 ; ldrb r1,[r0,#0x627] — same field. They diverge only on which literal-pool pointer they load: ldrne r0,[pc,#0x8c] → 0x0211ba80 holding 0x0212ef7c, vs ldreq r0,[pc,#0x7c] → 0x0211ba84 holding 0x0212ef8c. So it's the same shape as 0x02105488 (two parallel tables), not a third distinct one. Your other two descriptions are exact: 0x02105488 is ldrne r0,[r8,sb,lsl#2] / ldreq r0,[r6,sb,lsl#2], and 0x02126f58 is beq #0x2126f7c, skipping 8 instructions.
5. Column-0 line inside an indented comment block — include/dScMgBase_c.h:
+ALL EIGHTEEN ARE DECLARED (2026-08-31). This class and all 32 of
+ its descendants emit their full 36-slot vtables from source; …
The first line starts at column 0 inside a block indented 7 spaces, which reads at a glance as if the comment had terminated. Re-indent it.
Verdict
Approve on content. The slot itself is correct, the bodies are byte-verified, the arity and return type are both properly earned, and the range really is complete. Fixes 1 and 2 are prose-only; 3–5 are notes. No rebuild needed for any of them — I'll re-read and merge once they're in.
Nice piece of work on the dispatch evidence in particular. Thirteen sites across four classes with a uniform cmp r0,#0 afterwards is the first time in this campaign the ROM has answered the return-type question instead of just declining to contradict it.
The last of the eighteen. Slots 18-35 of
dScMgBase_care now all declared, one slot per PR, each with every descendant override of that slot. This is the one that closes the range.What it buys
The family stops being a prefix. Before slot 18, all 32 descendants emitted an 18-slot vtable against a cartridge table of 36 — byte-exact as far as it went, and unownable. After this commit 32 of the 33 family tables score VERIFIED at 144 bytes / 36 slots, emitted length equal to the ROM's extent,
blindWords 0:_ZTV12dScMgAmida_cis 148 because slot 36 is genuinely its own.What slot 35 is
A predicate on the scene's own spawn parameter. The base body (ov004:0x020ad660, 20 bytes) returns
(param1 & 0xff) != 0—fBase_c's word at+0x08, the one everyfBase_cis constructed with.dScMgAmida_coverrides it (ov006:0x020d1170, 24 bytes) to ask== 1. Nothing else in the family overrides it: 31 of 32 tables carry the base's answer.The evidence, which is the best in the campaign
Thirteen dispatch sites, all in ov006, all the same shape —
mov r0,<this>; ldr r1,[r0]; ldr r1,[r1,#0x8c]; blx r1— spread across four leaf classes' code regions:dScMgCoin_c(2),dScMgPanel_c(4),dScMgSound_c(3),dScMgSnowball_c(4). Each is a class asking the question of itself, then branching to pick between two variants of the same minigame: two asset tables at 0x02105488, a different field path at 0x0211b9e0, a whole block skipped at 0x02126f58.Arity: zero explicit parameters, measured. r1 is the loaded function pointer at every one of the thirteen sites and so cannot also be an argument; r2/r3 are never set up.
Return type
int, and this is the first slot where the ROM is seen to consume one. All 13 of 13 sites follow the call withcmp r0, #0. Earlier slots only established that a return value was permitted.The rename hazard
0x020ad660is an overlay load base, so ov000, ov002, ov003, ov004 and ov007 each have a different, unrelated symbol at that address — ov003's isdScTitle_c's D1. The rename was keyed on the module-qualified symbolfunc_ov004_020ad660, which is whatapply_renametakes. Keying it on the address would have hit five files in four unrelated overlays and every byte gate would still have passed.dScMgAmida_c::Unk36Moves from 35 to 36, where the ROM puts it. That was the last
DIFFERSin the minigame family and it is closed.The header also flags something now testable that was not before:
Unk36's three call sites still use the pre-migration vtable-shim dispatch, and the reason they had to (mwcc landingUnk36right after the base's compiler-visible virtuals) is gone. The shims are kept here — that is a measurement nobody has taken, and replacing them is its own commit with its own rombuild behind it.Verification
rombuild -j16 --no-rom: 11,088 / 11,088 reproducing, 106/106 modules exact, 100.000000%romdata_checkagainst the slot-34 baseline:Slots 18-34 each moved exactly 128 bytes (+4 verified / +124 partial, one word into each of 32 tables). Slot 35 does not, and that is the point. Completing a table doesn't add a word to it — it moves the whole table. Thirty flipped PARTIAL → VERIFIED and
_ZTV12dScMgAmida_cflipped DIFFERS → VERIFIED.check_dead_references,port_refcheck,check_duplicate_sources,check_header_offsets --changed origin/main,check_src_tu_compiles,langmode_audit --check: all exit 0.One residual, and it is not a missing virtual
_ZTV12dScMgSlot3_cstill scores PARTIAL: emitted 144,romExtent152. The eight bytes past the end read{0x00000000, 0x0213e5a0}, and 0x0213e5a0 is_ZTI12dScMgSlot1_c— the offset-to-top / typeinfo header of the next vtable in the image._ZTV12dScMgSlot3_csits at 0x0213eaa8 and_ZTV12dScMgSlot1_cat 0x0213eb40, exactly 152 apart; dsd points a_ZTVsymbol at slot 0 rather than at the header, so those two words belong to no symbol and dsd's "extends to the next symbol" sizing swallows them.So the class model is right and the config extent is eight bytes long. That is a
config/arm9/overlays/ov006/symbols.txtchange with its own gates, deliberately not folded into a keystone commit. Recorded innotes/dScMgBase_c-slots-18-35.md.It is the mirror image of the standing "a VERIFIED vtable is not a complete vtable" hazard: there dsd cuts a table short, here it runs one past.
Based on
cpp/minigame-slot34(#2110). Attribution deltas are a stated non-goal for this repo —attribution-overrideapplied.🤖 Generated with Claude Code
https://claude.ai/code/session_01QhhAeJwXBnfPp7B5DNjCwh