Skip to content

Promote ov006/dScMgBSC_c to a single 19-function C++ TU - #2067

Merged
andrewboudreau merged 3 commits into
mainfrom
cpp/minigame-tu-2
Aug 31, 2026
Merged

Promote ov006/dScMgBSC_c to a single 19-function C++ TU#2067
andrewboudreau merged 3 commits into
mainfrom
cpp/minigame-tu-2

Conversation

@andrewboudreau

Copy link
Copy Markdown
Collaborator

Stacked on #2064 — review that one first; this branch contains its commit.

src_tu/actors/dScMgBSC_c.cppsrc/actors/dScMgBSC_c.cpp, replacing 19 per-function legacy sources (14 flat-C, 5 already C++). The whole .text range 0x02124908..0x021255f8 now reproduces from one object.

The destructor is inline, and that is the load-bearing part

An out-of-line ~dScMgBSC_c() makes mwcc emit the synthesized D0 ahead of the written D1. The cartridge has D1 first, and linkcheck's [4b/8] pre-link licensing audit refuses a TU whose licensed .text is not in ROM address order — it refused this one:

emitted: ['_ZN10dScMgBSC_cD0Ev', '_ZN10dScMgBSC_cD1Ev', 'func_ov006_02124a04', ...]
REFUSED -- licensed .text functions are not emitted in ROM address order

Defining the destructor inline in the class body hands the key-function role to InitResources — the next virtual declared, non-inline, and defined in this TU. That emits _ZTV10dScMgBSC_c here; slots 16 and 17 name D1 then D0, odr-using both, so the compiler emits the pair for us, in cartridge order. It also deletes the homeless D2 outright, which is why compiler_only_output drops 14 → 13 rows. (A stale row there rejects the whole policy block — all 13 deadstrip-data rows come back as EXTRA until the dead D2 row is dropped.)

Verification

check result
tubuild verify ov006/dScMgBSC_c 19/19 MATCH, objisolate clean, exact deadstrip, ROM-ascending order
tubuild linkcheck --module ov006 SCRATCH-LINK-VERIFIED; 106/106 exact; ROM sha256 d1506e90… identical to stock
rombuild -j16 11,088 source-built, reproducing 11,088, mismatching 0; storage aliases exact
ROM data from source 462 verified (was 461), 250 partial, 9 differ — unchanged
premerge_check HEAD all 8 gates pass/pass on the merge tree
port_refcheck / attribution 405 resolve, 0 stale · 24 consolidated, 0 lost

The +1 verified record and +1 partial are the RTTI and the emitted _ZTV10dScMgBSC_c — 18 of the cartridge's 36 slots, now byte-compared against the cartridge rather than trusted.

Two merge-tree-only reds this had to fix

Both were green on the branch and red only on the merge result, so only premerge_check.py surfaces them:

  1. header-offsets — the inline body was written with an Allman brace. check_header_offsets.py's skip_body only engages when the signature line itself contains {, so the bare { fell through to DECL parsing (UNPARSED include/dScMgBSC_c.h:53: {). Worse than a red: one unparsed line suppresses the field walk for the whole header, so it reported 0 commented fields — hollow green, not a pass. Brace moved to the signature line, matching every existing inline dtor in include/; now 4 commented fields, 0 mismatched, 0 unparsed and the span corrects 0x4f380x51d0.
  2. dead-references — deleting 19 legacy sources stranded a prose reference in notes/minigame-provenance.md. Repointed at the consolidated path.

Deliberately not in this PR

Naming dScMgBase_c slots 18–35 would widen the emitted vtable past slot 17 and every descendant's with it. This TU independently confirms three of those slots from its own table (18 → Virtual48, 19 → OnTurnIntoEgg, 21 → OnGroundPounded); that evidence is recorded in notes/dScMgBase_c-slots-18-35.md for the atomic change that does the rename.

Converted-tier accounting is flat: 3 identities migrated, 3 score 5/5 — Render via its @symbol marker, D1/D0 via the inline lifecycle fragment.

🤖 Generated with Claude Code

@tangos-validator

tangos-validator Bot commented Aug 31, 2026

Copy link
Copy Markdown

✅ PR validation — Passed

Committed merge passes; 17 contributor credit reassignment(s) noted, not a blocker.

Full merge validation

Check Result
Committed test merge yes
Byte-verified functions 10,875 / 11,347 (95.84%, -18)
Byte-verified code bytes 2,029,252 / 2,211,124 (91.77%, -3,196)
Claimed, not byte-verified 342 functions, 76,428 bytes (+18)
Perfect source moves 0 R100
Enrolled ranges (delinks complete) 11,059 functions, 2,053,148 bytes (92.86%, +0) -- differs from byte-verified by +184
Contributor credit 0 added, 17 changed, 0 lost
Relocation check 19 checked; 19 VERIFIED
Port reference check 405 checked; 0 stale
Module fidelity 106/106 exact; 100.000000% compared bytes
Code linked from verified source 11,088 functions, 2,067,148 bytes (93.49%)
Module bytes from source 2,067,148 / 3,049,600 (67.8%); 811,492 (26.6%) are data no delink entry reaches
ROM data reproduced from source 462 symbol(s) exact, 250 partial, 9 differ

Contributor credit moved (17)

Function Source Before After
ov006:0x02124a04 src/func_ov006_02124a04.c -> src/actors/dScMgBSC_c.cpp tangosdev github-actions[bot]
ov006:0x02124a08 src/func_ov006_02124a08.c -> src/actors/dScMgBSC_c.cpp tangosdev github-actions[bot]
ov006:0x02124ae4 src/func_ov006_02124ae4.c -> src/actors/dScMgBSC_c.cpp tangosdev github-actions[bot]
ov006:0x02124b58 src/func_ov006_02124b58.c -> src/actors/dScMgBSC_c.cpp tangosdev github-actions[bot]
ov006:0x02124bb4 src/func_ov006_02124bb4.c -> src/actors/dScMgBSC_c.cpp tangosdev github-actions[bot]
ov006:0x02124cb4 src/func_ov006_02124cb4.c -> src/actors/dScMgBSC_c.cpp andrewboudreau github-actions[bot]
ov006:0x02124dc0 src/func_ov006_02124dc0.c -> src/actors/dScMgBSC_c.cpp tangosdev github-actions[bot]
ov006:0x02124e1c src/func_ov006_02124e1c.cpp -> src/actors/dScMgBSC_c.cpp tangosdev github-actions[bot]
ov006:0x02124ec4 src/func_ov006_02124ec4.c -> src/actors/dScMgBSC_c.cpp lunavyqo github-actions[bot]
ov006:0x02124fd8 src/func_ov006_02124fd8.c -> src/actors/dScMgBSC_c.cpp lunavyqo github-actions[bot]
ov006:0x021250e4 src/func_ov006_021250e4.c -> src/actors/dScMgBSC_c.cpp tangosdev github-actions[bot]
ov006:0x02125248 src/func_ov006_02125248.c -> src/actors/dScMgBSC_c.cpp ruspecial github-actions[bot]
ov006:0x0212527c src/func_ov006_0212527c.cpp -> src/actors/dScMgBSC_c.cpp tangosdev github-actions[bot]
ov006:0x02125364 src/func_ov006_02125364.cpp -> src/actors/dScMgBSC_c.cpp tangosdev github-actions[bot]
ov006:0x021253bc src/_ZN10dScMgBSC_c6RenderEv.cpp -> src/actors/dScMgBSC_c.cpp andrewboudreau github-actions[bot]
ov006:0x021254c0 src/_ZN10dScMgBSC_c8BehaviorEv.cpp -> src/actors/dScMgBSC_c.cpp andrewboudreau github-actions[bot]
ov006:0x0212551c src/_ZN10dScMgBSC_c13InitResourcesEv.cpp -> src/actors/dScMgBSC_c.cpp tangosdev github-actions[bot]

Byte-verified means the range carries complete in a delinks.txt, so the ROM build compiled it and compared it to the cartridge. The 342 claimed functions have a src/ file named after the symbol with no NONMATCHING banner, and nothing compiles them -- dsd fills their addresses with the ROM's own bytes. Both together are the 11,217 this project calls matched.

Warnings: contributor attribution changed, not a blocker (17 changed, 0 lost -- src/actors/dScMgBSC_c.cpp: tangosdev -> github-actions[bot]; src/actors/dScMgBSC_c.cpp: tangosdev -> github-actions[bot]; src/actors/dScMgBSC_c.cpp: tangosdev -> github-actions[bot]; +14 more); 19 address range(s) left the byte-verified set while enrolled totals held steady: ov006:0x02124908-0x0212497c, ov006:0x0212497c-0x02124a04, ov006:0x02124a04-0x02124a08, ov006:0x02124a08-0x02124ae4, ov006:0x02124ae4-0x02124b58, +14 more; 18 more function(s) now claim a match that nothing compiles; enroll them in a delinks.txt to have the ROM build check them.

Per-file link-check detail

All 1 changed file(s) compile to the ROM byte-for-byte with correct relocation targets.

File Symbol Result Slots checked
src/actors/dScMgBSC_c.cpp _ZN10dScMgBSC_cD1Ev + _ZN10dScMgBSC_cD0Ev + func_ov006_02124a04 + func_ov006_02124a08 + func_ov006_02124ae4 + func_ov006_02124b58 + func_ov006_02124bb4 + func_ov006_02124cb4 + func_ov006_02124dc0 + func_ov006_02124e1c + func_ov006_02124ec4 + func_ov006_02124fd8 + func_ov006_021250e4 + func_ov006_02125248 + func_ov006_0212527c + func_ov006_02125364 + _ZN10dScMgBSC_c6RenderEv + _ZN10dScMgBSC_c8BehaviorEv + _ZN10dScMgBSC_c13InitResourcesEv ✅ verified 19

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.

@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Changes requested — eleven @symbol markers. Everything else here I re-derived from the cartridge and it holds.

Reviewed at head 7c0f9a92c79419bfd8a844fc4d90485718957ffd, base cpp/minigame-tu @ 0fda8b4d1b5b. This is a good promotion and the evidence is better than most that come through here. One thing needs fixing before it lands, and it is mechanical.


The blocker: 11 of 17 hand-written members have no // @symbol

notes/tu-promotion-conventions.md §6 landed three PRs ago (#2065) and states the rule as "every hand-written member of a promoted TU carries // @symbol <linker-name> on its own line, immediately above its definition." The manifest declares 19 functions. Two are the destructor pair, which the lifecycle fallback covers because the destructor is inline in the header. That leaves 17 that need a marker, and six have one:

address symbol marker
0x02124908 _ZN10dScMgBSC_cD1Ev lifecycle fallback — fine
0x0212497c _ZN10dScMgBSC_cD0Ev lifecycle fallback — fine
0x02124a04 func_ov006_02124a04 missing
0x02124a08 func_ov006_02124a08 missing
0x02124ae4 func_ov006_02124ae4 missing
0x02124b58 func_ov006_02124b58 missing
0x02124bb4 func_ov006_02124bb4 missing
0x02124cb4 func_ov006_02124cb4 missing
0x02124dc0 func_ov006_02124dc0 missing
0x02124e1c func_ov006_02124e1c missing
0x02124ec4 func_ov006_02124ec4 missing
0x02124fd8 func_ov006_02124fd8 missing
0x021250e4 func_ov006_021250e4 missing
0x02125248 func_ov006_02125248 ok
0x0212527c func_ov006_0212527c ok
0x02125364 func_ov006_02125364 ok
0x021253bc _ZN10dScMgBSC_c6RenderEv ok
0x021254c0 _ZN10dScMgBSC_c8BehaviorEv ok
0x0212551c _ZN10dScMgBSC_c13InitResourcesEv ok

The consequence is worse than eleven members losing their boundary. _marked_member_fragment slices from the end of one marker to the start of the next marker. Your markers sit at lines 170, 209, 232, 278, 301 and 341, and the file is 695 lines. There is no marker after 341, so the fragment for func_ov006_02125248 runs 341 → EOF: 354 lines covering eleven other functions. That one member is currently scored against three quarters of the TU.

And the TU is not clean text — all four poisonable criteria are present in it:

criterion present at
no_unk_field unk_51b8 at lines 223, 312, 316, 319, 325
no_codegen_trick *(volatile unsigned char *)&data_020a0e40 at line 463
no_raw_offset *(int *)(c + 0xa8) line 200, *(int*)(base + i * 8 + 0x51a8) line 374, and others
no_mangled_refs _ZN5Sound12PlayBank2_2DEj line 476, _Z14ApproachLinearRiii lines 486-487, _ZN3GXS11LoadOBJPlttEPKvjj line 192

So this is not a hypothetical loss. config/converted-baseline.json banks three identities out of nineteen, and the fragment geometry is part of why.

The fix is eleven lines. Every function in the file already carries a /* ROM ordinal N -- <symbol>, <addr>, size 0x… */ comment with the exact symbol in it — the marker goes on its own line next to that comment, spelled as the manifest's functions[].symbol spells it. You already did this for six; the same treatment for ordinals 2 through 12.

I am not asking you to clean up the unk_/raw-offset/mangled text. That is a separate readability pass and the byte match is what matters first. I am asking that when someone does it, the credit lands on the member they fixed instead of being blocked by a neighbour.


What I verified independently, and what it showed

The linked range is exactly contiguous and abuts both neighbours

Nineteen delink entries collapse into one, and the endpoints are unchanged:

MgTrampolineTerror.cpp        .text 0x0212471c..0x02124908
src/actors/dScMgBSC_c.cpp     .text 0x02124908..0x021255f8   <- new

Chaining the nineteen removed ranges by hand: 908 -> 97c -> a04 -> a08 -> ae4 -> b58 -> bb4 -> cb4 -> dc0 -> e1c -> ec4 -> fd8 -> 0e4 -> 248 -> 27c -> 364 -> 3bc -> 4c0 -> 51c -> 5f8. No gap, no overlap, first start and last end preserved.

Every licensing address in compiler_only_output reads back correctly from the ROM

This is the check that SCRATCH-LINK-VERIFIED does not perform — the link round proves the bytes, not that the canonical_module/canonical_address rows point at the records they claim. I resolved all of them against extracted/arm9_dec.bin and the overlay images, preferring each row's own module:

_ZTI7fBase_c              arm9  0x02086d70 -> name '7fBase_c'                OK
_ZTS7fBase_c              arm9  0x02086e60 -> '7fBase_c'                     OK
_ZTS7dBase_c              arm9  0x02086e54 -> '7dBase_c'                     OK
_ZTS8dScene_c             arm9  0x020914b0 -> '8dScene_c'                    OK
_ZTI7dBase_c              arm9  0x02086e78 -> name '7dBase_c'                OK
_ZTI11dScMgBase_c         ov004 0x020bbf6c -> name '11dScMgBase_c'           OK
_ZTS11dScMgBase_c         ov004 0x020bbf84 -> '11dScMgBase_c'                OK
_ZTI8dScene_c             arm9  0x020914d4 -> name '8dScene_c'               OK
_ZTI10dScMgBSC_c          ov006 0x0213fd8c -> name '10dScMgBSC_c'            OK
_ZTS10dScMgBSC_c          ov006 0x0213fd98 -> '10dScMgBSC_c'                 OK
_ZTI19dScMgSingle3DBase_c ov006 0x0213bc64 -> name '19dScMgSingle3DBase_c'   OK
_ZTS19dScMgSingle3DBase_c ov006 0x0213bd00 -> '19dScMgSingle3DBase_c'        OK

Thirteen for thirteen, counting the vtable below.

The class's own RTTI chain, walked from the vtable

_ZTV10dScMgBSC_c @ 0x0213fec8   (ov006, base 0x020bfec0)
  [V-8] offset-to-top = 0x00000000
  [V-4] typeinfo      = 0x0213fd8c   manifest says 0x0213fd8c   MATCH
  TI[1] name ptr      = 0x0213fd98 -> '10dScMgBSC_c'
  TI[2] base's _ZTI   = 0x0213bc64   = _ZTI19dScMgSingle3DBase_c  MATCH

10 is exactly len("dScMgBSC_c"), so the read verifies itself, and TI[2] independently confirms the base clause dScMgBSC_c : dScMgSingle3DBase_c from the cartridge rather than from our headers.

This class's name agrees with the ROM, which is why the promotion can land at all. #2066 is the counter-example: PoleLift is daObjKm2_Ami_Bou_c in the cartridge, so consolidating its RTTI emitted _ZTS8PoleLift/_ZTI8PoleLift with no ROM symbol to license them and tubuild refused with HOMELESS. Measured tree-wide, 259 of 541 _ZTV-bearing classes are in that position — see #2069. dScMgBSC_c and dScMgSingle3DBase_c are both in the agreeing 276.

The converted ratchet is a clean re-key, not a backslide

Three entries move and the set size is unchanged at 2573:

- src/_ZN10dScMgBSC_c6RenderEv.cpp   -> src/actors/dScMgBSC_c.cpp#_ZN10dScMgBSC_c6RenderEv
- src/_ZN10dScMgBSC_cD0Ev.cpp        -> src/actors/dScMgBSC_c.cpp#_ZN10dScMgBSC_cD0Ev
- src/_ZN10dScMgBSC_cD1Ev.cpp        -> src/actors/dScMgBSC_c.cpp#_ZN10dScMgBSC_cD1Ev

The other five .cpp files this deletes — InitResources, Behavior, func_ov006_02124e1c, _0212527c, _02125364 — I checked against the base baseline and none of them were members, so nothing is silently dropped. This is the shape a promotion should have.

The inline destructor, and the brace

The header's reasoning is correct and it is the good kind of evidence — D1 at 0x02124908 sits before D0 at 0x0212497c, which an out-of-line destructor cannot produce, because mwcc emits the synthesized D0 ahead of the written D1. Inline in the class body, the destructor cannot be the key function, so InitResources (the next declared non-inline virtual) becomes it and emits the vtable, whose slots 16/17 odr-use D1 then D0 in the cartridge's order.

Worth saying out loud: dScMgBSC_c is the first case in the tree that actually sits on the check_header_offsets hazard shape — derived class, structor declared before any field, multi-statement body. You put the brace on the signature line, which is the one position that keeps the field walk alive. That is exactly what #2068 documented two commits ago, and this PR is the first real instance of it. Please leave it that way.

The notes are the best part of this PR

notes/dScMgBase_c-slots-18-35.md confirms the slot map from a second, independent direction — the base's table was walked from _ZTV11dScMgBase_c, the overrides from _ZTV10dScMgBSC_c, and the recovered-name comments predate both walks and land on the same two addresses. Slot 18 staying unnamed on both sides is consistent, not a hole. And measuring what the follow-up costs — _ZTV10dScMgBSC_c is 0x50 bytes, truncating exactly one slot short of 18, so widening the base widens every descendant's table too — is the kind of thing that stops a follow-up PR from being opened blind.

The notes/minigame-provenance.md line was correctly re-pointed at the new path. That would otherwise have gone red on check_dead_references.


Two smaller things, neither blocking

1. The manifest's five CONFLICT notes describe something that is no longer in the file. They each end "kept the first, the other is commented out for review" — for func_ov006_020c0c80, func_ov006_020c0d68, _ZN5Sound12PlayBank2_2DEj, func_ov006_020c1718 and func_ov006_020c0aa8. There is no commented-out extern anywhere in src/actors/dScMgBSC_c.cpp, so a reader who tries to do the review the note asks for finds nothing to compare against. If you resolved them, say so — replace the five with one line noting that the kept declaration is confirmed by the 19/19 byte match, and which one lost. If you did not, the alternatives should be restored.

2. boundary_evidence says corroborated=False. The sinit line is module-wide (31/31 for all of ov006), not narrowed to this TU, and the manifest is honest about that. The contiguous linker run plus the vtable-slot derivation of the three overrides carry the boundary here, so this is not a gap I am asking you to close — just noting that the strong evidence is the other two rows, not this one.


Verdict

Everything I could check from the cartridge checks out: the range, the licensing addresses, the RTTI chain, the base clause, the ratchet set. 19/19 byte-matched under SCRATCH-LINK-VERIFIED with a full ROM build.

Add the eleven markers and I will merge it. This is stacked on #2064, which is also awaiting fixes, so both can be pushed in one round — and #2064 needs one marker of its own (func_ov006_0210a534, the only unmarked member there, and it is the last function in that file so it is currently absorbed into func_ov006_0210a600's fragment). I have commented there separately.

andrewboudreau added a commit that referenced this pull request Aug 31, 2026
tiers.score_member slices a member at `// @symbol` markers and, finding
none after a marker, runs to end of file. Only 5 of the 21 in-file members
carried one, so _ZN13dScMgMCarlo_c6RenderEv was scored over everything
below it -- the flat-C bodies with their raw casts and mangled callees --
and the CONVERTED ratchet read that as a backslide of a banked entry:

  src/actors/dScMgMCarlo_c.cpp#_ZN13dScMgMCarlo_c6RenderEv
      No raw offset arithmetic; Calls things by real names, not mangled _Z

Neither is true of Render's own body. The 16 missing markers are added, so
each member is scored against itself. Comments only:

  tiers_ratchet --check   PASS   baseline 2568   current 2568
  rombuild -j16           106/106 exact, 100.000000%; 11,088/11,088
                          reproducing, 0 mismatching; the ROM is still
                          sha256 d1506e90..., identical to the stock build

dScMgBSC_c.cpp has the same gap -- 6 markers for 17 members -- but none of
its banked members currently mis-score, so it is a latent hazard rather
than a regression, and it belongs to #2067, not here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhhAeJwXBnfPp7B5DNjCwh
Base automatically changed from cpp/minigame-tu to main August 31, 2026 08:38
@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Latent @symbol gap in src/actors/dScMgBSC_c.cpp — found while landing the next TU, not red today.

tiers.score_member slices a member of a promoted TU at // @symbol markers, and when there is no next marker it runs to end of file (tools/tiers.py:317-325). The BSC TU has 6 markers for 17 in-file members, so the first-marked member is scored over every unmarked flat-C body below it — raw casts, mangled callees and all.

That is not currently a failure here: none of BSC's banked converted-baseline entries lands on a member whose slice over-reaches, so converted-ratchet is green on this PR's merge tree (re-confirmed just now with premerge_check 2064 2067 — all 8 gates pass on both).

It did fire one PR up the stack. #2071 (MCarlo, 21 in-file members, 5 marked) went green→red on converted-ratchet with:

src/actors/dScMgMCarlo_c.cpp#_ZN13dScMgMCarlo_c6RenderEv
    No raw offset arithmetic (*(u32*)(c + 0x74)); Calls things by real names, not mangled _Z

Neither is true of Render's own body — the slice had swallowed the flat-C members after it. Fixed there by marking all 21; comments only, ROM sha256 unchanged.

So the same 11 missing markers here are a trap waiting on the next banked entry, not a defect in what this PR proves. Adding them is comment-only and byte-neutral. I have not touched this branch, because doing so would force a restack of #2071 for a hazard that is not yet armed — flagging it so the choice is explicit rather than silently deferred.

andrewboudreau and others added 2 commits August 31, 2026 03:40
…IFIED)

Nineteen functions over 0x02124908..0x021255f8 compile from one object and
byte-match the cartridge: 19/19 MATCH, objisolate clean, reloc-destinations
clean. The unit emits its own _ZTV10dScMgBSC_c and the full RTTI chain.

Three things needed hand work beyond what tubuild reconstructs:

* `#pragma opt_strength_reduction off` is carried once, file-global, at the
  top. Two members (func_ov006_02124cb4, func_ov006_021250e4) had it in their
  legacy one-function sources and tubuild left it out with a [NOT carried --
  review] note, because a file-global pragma in a merged TU recompiles every
  other member. Measured both ways. Bracketing it around just those two does
  nothing -- unlike `long_calls`, this pragma is not positional in mwccarm
  2004/b56, it is file-global last-wins, so the trailing `on` won and both
  members still grew. Set once at the top, all nineteen match: the two that
  need it reproduce, and the seventeen that matched without it are unchanged.

* Two legacy files each spelled their own incompatible `struct Obj` and
  tubuild carried both verbatim. The layout typedef keeps the name; the other
  -- which exists only to reach vtable slot 18, undeclared in dScMgBase_c.h,
  so the self-dispatch cannot yet be a real virtual call -- is now
  `SelfVtblShim`, named for what it is instead of colliding.

* tubuild carried both the D1 and D0 legacy bodies as two identical source
  destructors. One source destructor now emits both.

The 14 compiler-only rows are generated from tubuild.all_symbol_homes(), not
hand-written. The D2 takes plain `deadstrip` on proven grounds, not on the
boilerplate reason: it has no configured ROM symbol, its bytes are identical
to the licensed D1 (0x74, compared section-for-section in the object), and
objisolate's plan confirms no surviving relocation reaches it. The other 13
are RTTI/vtable records taking `deadstrip-data` with canonical_module and
canonical_address, so romdata_check compares their bytes against the
cartridge before the duplicates are discarded.
src_tu/actors/dScMgBSC_c.cpp -> src/actors/dScMgBSC_c.cpp, replacing 19
per-function legacy sources (14 flat-C, 5 already C++). The whole .text
range 0x02124908..0x021255f8 now reproduces from one object.

The destructor is defined inline in the class body rather than out of
line here. An out-of-line ~dScMgBSC_c() makes mwcc emit the synthesized
D0 ahead of the written D1, and the cartridge has D1 first; linkcheck's
pre-link licensing audit refuses a TU whose licensed .text is not in ROM
address order, and it refused this one. Inlining hands the key-function
role to InitResources -- the next virtual declared, and non-inline, and
defined in this TU -- which emits _ZTV10dScMgBSC_c here. Slots 16 and 17
name D1 then D0, odr-using both, so the compiler emits the pair for us,
in cartridge order. It also deletes the homeless D2 outright, so that
policy row is gone (14 compiler_only_output rows -> 13).

Verification:
  tubuild verify ov006/dScMgBSC_c   19/19 MATCH, exact deadstrip,
                                    ROM-ascending section order
  tubuild linkcheck --module ov006  SCRATCH-LINK-VERIFIED; 106/106
                                    exact; ROM sha256 d1506e90... is
                                    IDENTICAL to the stock build
  rombuild -j16                     11,088 source-built functions,
                                    reproducing 11,088, mismatching 0;
                                    storage aliases exact; ROM data from
                                    source 462 verified (was 461)

The +1 verified record and the +1 partial are the RTTI and the emitted
_ZTV10dScMgBSC_c -- 18 of the cartridge's 36 slots, byte-compared rather
than trusted. Naming slots 18-35 on dScMgBase_c widens the table past
slot 17 and is deliberately left to its own change; notes/dScMgBase_c-
slots-18-35.md records the two independent confirmations this TU gives
for slots 18, 19 and 21.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhhAeJwXBnfPp7B5DNjCwh
@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Now DIRTY — and the conflict set says restack, do not resolve

#2064 merged as c97a323b8da7 (squash). GitHub auto-retargeted this PR to main, and because the parent landed squashed, main now contains #2064's content under a commit this branch has never seen. git merge-tree origin/main 7c0f9a92c794 reports:

CONFLICT (content) attribution.json
CONFLICT (add/add) config/tu_manifest.d/ov006/dScMgSingle3DBase_c.json
CONFLICT (content) include/dScMgSingle3DBase_c.h
CONFLICT (add/add) notes/dScMgBase_c-slots-18-35.md
CONFLICT (add/add) src/actors/dScMgSingle3DBase_c.cpp

Four of those five are not yours. They are #2064's files, colliding with themselves. Resolving them by hand is the wrong move twice over: it is work you already did, and include/dScMgSingle3DBase_c.h and the manifest on main are the post-review versions — they carry the five fixes and the two disclosures from ea9f05566. Any hand-resolve that takes this branch's side silently reverts them.

The restack

This branch has exactly two commits of its own:

d80b1cee7  TU: reconstruct ov006/dScMgBSC_c as a 19-function shadow TU (TEXT-VERIFIED)
7c0f9a92c  Promote ov006/dScMgBSC_c to a single 19-function C++ TU

Cut a fresh branch from current origin/main and replay just those two. Everything they touch is dScMgBSC_c, not dScMgSingle3DBase_c — I checked the 27-file diff and it contains no edit to include/dScMgSingle3DBase_c.h, to that class's manifest, or to src/actors/dScMgSingle3DBase_c.cpp. So all four of the add/add collisions disappear along with the parent's commits, and you should land on two remaining conflict points, both expected:

  • attribution.json — reorders on every pick. Three-way merge it; do not union the keys. Attribution is a stated non-goal here, so do not spend more than the mechanical resolve on it.
  • notes/dScMgBase_c-slots-18-35.mdmain has a version of this file now, so your +29 lands as add/add. Merge the two by hand; the slot-map content is the part worth keeping.

Roll the eleven @symbol markers into the same push — details in my review above. One round covers the restack and the markers together.

#2071 is stacked on this branch and will need the same treatment afterwards. I will review it once it sits on a clean base.

One correction to something in my review, in your favour: I said the run-on fragment poisons four criteria. It poisons them because it swallows eleven live function bodies — I re-checked all eight lines I cited (192, 200, 223, 312, 374, 463, 476, 486) and every one is code, not comment. tiers.py blanks comments before four of the five checks, so prose inside a fragment is harmless. The ask stands exactly as written; I just want the reason on the record precisely.

tiers.score_member slices a member at `// @symbol` markers and, finding no
next marker, runs to end of file (tools/tiers.py:317-325). This TU carried
6 markers for 17 in-file members, so the first-marked member's fragment
swallowed eleven live function bodies below it -- their raw casts and
mangled callees included.

Nothing banked mis-scores on it today, so the gate was green either way;
it is a trap waiting on the next banked entry rather than a present
defect. One PR up the stack the identical gap did fire, scoring
_ZN13dScMgMCarlo_c6RenderEv over the flat-C members after it and reading
as a CONVERTED backslide that Render's own body does not deserve.

Comments only:

  tiers_ratchet --check   PASS   baseline 2568   current 2568
  rombuild -j16           106/106 exact, 100.000000%; 11,088/11,088
                          reproducing, 0 mismatching; the ROM is still
                          sha256 d1506e90..., identical to the stock build

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhhAeJwXBnfPp7B5DNjCwh
andrewboudreau added a commit that referenced this pull request Aug 31, 2026
tiers.score_member slices a member at `// @symbol` markers and, finding
none after a marker, runs to end of file. Only 5 of the 21 in-file members
carried one, so _ZN13dScMgMCarlo_c6RenderEv was scored over everything
below it -- the flat-C bodies with their raw casts and mangled callees --
and the CONVERTED ratchet read that as a backslide of a banked entry:

  src/actors/dScMgMCarlo_c.cpp#_ZN13dScMgMCarlo_c6RenderEv
      No raw offset arithmetic; Calls things by real names, not mangled _Z

Neither is true of Render's own body. The 16 missing markers are added, so
each member is scored against itself. Comments only:

  tiers_ratchet --check   PASS   baseline 2568   current 2568
  rombuild -j16           106/106 exact, 100.000000%; 11,088/11,088
                          reproducing, 0 mismatching; the ROM is still
                          sha256 d1506e90..., identical to the stock build

dScMgBSC_c.cpp had the same gap -- 6 markers for 17 members -- and none of
its banked members mis-scored, so it was a latent hazard rather
than a regression; it is fixed one commit down, in #2067, where it lives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhhAeJwXBnfPp7B5DNjCwh
@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Restack and markers verified — approving, merging once the ROM validator reports

Re-reviewed at 3194e6d6e058. Both asks are done, and the restack is the clean kind.

The markers

17 of 19 declared functions now carry // @symbol. The two that do not are _ZN10dScMgBSC_cD1Ev and _ZN10dScMgBSC_cD0Ev, which is correct — the destructor is inline in the header, so the pair is compiler-emitted and the lifecycle fallback covers it. No extra markers, no misspellings against functions[].symbol.

The last marker now sits at line 681 of 706, so the final fragment is 25 lines instead of the 354 it was. The eleven bodies that were being scored against func_ov006_02125248 are their own again.

The markers commit is src/actors/dScMgBSC_c.cpp | 11 +++++++++++ — eleven added lines, nothing else in it. That is exactly the ask and nothing else, which is what makes it cheap to re-verify.

The restack

3194e6d6e  Mark every member of the BSC TU with its @symbol boundary
e3c4a7493  Promote ov006/dScMgBSC_c to a single 19-function C++ TU
85dd44510  TU: reconstruct ov006/dScMgBSC_c as a 19-function shadow TU (TEXT-VERIFIED)

Three commits on current main, 27 files, same set as before.

I checked the thing I was actually worried about — that a hand-resolve would take this branch's side on #2064's files and silently revert its post-review fixes. It did not:

git diff origin/main..3194e6d6e058 -- include/dScMgSingle3DBase_c.h \
    config/tu_manifest.d/ov006/dScMgSingle3DBase_c.json \
    src/actors/dScMgSingle3DBase_c.cpp src/_ZN10dBgActor_cD0Ev.cpp
  (no output)

All four are byte-identical to main. notes/dScMgBase_c-slots-18-35.md is +29 / -0 against main, so your slot map was appended to the landed version rather than replacing it — the right resolution for that add/add.

And the promotion content itself survived the re-cut unchanged:

git diff 7c0f9a92c794 e3c4a7493 -- src/actors/dScMgBSC_c.cpp \
    config/tu_manifest.d/ov006/dScMgBSC_c.json include/dScMgBSC_c.h \
    config/arm9/overlays/ov006/delinks.txt config/converted-baseline.json
  (no output)

So everything I verified from the cartridge in my first review — the contiguous 0x02124908..0x021255f8 run, the thirteen licensing addresses, the RTTI chain, the base clause — carries over without re-checking.

Gates on the real merge tree

tools/premerge_check.py against origin/main @ 22da79ddb8de, merge tree a1918a3c40df:

converted-ratchet   pass  ->  pass
dead-references     pass  ->  pass
duplicate-sources   pass  ->  pass
header-offsets      pass  ->  pass
langmode-ratchet    pass  ->  pass
layout-check        pass  ->  pass
src-tu-refs         pass  ->  pass
source-coverage     pass  ->  pass
  info: entries: 10925 -> 10907 (-18)   (consolidation, not a loss -- bytes are flat)

8/8, and the −18 is arithmetic: nineteen delink entries become one.

The converted ratchet is a clean 3-way re-key with the set size unchanged:

- src/_ZN10dScMgBSC_c6RenderEv.cpp   -> src/actors/dScMgBSC_c.cpp#_ZN10dScMgBSC_c6RenderEv
- src/_ZN10dScMgBSC_cD0Ev.cpp        -> src/actors/dScMgBSC_c.cpp#_ZN10dScMgBSC_cD0Ev
- src/_ZN10dScMgBSC_cD1Ev.cpp        -> src/actors/dScMgBSC_c.cpp#_ZN10dScMgBSC_cD1Ev

Approved. I will merge as soon as PR validation reports — it is the only check still pending, and it is the one that builds the ROM, so I am not merging ahead of it.

One carry-forward, still not blocking

The manifest's five CONFLICT notes still end "the other is commented out for review", and grep -c "TUBUILD CONFLICT" src/actors/dScMgBSC_c.cpp returns 0. Worth noting because your own dScMgMCarlo_c TU on #2071 does carry those comments at lines 202-212 — so the generator emits them and something dropped them here. A reader following the note finds nothing to compare. Fix it forward in whichever PR is convenient: either restore the alternates, or replace the five notes with one line saying the kept declaration is confirmed by the 19/19 byte match and naming which spelling lost.

#2071 is stacked on this branch and its merge base is still the pre-restack tip, so it will need the same re-cut. I have already verified its content independently — comment coming on that PR.

@andrewboudreau
andrewboudreau merged commit 244fd4d into main Aug 31, 2026
8 checks passed
@andrewboudreau
andrewboudreau deleted the cpp/minigame-tu-2 branch August 31, 2026 09:03
andrewboudreau added a commit that referenced this pull request Aug 31, 2026
tiers.score_member slices a member at `// @symbol` markers and, finding
none after a marker, runs to end of file. Only 5 of the 21 in-file members
carried one, so _ZN13dScMgMCarlo_c6RenderEv was scored over everything
below it -- the flat-C bodies with their raw casts and mangled callees --
and the CONVERTED ratchet read that as a backslide of a banked entry:

  src/actors/dScMgMCarlo_c.cpp#_ZN13dScMgMCarlo_c6RenderEv
      No raw offset arithmetic; Calls things by real names, not mangled _Z

Neither is true of Render's own body. The 16 missing markers are added, so
each member is scored against itself. Comments only:

  tiers_ratchet --check   PASS   baseline 2568   current 2568
  rombuild -j16           106/106 exact, 100.000000%; 11,088/11,088
                          reproducing, 0 mismatching; the ROM is still
                          sha256 d1506e90..., identical to the stock build

dScMgBSC_c.cpp had the same gap -- 6 markers for 17 members -- and none of
its banked members mis-scored, so it was a latent hazard rather
than a regression; it is fixed one commit down, in #2067, where it lives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhhAeJwXBnfPp7B5DNjCwh
andrewboudreau added a commit that referenced this pull request Aug 31, 2026
* Promote ov006/dScMgMCarlo_c to a single 23-function C++ TU

src_tu/actors/dScMgMCarlo_c.cpp -> src/actors/dScMgMCarlo_c.cpp, replacing
23 per-function legacy sources (18 flat-C, 5 already C++). The whole .text
range 0x020f7634..0x020f8e44 now reproduces from one object.

CONSOLIDATION IS WHERE THE WORK WAS. Each of the 23 legacy files compiled
in isolation with its own private shadow structs and extern declarations,
so they were free to contradict each other and never did so out loud. Put
in one TU they produced ~25 compile errors, every one a real disagreement
about the same object. Each was settled against evidence:

  - `Node` and the list-`Obj` are the SAME struct. Five partial views, all
    agreeing on every offset they share; unified into one layout with the
    fields commented.
  - A THIRD, unrelated `Obj` was not a list node at all -- it was a
    shape-only shadow of this scene class's OWN vtable, used for the
    slot-18 self-dispatch in InitResources. Revived as `SceneVtable` so
    the two meanings stop colliding. It becomes a real base-class virtual
    call once dScMgBase_c declares slots 18-35.
  - data_ov006_02142500 is the list HEAD and _02142504 the TAIL, proven by
    func_ov006_020f7740 walking head->next and tail->prev. Render starts
    from the tail, so both its loops step `prev`, not `next` -- corrobo-
    rated independently by func_ov006_020f8a3c's raw `*(char**)(r3 + 4)`.
  - _Z14ApproachLinearRiii and _Z15ApproachLinear2Rsss take REFERENCES;
    the mangled names say so, and a reference and a pointer pass the same
    address, so the accurate spelling costs no code.
  - func_ov006_020f8540 was declared (void) but called with an argument.
    ARM passes it in r0 and the body ignores it; spelling the parameter
    keeps the call site honest and changes no byte.
  - include/dScMgBase_c.h already declared data_ov004_020beb68 as void*,
    which settled that conflict against the TU's char*.

The destructor is defined inline in the class body, for the same reason as
dScMgBSC_c: out of line, mwcc emits the synthesized D0 ahead of the written
D1, and the cartridge has D1 first (0x020f7634) then D0 (0x020f76a8).
Inlining hands the key-function role to InitResources -- the next virtual
declared, non-inline, and defined here -- which is what emits
_ZTV13dScMgMCarlo_c. Slots 16 and 17 name D1 then D0, odr-using both, so
the compiler emits the pair in cartridge order, and the homeless D2 is gone.

decl_common.h loses the scalar declarations of the four list globals
(_021424fc/_02142500/_02142504/_02142508). They were arbitrary winners
among 19 contradictory isolated spellings; every consumer is in this TU,
which now declares all four with the recovered pointer type.

Verification:
  tubuild verify ov006/dScMgMCarlo_c   23/23 MATCH, objisolate clean,
                                       reloc-destinations clean,
                                       ROM-ascending section order
  tubuild linkcheck (pre-promotion)    SCRATCH-LINK-VERIFIED; 106/106
                                       exact; full ROM built and IDENTICAL
                                       to the stock build. dsd check
                                       symbols --fail still fails on the
                                       same 9 errors the baseline control
                                       has -- 0 new, so not attributable
                                       to this TU, but NOT green and not
                                       reported as such.
  rombuild -j16 (post-promotion)       11,088 source-built functions,
                                       reproducing 11,088, mismatching 0;
                                       106/106 exact, 100.000000%; the ROM
                                       it builds is sha256 d1506e90...,
                                       byte-identical to the stock build.
  layout_check / eligible / langmode / port_refcheck / dead-references: clean

All 13 compiler-emitted RTTI and vtable symbols are licensed in
compiler_only_output, every canonical address confirmed against
config/arm9 symbols.txt, and romdata_check byte-compares each against the
cartridge: 6 VERIFIED, 7 PARTIAL (exact prefixes), 0 DIFFER.
_ZTV13dScMgMCarlo_c reproduces 72 of the cartridge's 144 bytes -- 18 of 36
slots -- because dScMgBase_c still leaves slots 18-35 undeclared. That is a
verified prefix, not a disagreement, and widening it is the dScMgBase_c
follow-up, not this change.

The cartridge's own _ZTS strings corroborate the names: reading
extracted/dsd/arm9_overlays/ov006.bin at overlay base 34340544 gives
13dScMgMCarlo_c at 0x0213d594 and 19dScMgSingle3DBase_c at 0x0213bd00 --
so this TU emits no homeless typeinfo record.

Attribution: a 23-delete + 1-add collapse ends 22 git lineages, so
attribution.json takes 23 explicit override keys on the surviving path,
each carrying the first matcher the gate itself computes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhhAeJwXBnfPp7B5DNjCwh

* Mark every member of the MCarlo TU with its @symbol boundary

tiers.score_member slices a member at `// @symbol` markers and, finding
none after a marker, runs to end of file. Only 5 of the 21 in-file members
carried one, so _ZN13dScMgMCarlo_c6RenderEv was scored over everything
below it -- the flat-C bodies with their raw casts and mangled callees --
and the CONVERTED ratchet read that as a backslide of a banked entry:

  src/actors/dScMgMCarlo_c.cpp#_ZN13dScMgMCarlo_c6RenderEv
      No raw offset arithmetic; Calls things by real names, not mangled _Z

Neither is true of Render's own body. The 16 missing markers are added, so
each member is scored against itself. Comments only:

  tiers_ratchet --check   PASS   baseline 2568   current 2568
  rombuild -j16           106/106 exact, 100.000000%; 11,088/11,088
                          reproducing, 0 mismatching; the ROM is still
                          sha256 d1506e90..., identical to the stock build

dScMgBSC_c.cpp had the same gap -- 6 markers for 17 members -- and none of
its banked members mis-scored, so it was a latent hazard rather
than a regression; it is fixed one commit down, in #2067, where it lives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhhAeJwXBnfPp7B5DNjCwh

* Spell func_ov006_020f7730 the way the TU recovers it

src/func_ov006_020f8e44.cpp declared the array's element destructor as
`void(void)` while src/actors/dScMgMCarlo_c.cpp recovers it as `void(int *)`.
Both compile -- the factory casts to void* at the call -- but that file is
the corroboration for mArray at 0x51a8 being 0x50 x 0x30 with that element
dtor, so it is the one place the disagreement is expensive to leave.

rombuild: 11,088/11,088 reproducing, 0 mismatching, 106/106 exact,
100.000000%; ROM sha256 unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhhAeJwXBnfPp7B5DNjCwh

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant