Skip to content

TU: promote ov006/dScMgSingle3DBase_c — nine functions, vtable-proven boundary - #2064

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

TU: promote ov006/dScMgSingle3DBase_c — nine functions, vtable-proven boundary#2064
andrewboudreau merged 4 commits into
mainfrom
cpp/minigame-tu

Conversation

@andrewboudreau

Copy link
Copy Markdown
Collaborator

Promotes ov006/dScMgSingle3DBase_c — the base class of thirteen minigame scenes — from nine hand-placed one-function files into one C++ translation unit that emits its own destructor pair, vtable and RTTI records the way the cartridge's own build did.

What lands

Nine functions, .text 0x0210a4b0..0x0210a8c0 (1040 bytes), one complete delink entry replacing nine:

symbol size
D1 _ZN19dScMgSingle3DBase_cD1Ev 0x38
D0 _ZN19dScMgSingle3DBase_cD0Ev 0x4c
func_ov006_0210a534 0xcc
slot 26 func_ov006_0210a600 0x08
slot 5 _ZN19dScMgSingle3DBase_c21AfterCleanupResourcesEj 0x5c
slot 10 _ZN19dScMgSingle3DBase_c12BeforeRenderEv 0x34
slot 7 _ZN19dScMgSingle3DBase_c14BeforeBehaviorEv 0x4c
slot 2 _ZN19dScMgSingle3DBase_c18AfterInitResourcesEj 0x24
slot 33 func_ov006_0210a708 0x1b8

The destructor pair is no longer hand-placed under // @symbol markers. The class's key function pulls D1/D0 out of line through vtable slots 16/17, and mwcc emits both — which is the point of the whole workstream.

The boundary is corrected, and the correction is proved

tools/tu_map.py reported this unit as eight functions, 0x0210a4b0..0x0210a708, and split func_ov006_0210a708 into a single-function unit of its own. That is a tu_map defect, not a real boundary: the function's symbol is still spelled func_<module>_<address>, so it gets no class label, so tu_map cuts there.

The vtable says otherwise. Scanning every _ZTV in ov006:

  • 0x0210a708 is referenced at slot 33 by exactly fourteen vtables — dScMgSingle3DBase_c and all thirteen of its children.
  • 0x0210a600 — slot 26, which tu_map did place inside the unit — is referenced by the same fourteen.

Identical reference sets. One function tu_map kept and one it dropped are members of the same class. The unit is nine functions, and the ninth byte-matches inside the TU on the first compile.

Two accuracy defects fixed on the way

func_ov006_0210a600 was labelled one class too deep. Its legacy source carried recovered name: dScMgFlower_c_OnHitByCannonBlastedChar. dScMgFlower_c is one of the fourteen classes that reference it — a body every sibling shares belongs to the common ancestor. Same off-by-one-class shape already documented for AfterInitResources.

include/dScMgSingle3DBase_c.h called slots 26 and 33 "new at this class". They are re-overrides: _ZTV11dScMgBase_c already carries bodies at both (ov004:0x020b04e0 and ov004:0x020b265c, read out of the ROM). They stay undeclared only because dScMgBase_c leaves slots 18-35 undeclared, and a derived class cannot override a slot the base has not spelled.

Compiler-generated output: eleven licensed symbols

The TU emits three of its own records — _ZTI/_ZTS/_ZTV19dScMgSingle3DBase_c — plus eight ancestor RTTI passengers. All eleven are deadstrip-data, each naming the cartridge home it defers to, so romdata_check becomes binding for exactly those symbols and compares them relocations-applied before isolation discards them. Every canonical address is derived from tubuild.all_symbol_homes().

_ZTV19dScMgSingle3DBase_c scores PARTIAL, and that is expected rather than a disagreement: dScMgBase_c leaves slots 18-35 undeclared, so mwcc emits an 18-slot table where the cartridge has 36 — a verified byte-exact prefix. Owning it outright is the follow-up, mapped in notes/dScMgBase_c-slots-18-35.md and notes/dScMgSingle3DBase_c-vtable-ownership.md.

Evidence

  • tubuild verify9/9 MATCH, objisolate clean, relocation destinations clean, emission order ROM-ascending.
  • tubuild linkcheckSCRATCH-LINK-VERIFIED; ov006 built 525344 == retail 525344; TU range IDENTICAL; ROM sha256 d1506e90…c478e8, identical to the stock build. dsd check symbols --fail reports 9 errors, all of which the --baseline control reproduces exactly: 0 new, 0 resolved. That check is not green and is not reported as green.
  • rombuild after promotion — module fidelity, source fidelity and the compiler-only policy, against the real tree.

Notes for review

  • The delink change is a contiguous 9→1 merge with the same outer endpoints; no entry lost its complete marker.
  • config/converted-baseline.json moves six rows to promoted-path#symbol identities, so the readability ratchet keeps function granularity — six out, six in, count invariant. Edited by tu_promote.py, not by tiers_ratchet.py --update, which would sweep unrelated baseline staleness into the diff.
  • Two legacy sources disagreed on the same two signatures — one typed the loaded file buffer int, the other void *, and both passed the same value to the same DecompressLZ16. One TU cannot hold both; the pointer form is the true one and removes four casts. Byte-neutral, and verify re-confirms all nine.
  • notes/actor-vtables.md is repointed at the promoted path so check_dead_references stays green.
  • Rebased onto origin/main @ 9bfcd5d8 (after TU: first compiler-built vtable — promote ov047/daObjKm3_Kurumajiku_c to intact-object production #2057 and docs: an unkeepable data_* entry MUST be dropped — correct anti-pattern 3 #2061 landed). The only conflict was attribution.json, where both sides append to the same trailing line; resolved as the union of both blocks — main's five daObjKm3_Kurumajiku_c rows and this branch's nine — hand-edited at the markers rather than resolved whole-file. config/converted-baseline.json auto-merged.
  • tools/premerge_check.py HEAD against the rebased base: all eight static gates pass → pass, nothing goes green → red. source-coverage notes entries: 10933 -> 10925 (-8), which is the nine-into-one consolidation with bytes flat.

andrewboudreau and others added 3 commits August 30, 2026 22:32
…tion boundary

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhhAeJwXBnfPp7B5DNjCwh
Consolidates nine ov006 functions at 0x0210a4b0..0x0210a8c0 into one
translation unit, src/actors/dScMgSingle3DBase_c.cpp, replacing six
mangled-name single-function sources and three func_<mod>_<addr> ones.

The boundary is proved by the vtable, not by tu_map. tu_map cut the unit
at eight functions because func_ov006_0210a708 still carries a
func_<module>_<address> name and so gets no class label. But exactly 14
vtables in ov006 reference 0x0210a708 at slot 33, and the same 14
reference 0x0210a600 at slot 26 -- a function tu_map did include.
Identical reference sets, so both are members of this class. The ninth
function byte-matched on the first compile at the extended boundary.

Two naming corrections carried in the source and header:

  - Ordinal 3 was labelled dScMgFlower_c_OnHitByCannonBlastedChar. It is
    dScMgSingle3DBase_c's slot 26, an override of
    dScMgBase_c::OnHitByCannonBlastedChar (ov004:0x020b04e0). The old
    label was one class too deep -- the same off-by-one already
    documented for AfterInitResources.
  - The header annotated slots 26 and 33 as new virtuals at this class.
    Both are re-overrides: dScMgBase_c's own vtable already carries
    bodies at each (ov004:0x020b04e0, ov004:0x020b265c).

Signature reconciliation: the two legacy sources disagreed on the type of
the loaded-file buffer (int in one, void* in the other) while passing it
to the same functions. Unified on the pointer form, removing four casts.
Byte-neutral -- the other eight functions still MATCH, which proves it.

Eleven compiler-emitted symbols (vtable, typeinfo, typeinfo names for the
whole base chain) are licensed as deadstrip-data with canonical homes
read from the symbol table.

Verification, on the promoted tree:
  linkcheck --baseline --module ov006: ov006 built 525344 == retail
    525344, ROM sha256 d1506e90... identical to stock, dsd check symbols
    0 NEW / 0 resolved against the 9 pre-existing errors.
  rombuild -j16: mismatching 0, module fidelity 106/106 exact,
    100.000000% of compared bytes, ROM-build analysis PASS.

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

tangos-validator Bot commented Aug 31, 2026

Copy link
Copy Markdown

✅ PR validation — Passed

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

Full merge validation

Check Result
Committed test merge yes
Byte-verified functions 10,893 / 11,347 (96.00%, -8)
Byte-verified code bytes 2,032,448 / 2,211,124 (91.92%, -984)
Claimed, not byte-verified 324 functions, 73,232 bytes (+8)
Perfect source moves 0 R100
Enrolled ranges (delinks complete) 11,059 functions, 2,053,148 bytes (92.86%, +0) -- differs from byte-verified by +166
Contributor credit 0 added, 7 changed, 0 lost
Relocation check 99 checked; 99 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 (7)

Function Source Before After
ov006:0x0210a534 src/func_ov006_0210a534.cpp -> src/actors/dScMgSingle3DBase_c.cpp tangosdev github-actions[bot]
ov006:0x0210a600 src/func_ov006_0210a600.c -> src/actors/dScMgSingle3DBase_c.cpp tangosdev github-actions[bot]
ov006:0x0210a608 src/_ZN19dScMgSingle3DBase_c21AfterCleanupResourcesEj.cpp -> src/actors/dScMgSingle3DBase_c.cpp andrewboudreau github-actions[bot]
ov006:0x0210a664 src/_ZN19dScMgSingle3DBase_c12BeforeRenderEv.cpp -> src/actors/dScMgSingle3DBase_c.cpp andrewboudreau github-actions[bot]
ov006:0x0210a698 src/_ZN19dScMgSingle3DBase_c14BeforeBehaviorEv.cpp -> src/actors/dScMgSingle3DBase_c.cpp andrewboudreau github-actions[bot]
ov006:0x0210a6e4 src/_ZN19dScMgSingle3DBase_c18AfterInitResourcesEj.cpp -> src/actors/dScMgSingle3DBase_c.cpp andrewboudreau github-actions[bot]
ov006:0x0210a708 src/func_ov006_0210a708.c -> src/actors/dScMgSingle3DBase_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 324 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 (7 changed, 0 lost -- src/actors/dScMgSingle3DBase_c.cpp: tangosdev -> github-actions[bot]; src/actors/dScMgSingle3DBase_c.cpp: tangosdev -> github-actions[bot]; src/actors/dScMgSingle3DBase_c.cpp: andrewboudreau -> github-actions[bot]; +4 more); 9 address range(s) left the byte-verified set while enrolled totals held steady: ov006:0x0210a4b0-0x0210a4e8, ov006:0x0210a4e8-0x0210a534, ov006:0x0210a534-0x0210a600, ov006:0x0210a600-0x0210a608, ov006:0x0210a608-0x0210a664, +4 more; 8 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 91 changed file(s) compile to the ROM byte-for-byte with correct relocation targets.

File Symbol Result Slots checked
src/_ZN10dBgActor_cD0Ev.cpp _ZN10dBgActor_cD0Ev ✅ verified 1
src/_ZN10dScMgBSC_c13InitResourcesEv.cpp _ZN10dScMgBSC_c13InitResourcesEv ✅ verified 1
src/_ZN10dScMgBSC_c6RenderEv.cpp _ZN10dScMgBSC_c6RenderEv ✅ verified 1
src/_ZN10dScMgBSC_c8BehaviorEv.cpp _ZN10dScMgBSC_c8BehaviorEv ✅ verified 1
src/_ZN10dScMgBSC_cD0Ev.cpp _ZN10dScMgBSC_cD0Ev ✅ verified 1
src/_ZN10dScMgBSC_cD1Ev.cpp _ZN10dScMgBSC_cD1Ev ✅ verified 1
src/_ZN10dScMgCup_c13InitResourcesEv.cpp _ZN10dScMgCup_c13InitResourcesEv ✅ verified 1
src/_ZN10dScMgCup_c6RenderEv.cpp _ZN10dScMgCup_c6RenderEv ✅ verified 1
src/_ZN10dScMgCup_c8BehaviorEv.cpp _ZN10dScMgCup_c8BehaviorEv ✅ verified 1
src/_ZN10dScMgCup_cD0Ev.cpp _ZN10dScMgCup_cD0Ev ✅ verified 1
src/_ZN10dScMgCup_cD1Ev.cpp _ZN10dScMgCup_cD1Ev ✅ verified 1
src/_ZN11dScMgCard_c13InitResourcesEv.cpp _ZN11dScMgCard_c13InitResourcesEv ✅ verified 1
src/_ZN11dScMgCard_c16CleanupResourcesEv.cpp _ZN11dScMgCard_c16CleanupResourcesEv ✅ verified 1
src/_ZN11dScMgCard_c6RenderEv.cpp _ZN11dScMgCard_c6RenderEv ✅ verified 1
src/_ZN11dScMgCard_c8BehaviorEv.cpp _ZN11dScMgCard_c8BehaviorEv ✅ verified 1
src/_ZN11dScMgCard_cD0Ev.cpp _ZN11dScMgCard_cD0Ev ✅ verified 1
src/_ZN11dScMgCard_cD1Ev.cpp _ZN11dScMgCard_cD1Ev ✅ verified 1
src/_ZN12dScMg3DEsp_c13InitResourcesEv.cpp _ZN12dScMg3DEsp_c13InitResourcesEv ✅ verified 1
src/_ZN12dScMg3DEsp_c16CleanupResourcesEv.cpp _ZN12dScMg3DEsp_c16CleanupResourcesEv ✅ verified 1
src/_ZN12dScMg3DEsp_c6RenderEv.cpp _ZN12dScMg3DEsp_c6RenderEv ✅ verified 1
src/_ZN12dScMg3DEsp_c8BehaviorEv.cpp _ZN12dScMg3DEsp_c8BehaviorEv ✅ verified 1
src/_ZN12dScMg3DEsp_cD0Ev.cpp _ZN12dScMg3DEsp_cD0Ev ✅ verified 1
src/_ZN12dScMg3DEsp_cD1Ev.cpp _ZN12dScMg3DEsp_cD1Ev ✅ verified 1
src/_ZN12dScMgSlot3_c13InitResourcesEv.cpp _ZN12dScMgSlot3_c13InitResourcesEv ✅ verified 1
src/_ZN12dScMgSlot3_c6RenderEv.cpp _ZN12dScMgSlot3_c6RenderEv ✅ verified 1
src/_ZN12dScMgSlot3_c8BehaviorEv.cpp _ZN12dScMgSlot3_c8BehaviorEv ✅ verified 1
src/_ZN12dScMgSlot3_cD0Ev.cpp _ZN12dScMgSlot3_cD0Ev ✅ verified 1
src/_ZN12dScMgSlot3_cD1Ev.cpp _ZN12dScMgSlot3_cD1Ev ✅ verified 1
src/_ZN12dScMgSound_c13InitResourcesEv.cpp _ZN12dScMgSound_c13InitResourcesEv ✅ verified 1
src/_ZN12dScMgSound_c6RenderEv.cpp _ZN12dScMgSound_c6RenderEv ✅ verified 1
src/_ZN12dScMgSound_c8BehaviorEv.cpp _ZN12dScMgSound_c8BehaviorEv ✅ verified 1
src/_ZN12dScMgSound_cD0Ev.cpp _ZN12dScMgSound_cD0Ev ✅ verified 1
src/_ZN12dScMgSound_cD1Ev.cpp _ZN12dScMgSound_cD1Ev ✅ verified 1
src/_ZN13dScMgFlower_c13InitResourcesEv.cpp _ZN13dScMgFlower_c13InitResourcesEv ✅ verified 1
src/_ZN13dScMgFlower_c6RenderEv.cpp _ZN13dScMgFlower_c6RenderEv ✅ verified 1
src/_ZN13dScMgFlower_c8BehaviorEv.cpp _ZN13dScMgFlower_c8BehaviorEv ✅ verified 1
src/_ZN13dScMgFlower_cD0Ev.cpp _ZN13dScMgFlower_cD0Ev ✅ verified 1
src/_ZN13dScMgFlower_cD1Ev.cpp _ZN13dScMgFlower_cD1Ev ✅ verified 1
src/_ZN13dScMgMCarlo_c13InitResourcesEv.cpp _ZN13dScMgMCarlo_c13InitResourcesEv ✅ verified 1
src/_ZN13dScMgMCarlo_c6RenderEv.cpp _ZN13dScMgMCarlo_c6RenderEv ✅ verified 1
src/_ZN13dScMgMCarlo_c8BehaviorEv.cpp _ZN13dScMgMCarlo_c8BehaviorEv ✅ verified 1
src/_ZN13dScMgMCarlo_cD0Ev.cpp _ZN13dScMgMCarlo_cD0Ev ✅ verified 1
src/_ZN13dScMgMCarlo_cD1Ev.cpp _ZN13dScMgMCarlo_cD1Ev ✅ verified 1
src/_ZN13dScMgMemory_c13InitResourcesEv.cpp _ZN13dScMgMemory_c13InitResourcesEv ✅ verified 1
src/_ZN13dScMgMemory_c6RenderEv.cpp _ZN13dScMgMemory_c6RenderEv ✅ verified 1
src/_ZN13dScMgMemory_c8BehaviorEv.cpp _ZN13dScMgMemory_c8BehaviorEv ✅ verified 1
src/_ZN13dScMgMemory_cD0Ev.cpp _ZN13dScMgMemory_cD0Ev ✅ verified 1
src/_ZN13dScMgMemory_cD1Ev.cpp _ZN13dScMgMemory_cD1Ev ✅ verified 1
src/_ZN14dScMgMCarlo2_c13InitResourcesEv.cpp _ZN14dScMgMCarlo2_c13InitResourcesEv ✅ verified 1
src/_ZN14dScMgMCarlo2_c16CleanupResourcesEv.cpp _ZN14dScMgMCarlo2_c16CleanupResourcesEv ✅ verified 1
src/_ZN14dScMgMCarlo2_c6RenderEv.cpp _ZN14dScMgMCarlo2_c6RenderEv ✅ verified 1
src/_ZN14dScMgMCarlo2_c8BehaviorEv.cpp _ZN14dScMgMCarlo2_c8BehaviorEv ✅ verified 1
src/_ZN14dScMgMCarlo2_cD0Ev.cpp _ZN14dScMgMCarlo2_cD0Ev ✅ verified 1
src/_ZN14dScMgMCarlo2_cD1Ev.cpp _ZN14dScMgMCarlo2_cD1Ev ✅ verified 1
src/_ZN14dScMgMemory2_c13InitResourcesEv.cpp _ZN14dScMgMemory2_c13InitResourcesEv ✅ verified 1
src/_ZN14dScMgMemory2_c6RenderEv.cpp _ZN14dScMgMemory2_c6RenderEv ✅ verified 1
src/_ZN14dScMgMemory2_c8BehaviorEv.cpp _ZN14dScMgMemory2_c8BehaviorEv ✅ verified 1
src/_ZN14dScMgMemory2_cD0Ev.cpp _ZN14dScMgMemory2_cD0Ev ✅ verified 1
src/_ZN14dScMgMemory2_cD1Ev.cpp _ZN14dScMgMemory2_cD1Ev ✅ verified 1
src/_ZN15dScMgRoulette_c13InitResourcesEv.cpp _ZN15dScMgRoulette_c13InitResourcesEv ✅ verified 1
src/_ZN15dScMgRoulette_c16CleanupResourcesEv.cpp _ZN15dScMgRoulette_c16CleanupResourcesEv ✅ verified 1
src/_ZN15dScMgRoulette_c6RenderEv.cpp _ZN15dScMgRoulette_c6RenderEv ✅ verified 1
src/_ZN15dScMgRoulette_c8BehaviorEv.cpp _ZN15dScMgRoulette_c8BehaviorEv ✅ verified 1
src/_ZN15dScMgRoulette_cD0Ev.cpp _ZN15dScMgRoulette_cD0Ev ✅ verified 1
src/_ZN15dScMgRoulette_cD1Ev.cpp _ZN15dScMgRoulette_cD1Ev ✅ verified 1
src/_ZN15dScMgSnowball_c13InitResourcesEv.cpp _ZN15dScMgSnowball_c13InitResourcesEv ✅ verified 1
src/_ZN15dScMgSnowball_c16CleanupResourcesEv.cpp _ZN15dScMgSnowball_c16CleanupResourcesEv ✅ verified 1
src/_ZN15dScMgSnowball_c6RenderEv.cpp _ZN15dScMgSnowball_c6RenderEv ✅ verified 1
src/_ZN15dScMgSnowball_c8BehaviorEv.cpp _ZN15dScMgSnowball_c8BehaviorEv ✅ verified 1
src/_ZN15dScMgSnowball_cD0Ev.cpp _ZN15dScMgSnowball_cD0Ev ✅ verified 1
src/_ZN15dScMgSnowball_cD1Ev.cpp _ZN15dScMgSnowball_cD1Ev ✅ verified 1
src/actors/dScMgSingle3DBase_c.cpp _ZN19dScMgSingle3DBase_cD1Ev + _ZN19dScMgSingle3DBase_cD0Ev + func_ov006_0210a534 + func_ov006_0210a600 + _ZN19dScMgSingle3DBase_c21AfterCleanupResourcesEj + _ZN19dScMgSingle3DBase_c12BeforeRenderEv + _ZN19dScMgSingle3DBase_c14BeforeBehaviorEv + _ZN19dScMgSingle3DBase_c18AfterInitResourcesEj + func_ov006_0210a708 ✅ verified 9
src/func_ov006_020dac34.cpp func_ov006_020dac34 ✅ verified 1
src/func_ov006_020db6ec.c func_ov006_020db6ec ✅ verified 1
src/func_ov006_020db720.c func_ov006_020db720 ✅ verified 1
src/func_ov006_020db9dc.cpp func_ov006_020db9dc ✅ verified 1
src/func_ov006_020e9c20.cpp func_ov006_020e9c20 ✅ verified 1
src/func_ov006_020f523c.c func_ov006_020f523c ✅ verified 1
src/func_ov006_020f5250.c func_ov006_020f5250 ✅ verified 1
src/func_ov006_020f52c4.c func_ov006_020f52c4 ✅ verified 1
src/func_ov006_020f730c.c func_ov006_020f730c ✅ verified 1
src/func_ov006_020f7320.c func_ov006_020f7320 ✅ verified 1
src/func_ov006_020f7394.c func_ov006_020f7394 ✅ verified 1
src/func_ov006_020f8a3c.c func_ov006_020f8a3c ✅ verified 1
src/func_ov006_020f8c68.c func_ov006_020f8c68 ✅ verified 1
src/func_ov006_020fa4d4.cpp func_ov006_020fa4d4 ✅ verified 1
src/func_ov006_021095cc.cpp func_ov006_021095cc ✅ verified 1
src/func_ov006_02125248.c func_ov006_02125248 ✅ verified 1
src/func_ov006_0212527c.cpp func_ov006_0212527c ✅ verified 1
src/func_ov006_02125364.cpp func_ov006_02125364 ✅ verified 1
src/func_ov006_02128fb8.c func_ov006_02128fb8 ✅ verified 1

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 pushed a commit that referenced this pull request Aug 31, 2026
Two corrections to the section, both measured rather than reasoned.

1. The section told authors to mark ctor/dtor variants "anyway". No landed
   promoted TU does: daObjKinokoTag_c, daObjFm_Battan_c, daObjKm3_Kurumajiku_c
   and daEyBm_c mark zero structors between them. In an inline-destructor TU
   there is nothing to mark -- D1 and D0 have no definition text in the .cpp at
   all -- and a bare marker placed elsewhere is actively harmful, since the
   slice runs from it to the next marker and would charge the following
   member's text to the destructor. Inlining is also usually deliberate: out of
   line, mwcc emits the synthesized D0 ahead of the written D1, reversing
   cartridge order, and linkcheck refuses a TU whose licensed .text is not in
   ROM address order. The rule is now: do not mark them, keep the inline
   definition in a directly included header, and check the fragment survives.

2. The #2064 bullet said its unmarked func_ov006_0210a534 "cannot be rescued
   by a boundary anyway". Measured, the omission does cost -- just not there.
   With no marker after it, the preceding func_ov006_0210a600 fragment runs to
   end of file and swallows a534's volatile body, so an eight-byte 'return 1;'
   can never score readable. Rewritten to make the general point: the member
   that pays is rarely the member you left unmarked.

Reviewer checklist item 7 updated to match.

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

Copy link
Copy Markdown
Collaborator Author

Changes requested — five one-line comment fixes. The boundary itself I reproduced from the ROM, and it is right.

This is the best-evidenced TU promotion that has come through this queue. I had it reviewed by someone who did not write it, with instructions not to take the PR body on trust, and the central claim was re-derived from the cartridge image rather than read off your JSON. Everything I am asking for is a comment. Not one byte moves.

The boundary is real, and here is the independent derivation

extracted/dsd/arm9_overlays/ov006.bin read directly, all 47 _ZTV* symbols in ov006 resolved from config/arm9/overlays/ov006/symbols.txt, every slot decoded. The address point convention was established empirically first rather than assumed — offset-to-top and typeinfo sit at −8/−4 from the _ZTV symbol address:

_ZTV19dScMgSingle3DBase_c @ 0x0213e448
  -8: 0x0        -4: 0x0213bc64   (= _ZTI19dScMgSingle3DBase_c)
  slot  2 = 0x0210a6e4   slot  5 = 0x0210a608   slot  7 = 0x0210a698
  slot 10 = 0x0210a664   slot 16 = 0x0210a4b0   slot 17 = 0x0210a4e8
  slot 26 = 0x0210a600   slot 33 = 0x0210a708

Every slot number your header and body assert is confirmed. Then the reference scan:

total ov006 _ZTV: 47
reference 0x0210a600: 14  (all at slot 26)
reference 0x0210a708: 14  (all at slot 33)
SETS IDENTICAL: True     only in 600: set()     only in 708: set()

The "fourteen vtables, identical reference sets" claim is exactly true — the class plus its thirteen children, matching the thirteen headers that #include "dScMgSingle3DBase_c.h".

Both neighbours were checked, which is the question that actually decides a boundary:

PREV (MgMushroomRoulette range) 0x0210a400  refs=0
NEXT (dScMgSlot1_c D1)          0x0210a8c0  refs=1  slot 16  <- a DIFFERENT class

dScMgSlot1_c is not among the fourteen, so the upper edge is hard. A whole-image word scan across arm9.bin, arm9_dec.bin and every overlay found references to these addresses in ov006.bin and nowhere else — no cross-overlay collision risk. Manifest read through tools/tu_manifest.py export: nine functions, sizes summing to exactly 0x410, no gaps, no overlap, endpoints identical to the nine delink entries they replace.

Key function confirmed too. The inline ~dScMgSingle3DBase_c() {} at header line 95 is skipped, so it is AfterInitResources, this TU defines it, and grep -rln "dScMgSingle3DBase_c::" src/ returns only your file — exactly one possible emitter. Deleting the two _Emit*Destructor forcing scaffolds rather than porting them is the right call and the reasoning survives in the footer.

All eleven licensed symbols were checked against the tree's own symbol tables by address — all eleven correct, all outside the licensed .text range. That is the part that usually goes unchecked until enrollment.

What I need changed

1. include/dScMgSingle3DBase_c.h:105 names a path that has never existed. The new comment block points at src_tu/actors/dScMgSingle3DBase_c.cpp. The file is src/actors/dScMgSingle3DBase_c.cpp.

test -f src_tu/actors/dScMgSingle3DBase_c.cpp                          -> ABSENT
git ls-tree -r --name-only origin/main -- src_tu/actors | grep ScMg    -> (none)

Not on this branch, not on main, not ever. And check_dead_references.py will not catch it: its _prose_targets() scans tools/**/*.py, notes/**, top-level and docs/** markdown, port/docs/*.md, workflow YAML and skill markdown. include/*.h is not a prose surface. One word: src_tu/src/.

2. src/_ZN10dBgActor_cD0Ev.cpp:21 names a file this PR deletes.

 * Same shape, same fix, same
 * sentence in src/_ZN19dScMgSingle3DBase_cD0Ev.cpp, which has had a key
 * function since #1544.

You already fixed the identical sentence in notes/actor-vtables.md, repointed at the promoted path — correctly. That one is inside a gate-scanned prose surface; this one is inside src/, which is not, so it survived. Same fix. This is reviewer checklist item 5 of notes/tu-promotion-conventions.md in substance: a promotion should not leave a comment naming a file it deleted.

3. The generated banner contradicts the file's own body. The header says ov006/dScMgSingle3DBase_c (8 function(s)) and lists eight legacy sources; src/func_ov006_0210a708.c is missing from the assembled-from list. Twenty lines down the body says /* ROM ordinal 8 -- func_ov006_0210a708 ... */. This is the artifact a future reader uses to learn what the unit contains, and it is the one thing the boundary correction did not update. (The manifest's boundary_evidence[0] also says "8", but that is a faithful quote of what tu_map reported followed immediately by the correction — that one is honest and should stay.)

4. Add // @symbol func_ov006_0210a534 above line 281. You have six markers and this is the one hand-written member without one. It costs you nothing directly — a534 was not CONVERTED before, so nothing backslides — but it costs its neighbour: _marked_member_fragment slices marker-to-next-marker, so with no marker after it the func_ov006_0210a600 fragment currently runs from line 257 to end of file and swallows a534's volatile body and raw addresses. func_ov006_0210a600 is an eight-byte return 1; that cannot score readable while that is true. One line unblocks it.

Your D1/D0 being unmarked is correct and I am not asking you to change it — score_member falls through to _lifecycle_member_fragment, which finds the inline destructor in the directly-included header. I have a PR open (#2065) documenting all of this, and your file is cited in it as the counter-example that gets the ledger right.

5. include/dScMgSingle3DBase_c.h:90 still says the inline destructor is the key function. /* Declared first (key function); overrides slots 16 (D1) and 17 (D0). */ contradicts your own correct reasoning at lines 310-313 that an inline dtor cannot be one. Pre-existing, but you rewrote the block immediately below it.

Two disclosures I would like, not blockers

func_ov006_0210a534 has zero vtable corroboration. It is referenced by none of the 47 tables. Its membership rests on tu_map's contiguous linker run plus address containment between two vtable-proven members. That is very probably right — a free function sandwiched inside a class's run is a normal thing — but it is a weaker evidence class than the other eight, and the title "vtable-proven boundary" reads as covering all nine. One sentence in the manifest reason fixes it.

The retyping added a const nobody mentioned. DecompressLZ16(const void*, void*) — neither legacy declaration had it. The intvoid* change is described and is plainly a genuine accuracy fix forced by two extern "C" declarations of the same symbol with contradictory signatures; the const is an unremarked extra. It should be byte-neutral on a callee signature, but say so rather than leave a reader to find it.

Optional and genuinely minor: lines 150/154 carry char *dst = (char *)0x6400000; dst += 0x4000; verbatim from the legacy file. That split is almost certainly steering the literal pool rather than expressing intent, and a reader cannot tell whether it may be folded to (char *)0x6404000. The file goes out of its way to justify volatile and the mangled _ZN2GX... block — this deserves the same one-line treatment.

Gates, and what nobody verified

Re-gated against current main, since main moved out from under your base:

base   origin/main @ a00c09854768        (was 9bfcd5d86d93 when you opened)
target #2064  head 0fda8b4d1b5b          merge tree 5816ea677e9e
  converted-ratchet  dead-references  duplicate-sources  header-offsets
  langmode-ratchet   layout-check     src-tu-refs        source-coverage
                                          ALL pass -> pass
  info: entries 10933 -> 10925 (-8)  -- consolidation, bytes flat
  RESULT: nothing goes green -> red.

The only new commit in the drift is #2063, test-only, no overlap; merge-tree --write-tree is clean.

Being explicit about the limits of the above, because your PR body is precise and this review should be too: no build was run. tubuild verify 9/9 MATCH, differingBytes: 0, objectAudit LICENSED: 9, emittedTextOrderIsRomAscending, the ROM sha256, the moduleSetSha256, and the romdata_check PARTIAL score are author-recorded values verified only for internal consistency and arithmetic. The source-order claim was checked structurally — definitions run 0x708 → 0x6e4 → 0x698 → 0x664 → 0x608 → 0x600 → 0x534, strictly descending, which is the required reverse-of-ROM order — but that is not the same as a compile. The validator's green is what covers those, not me.

Also worth recording: that romdata_check PARTIAL score exists only as prose inside the manifest reason string. There is no machine-readable field holding it, so nothing will notice if it regresses. That is a gap in the tooling, not in your PR, and I am not asking you to fix it here.

Nothing above touches the decomp work. Fix the five comments and I will merge it.

andrewboudreau added a commit that referenced this pull request Aug 31, 2026
* docs: the `// @symbol` marker convention for promoted TUs

tools/tiers.py scores a promoted TU per member, and `// @symbol` is the only
thing that tells it where one member ends. Without a marker score_member falls
back to scoring that member against the ENTIRE FILE, so one volatile object,
one unk_ field or one mangled call anywhere in the TU strips the tier from
every clean member in it. The convention is load-bearing and was documented
nowhere.

Adds section 6 to notes/tu-promotion-conventions.md and a reviewer-checklist
item. Measured against this tree:

  src/actors/daObjKinokoTag_c.cpp       9 members  7 markers  6 banked
  src/actors/daObjFm_Battan_c.cpp       9 members  7 markers  5 banked
  src/actors/daObjKm3_Kurumajiku_c.cpp  5 members  3 markers  4 banked
  src/actors/daEyBm_c.cpp              13 members  0 markers  2 banked

The two banked daEyBm_c members are D1/D0, rescued by the second fallback --
the inline `virtual ~daEyBm_c() {}` at include/daEyBm_c.h:54.

Also corrects a stale line: #2004 closed and Kurumajiku landed via #2057, so
that class is precedent now, not a draft data point.

Documentation-only. No tool, gate, config, ledger or src/ file is touched.

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

* docs: structors are unmarkable by design, not under-marked

Two corrections to the section, both measured rather than reasoned.

1. The section told authors to mark ctor/dtor variants "anyway". No landed
   promoted TU does: daObjKinokoTag_c, daObjFm_Battan_c, daObjKm3_Kurumajiku_c
   and daEyBm_c mark zero structors between them. In an inline-destructor TU
   there is nothing to mark -- D1 and D0 have no definition text in the .cpp at
   all -- and a bare marker placed elsewhere is actively harmful, since the
   slice runs from it to the next marker and would charge the following
   member's text to the destructor. Inlining is also usually deliberate: out of
   line, mwcc emits the synthesized D0 ahead of the written D1, reversing
   cartridge order, and linkcheck refuses a TU whose licensed .text is not in
   ROM address order. The rule is now: do not mark them, keep the inline
   definition in a directly included header, and check the fragment survives.

2. The #2064 bullet said its unmarked func_ov006_0210a534 "cannot be rescued
   by a boundary anyway". Measured, the omission does cost -- just not there.
   With no marker after it, the preceding func_ov006_0210a600 fragment runs to
   end of file and swallows a534's volatile body, so an eight-byte 'return 1;'
   can never score readable. Rewritten to make the general point: the member
   that pays is rarely the member you left unmarked.

Reviewer checklist item 7 updated to match.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Review follow-ups on #2064. Comment and manifest text only -- no code
byte moves; rombuild still reports 106/106 exact, 100.000000%.

  1. include/dScMgSingle3DBase_c.h: the new slot-26/33 note pointed at
     src_tu/actors/dScMgSingle3DBase_c.cpp. The promoted file is
     src/actors/dScMgSingle3DBase_c.cpp.
  2. include/dScMgSingle3DBase_c.h: the destructor was still labelled
     "(key function)", contradicting this TU's own manifest rows. An
     inline destructor is emitted in every TU that needs it and anchors
     nothing; the key function is AfterInitResources, the first DECLARED
     non-inline virtual. Rewritten to say so, keeping the MUST STAY
     INLINE measurement intact.
  3. src/_ZN10dBgActor_cD0Ev.cpp: the "same sentence in ..." pointer
     named src/_ZN19dScMgSingle3DBase_cD0Ev.cpp, a file this PR deletes.
     Repointed at the promoted path, matching the already-correct fix in
     notes/actor-vtables.md.
  4. src/actors/dScMgSingle3DBase_c.cpp: the banner said 8 function(s)
     and listed eight legacy sources while the body carries nine;
     src/func_ov006_0210a708.c was missing from the assembled-from list.
     (boundary_evidence[0]'s "8" stays -- it is a faithful quote of
     tu_map, immediately corrected by boundary_evidence[3].)
  5. src/actors/dScMgSingle3DBase_c.cpp: added the missing
     // @symbol func_ov006_0210a534 marker. _marked_member_fragment
     slices marker-to-next-marker, so without it the func_ov006_0210a600
     fragment ran to end of file and swallowed a534's volatile body and
     raw addresses, keeping a600 -- an eight-byte `return 1;` -- from
     scoring readable. The unmarked D1/D0 are correct and unchanged.

Two disclosures the review asked for:

  - config/tu_manifest.d/ov006/dScMgSingle3DBase_c.json: new
    boundary_evidence entry recording that func_ov006_0210a534 is the
    one member with NO vtable corroboration -- its membership rests on
    tu_map's contiguous run plus address containment, a weaker evidence
    class than the other eight.
  - src/actors/dScMgSingle3DBase_c.cpp: the signature reconciliation
    also added a `const` neither legacy file had
    (DecompressLZ16(const void*, void*)). Nothing in the ROM
    distinguishes const from non-const here, so it is now stated as a
    judgement call rather than left implied.

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

Copy link
Copy Markdown
Collaborator Author

One more, found while reviewing the stacked #2067: a missing @symbol marker

Adding to the five comment fixes above — this one is a scoring defect rather than a prose one, and it is a single line.

src/actors/dScMgSingle3DBase_c.cpp declares nine functions. Two are the destructor pair, covered by the lifecycle fallback. Of the remaining seven, six carry a marker and one does not:

address symbol marker
0x0210a4b0 _ZN19dScMgSingle3DBase_cD1Ev lifecycle fallback — fine
0x0210a4e8 _ZN19dScMgSingle3DBase_cD0Ev lifecycle fallback — fine
0x0210a534 func_ov006_0210a534 missing
0x0210a600 func_ov006_0210a600 ok
0x0210a608 _ZN19dScMgSingle3DBase_c21AfterCleanupResourcesEj ok
0x0210a664 _ZN19dScMgSingle3DBase_c12BeforeRenderEv ok
0x0210a698 _ZN19dScMgSingle3DBase_c14BeforeBehaviorEv ok
0x0210a6e4 _ZN19dScMgSingle3DBase_c18AfterInitResourcesEj ok
0x0210a708 func_ov006_0210a708 ok

It matters more than one missing marker usually would, because of where it sits. _marked_member_fragment slices from one marker to the next one, and your last marker is at line 257 in a 323-line file. func_ov006_0210a534 is defined after it, so there is no next marker — the fragment for func_ov006_0210a600 runs 257 → EOF and swallows func_ov006_0210a534 whole. Two members are affected by the one omission: the unmarked one falls to the whole-file fallback, and the marked one gets scored against text that is not its own.

notes/tu-promotion-conventions.md §6 (landed as #2065) is the rule; it calls this "the cheapest rule in this note to satisfy and the most expensive one to skip", which is exactly the shape of it here.

I have asked for the same fix at larger scale on #2067, which needs eleven. Both are on your stack, so one push covers both.

@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

All five review fixes pushed as ea9f055, plus the two disclosures you asked for. Comment and manifest text only — no code byte moves; rombuild --no-rom in a wired worktree still reports 106/106 exact, 100.000000%, and check_header_offsets include/dScMgSingle3DBase_c.h gives 1 commented fields, 0 mismatched, 0 unparsed (the new comment sits above the destructor, so the Allman-brace trap from #2068 is not armed here).

# fix
1 include/dScMgSingle3DBase_c.h: src_tu/actors/…src/actors/dScMgSingle3DBase_c.cpp
2 same header: the destructor is no longer called the key function. It now says an inline dtor is emitted in every TU that needs it and anchors nothing, and that the key function is AfterInitResources, the first DECLARED non-inline virtual — which is what this TU's own manifest rows already said. The MUST STAY INLINE measurement is untouched.
3 src/_ZN10dBgActor_cD0Ev.cpp:21: repointed at the promoted path, matching the already-correct notes/actor-vtables.md
4 banner (8 function(s))(9 function(s)), and src/func_ov006_0210a708.c added to the assembled-from list at [8]. boundary_evidence[0]'s "8" stays — it is a faithful quote of tu_map, corrected on the spot by boundary_evidence[3].
5 // @symbol func_ov006_0210a534 added above the definition, so _marked_member_fragment stops slicing a600's fragment to end of file. The unmarked D1/D0 are correct and unchanged.

Disclosures:

  • a534's evidence class. New boundary_evidence entry stating plainly that func_ov006_0210a534 is the one member with no vtable corroboration — its membership rests on tu_map's contiguous run plus address containment, and it would be the first member to re-examine if the boundary is ever contested.
  • The unremarked const. The signature reconciliation added DecompressLZ16(const void*, void*), which neither legacy file had. Nothing in the ROM distinguishes const from non-const there, so the file now says it is a judgement call and says what would falsify it.

I left lines 150/154 (char *dst = (char *)0x6400000; dst += 0x4000;) alone. I have no measurement showing the split is literal-pool steering rather than transcription, and a comment asserting either way would be a guess — happy to measure it as a follow-up if you want the note.

@andrewboudreau
andrewboudreau merged commit c97a323 into main Aug 31, 2026
8 checks passed
@andrewboudreau
andrewboudreau deleted the cpp/minigame-tu branch August 31, 2026 08:38
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