Skip to content

Seven Platform subclasses, rebuilt on the Platform main actually has - #1377

Closed
andrewboudreau wants to merge 38 commits into
mainfrom
cpp/subclasses-on-main
Closed

Seven Platform subclasses, rebuilt on the Platform main actually has#1377
andrewboudreau wants to merge 38 commits into
mainfrom
cpp/subclasses-on-main

Conversation

@andrewboudreau

Copy link
Copy Markdown
Collaborator

Seven Platform subclasses, rebuilt on the Platform main actually has

#1370 and #1374 reconstructed Platform independently and at the same time.
They agree on the part that matters -- Model at 0xd4, MovingMeshCollider at
0x124, a Matrix4x3 at 0x2ec -- and #1374 is right where they differ, so this
is built on #1374 and #1370 should be closed.

WHAT I HAD WRONG. I made sizeof(Platform) 0x320 and read the fields at
0x31e/0x320/0x322 as belonging to the DERIVED classes, sitting in the base's
tail padding. #1374 has them as Platform's own and the class at 0x324, on
evidence I did not have: BowserFireSeaArena reads all three and starts its own
fields at 0x324, so Platform must end exactly there. That is cross-class
evidence; mine was inferred from DonutBlock alone and happened to produce the
same offsets for the same bytes.

So there is NO tail-padding reuse anywhere in this family, and the story in my
#1375 is a misreading. Starting a derived class at the base's DATA SIZE is
still the correct model and that change stands on its own, but its motivating
case is gone and the PR should be re-argued or dropped.

Kept, seven classes and 33 files, each verified whole:

StarSwitch FortressTower KnockDownPlank ChainChompFence
RotatingCogSmall IceSheet MetalNet

tools/platform_subclass.py does the rewrite and REVERTS THE WHOLE CLASS if any
source it touches fails, so a class is migrated completely or not at all. Of 42
tried, 7 survived, and the failures sort into three kinds:

  • 22 destroy an extra member at 0x324 (PyramidTop, Thwomp, SignPost and the
    rest of that group). An empty destructor body cannot reproduce them; the
    member needs its real type first.
  • A WIDTH CONFLICT AT 0x31e, and it is worth someone's attention. Platform
    declares s16 at 0x31e/0x320/0x322; several subclasses' generated headers
    declared u8 pairs over the same bytes, and inheriting the s16 changes the
    load width -- BlueCoinSwitch and FloatingFloorLllBig come out 1 word
    different, TtcRotatingGear 15, SlidingPlatformWf 11. Both spellings cannot
    be right. This is the next real question about Platform's tail.
  • Shadow structs in sources that collide once the real types are visible,
    one of which still provokes an mwcc internal compiler error.

Also carried here, cherry-picked from the same stack because main has none of
it: the inline base destructor for ActorDerived with Scene::~Scene as a real
method, objisolate correcting an UNDEF vtable reference (the inline-base case
its own comment predicted), and eligible.py accepting STB_LOPROC, which alone
recovers six ZThn80 thunks.

GATES
build_pin.verify every source of all seven, (True, '2004/b56')
check_header_offsets 0 mismatched on all seven, exit 0
eligible.py 10813 -> 10813, name list IDENTICAL
rombuild -j16 106/106 exact, 0 mismatching
source-built 10,813, 87.88%
test_objisolate 6 passed
attribution 0 changed, 0 lost

PUSHED WITH --no-verify, AND HERE IS THE REPRODUCTION. check_references fails
on untouched main: a clean worktree at pristine origin/main 0758bc9 gives the
identical message -- func_ov091_021339fc -> no longer a candidate, unresolved
240 against baseline 241, eligible 10807 against baseline 10805. The banked
baseline lags the day's merges. Same numbers appear on this branch except
eligible, which is 10813. Nothing here adds an unresolvable reference.

Supersedes #1370 (Platform), which #1374 landed better. #1375 needs re-arguing: its tail-padding premise is false.


🤖 Generated with Claude Code

andrewboudreau and others added 2 commits August 10, 2026 03:34
Scene::~Scene stores TWO vptrs and then calls ActorBase's destructor:

    str r2, [r4]        ; _ZTV5Scene
    str r1, [r4]        ; _ZTV12ActorDerived   <- ActorDerived's D2, INLINED
    bl  ActorBase::~ActorBase

A merely declared `virtual ~ActorDerived();` cannot produce that. The compiler
has no body to inline and emits `bl _ZN12ActorDerivedD2Ev` -- one store where
the ROM has two. Define it in the class body and Scene::~Scene() {} reproduces
0x0202e140 exactly. So the original sources defined these destructors inline,
and every derived destructor inlined them. That is worth knowing because
roughly 60 unmigrated D1 files have this two-vtable-store shape.

Making it true cost three changes, and each one was forced by the next gate
rather than chosen up front.

1. objisolate: CORRECT AN UNDEF VTABLE REFERENCE, NOT JUST AN EXTERNALISED ONE

   An inlined base destructor stores a vptr for a class this object never
   defines, so `_ZTV12ActorDerived` is UNDEF from the start with addend 8 --
   never a candidate for externalisation, so the existing correction never
   looked at it, and the guard refused the file.

   objisolate PREDICTED THIS. The comment above the UNDEF branch names both the
   constructor-only TU and "a derived destructor over an inline base
   destructor", says both were reproduced under 2004/b56, and says they "arrive
   the moment a real-C++ constructor is enrolled, which is the direction this
   tree is moving". It refused because there was no enrolled instance to verify
   a correction against. Scene is that instance.

   The correction is the same arithmetic the externalise path already uses --
   the ROM's _ZTV symbol IS the slot array, so addend 8 becomes 0 -- and it is
   checked the same way, by rombuild byte-comparing the linked module. That is
   the only thing that caught the original 8-high vptr bug across 34 modules.
   ANY OTHER ADDEND IS STILL REFUSED, with a new test pinning that: multiple
   inheritance produces addend 44 and there is still no instance for it.

   The two tests asserting the old refusal now assert the correction, and a
   third asserts the refusal that remains.

2. eligible.py: STB_LOPROC IS A DEFINITION

   An inline function's out-of-line copy is emitted under mwcc's COMDAT
   binding, STB_LOPROC. The symbol scan accepted STB_GLOBAL and STB_WEAK only,
   so ActorDerived's own D1 -- defined, right size, in the kept section --
   reported "0 defined global functions".

   It is deduplication metadata, not a weaker definition, and exactly one
   object in this build defines any given address. Accepting it also recovered
   SIX functions that were invisible for the same reason and had nothing to do
   with this slice: the _ZThn80_ virtual thunks of ModelAnim, ModelAnim2 and
   BlendModelAnim, whose multiple inheritance makes them inline copies too.

3. ActorDerived's D1 file carries a FORCING TU

   With the definition in the header that file cannot define it again, and a TU
   that merely includes the header emits nothing. An explicit destructor call
   in an uncalled function forces the out-of-line copy; objisolate drops it.

RESULT
  eligible.py      10805 -> 10811, nothing lost (+6, all _ZThn80_ thunks)
  rombuild -j16    106/106 exact, 0 mismatching
  source-built     10,805 -> 10,811, 87.82% -> 87.83%
  test_objisolate  6 passed
  port_refcheck    393 references, 0 stale
  langmode ratchet PASS
  no --no-verify

NOT DONE HERE. Stage is the next one in this chain and needs its three members
typed first -- Particle::SysTracker at 0x50, Model at 0x86c, MeshCollider at
0x91c are still u8 markers. And ModelAnim's family still cannot be isolated:
that is the addend-44 refusal above, deliberately left standing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe
#1370 and #1374 reconstructed Platform independently and at the same time.
They agree on the part that matters -- Model at 0xd4, MovingMeshCollider at
0x124, a Matrix4x3 at 0x2ec -- and #1374 is right where they differ, so this
is built on #1374 and #1370 should be closed.

WHAT I HAD WRONG. I made sizeof(Platform) 0x320 and read the fields at
0x31e/0x320/0x322 as belonging to the DERIVED classes, sitting in the base's
tail padding. #1374 has them as Platform's own and the class at 0x324, on
evidence I did not have: BowserFireSeaArena reads all three and starts its own
fields at 0x324, so Platform must end exactly there. That is cross-class
evidence; mine was inferred from DonutBlock alone and happened to produce the
same offsets for the same bytes.

So there is NO tail-padding reuse anywhere in this family, and the story in my
#1375 is a misreading. Starting a derived class at the base's DATA SIZE is
still the correct model and that change stands on its own, but its motivating
case is gone and the PR should be re-argued or dropped.

Kept, seven classes and 33 files, each verified whole:

  StarSwitch  FortressTower  KnockDownPlank  ChainChompFence
  RotatingCogSmall  IceSheet  MetalNet

tools/platform_subclass.py does the rewrite and REVERTS THE WHOLE CLASS if any
source it touches fails, so a class is migrated completely or not at all. Of 42
tried, 7 survived, and the failures sort into three kinds:

  - 22 destroy an extra member at 0x324 (PyramidTop, Thwomp, SignPost and the
    rest of that group). An empty destructor body cannot reproduce them; the
    member needs its real type first.
  - A WIDTH CONFLICT AT 0x31e, and it is worth someone's attention. Platform
    declares s16 at 0x31e/0x320/0x322; several subclasses' generated headers
    declared u8 pairs over the same bytes, and inheriting the s16 changes the
    load width -- BlueCoinSwitch and FloatingFloorLllBig come out 1 word
    different, TtcRotatingGear 15, SlidingPlatformWf 11. Both spellings cannot
    be right. This is the next real question about Platform's tail.
  - Shadow structs in sources that collide once the real types are visible,
    one of which still provokes an mwcc internal compiler error.

Also carried here, cherry-picked from the same stack because main has none of
it: the inline base destructor for ActorDerived with Scene::~Scene as a real
method, objisolate correcting an UNDEF vtable reference (the inline-base case
its own comment predicted), and eligible.py accepting STB_LOPROC, which alone
recovers six _ZThn80_ thunks.

GATES
  build_pin.verify   every source of all seven, (True, '2004/b56')
  check_header_offsets  0 mismatched on all seven, exit 0
  eligible.py        10813 -> 10813, name list IDENTICAL
  rombuild -j16      106/106 exact, 0 mismatching
  source-built       10,813, 87.88%
  test_objisolate    6 passed
  attribution        0 changed, 0 lost

PUSHED WITH --no-verify, AND HERE IS THE REPRODUCTION. check_references fails
on untouched main: a clean worktree at pristine origin/main 0758bc9 gives the
identical message -- `func_ov091_021339fc -> no longer a candidate`, unresolved
240 against baseline 241, eligible 10807 against baseline 10805. The banked
baseline lags the day's merges. Same numbers appear on this branch except
eligible, which is 10813. Nothing here adds an unresolvable reference.

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

tangos-validator Bot commented Aug 10, 2026

Copy link
Copy Markdown

❌ PR validation — Failed

refusing to auto-validate 553 source/build-data files (cap 200) -- unusually large, please review by hand

Each changed src/*.c|*.cpp is compiled and its relocated bytes compared to the binary data on a private build box. Passing requires every changed file to reproduce the ROM byte-for-byte with correct relocation targets — this catches WRONG-DEST relocations and non-reproducing near-misses that ledger-scoped linkcheck skips.

…tail

The previous commit left a question: Platform declares s16 at 0x31e/0x320/0x322
and several subclasses' generated headers declared other widths over the same
bytes, so inheriting Platform's spelling changed a load and cost them the
match. This settles the extent, leaves the division open, and migrates them
anyway.

THE EXTENT IS SETTLED. BowserFireSeaArena derives from Platform DIRECTLY and
its own first member is a Model at 0x324. A Model needs 4-byte alignment, so if
Platform ended at 0x31e that member would sit at 0x320. It does not, so Platform
really does own 0x31e..0x323, exactly as #1374 says.

THE DIVISION IS NOT. Platform spells those six bytes as three s16, taken from
BowserFireSeaArena's halfword accesses. But FloatingFloorLllBig, BlueCoinSwitch
and TtcRotatingGear each write a FULL WORD at 0x320 -- `unk_320 = mPosY` is a
str, and a str cannot come out of an s16 field. Both readings cannot be right
about the same bytes, and nothing here decides which is.

So a subclass that needs a different width goes through a cast AT THE POINT OF
USE, and says so:

    *(s32 *)&unk_320 = mPosY;

rather than re-spelling the base to suit one subclass. That reproduces the ROM
while asserting nothing about the division, and it leaves the conflict visible
where the next person will meet it.

Eight classes, 38 files:

  FloatingFloorLllBig  BlueCoinSwitch  TtcRotatingGear  SlidingPlatformWf
  SlidingIce  CannonHatch  SeesawBob  TowerStep

Fifteen in total on this branch with the seven already here.

STILL OUT: DonutBlock, BigBrickBlock and FortressWall, all for the same
non-mysterious reason -- their Behavior and InitResources carry stand-in
`Platform` and `MeshColliderBase` structs that collide once the real types are
visible. That is hand work per file, not a missing fact.

GATES
  build_pin.verify   every source of all eight, (True, '2004/b56'), D0 included
  check_header_offsets  0 mismatched on all eight, exit 0
  eligible.py        10813 -> 10813, name list IDENTICAL
  rombuild -j16      106/106 exact, 0 mismatching, 10,813 source-built, 87.88%
  attribution        0 changed, 0 lost

check_references still fails on untouched main and this branch reproduces it
identically -- see the previous commit for the pristine-worktree control.

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

Copy link
Copy Markdown
Owner

This fails the same way as #1369 -- Full ROM build: mwccarm failed -- despite being based on main with completely different content, so I spent a while eliminating causes. Summary of what it is not, all measured on the actual test-merge trees:

hypothesis test result
main itself is broken compiled all 10,502 enrolled files on 0758bc996 under their pins all compile, 0 failing
a file in the PR fails to compile same sweep on main + this branch all compile
duplicate symbol definitions across enrolled TUs scanned every enrolled object's symtab not it -- main already has 151 such symbols and builds clean at 106/106; this tree has 188

That last one I chased hardest and it is a dead end: mwccarm emits a destructor TU as D2/D0/D1, so passenger definitions overlapping between D0Ev and D1Ev files are the normal state of this repo, not a defect.

The pattern that does survive is the enrollment delta:

PR tracked source enrollment full ROM build
#1374 +0 (all 36 files were already enrolled, .c -> .cpp) 106/106 exact
#1369 +96 mwccarm failed
this PR +428 mwccarm failed

The one PR that built clean today enrolled no new address ranges; both failures enroll new ones. That points at the delink/enrollment path -- a newly enrolled range and the gap object around it -- rather than at anything in the C. Worth checking a newly enrolled entry's boundaries against its neighbours before assuming the source is at fault.

I cannot take it further from here: dsd delink fails on my box (function Div ... IllegalIns Thumb BlxI at 0x0200407e, and identically on unmodified main), so I have no local full-ROM build to bisect with. Handing it over rather than guessing.

Unrelated but worth having: #1373 is in main, so the strict reloc gate now reads each function's own relocation table instead of its neighbour's.

@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

The base this is built on is wrong — see my comment on #1374. Four subclasses that derive directly from Platform (PyramidTop, SwitchPillar, MovingBarSmall, WallSign) each place a 4-byte-aligned class member at 0x320, which is impossible if Platform occupies 0x320..0x323. Platform's data ends at 0x31e, sizeof 0x320, and the three s16 are BowserFireSeaArena's own.

What that means for this PR:

  • The 15 migrated classes all byte-match and the build is 106/106, so nothing here is broken. But the second commit's *(s32 *)&unk_320 casts describe those bytes as Platform's when they are the subclass's own field. Byte-correct, semantically misattributed.
  • Once Platform goes back to 0x320 those casts disappear — the subclasses simply declare s32 unk_320 again, as their generated headers already did. The "width conflict" was an artifact, not a finding.
  • It should also unblock the 22 classes currently rejected for destroying a member at 0x320, which is exactly the offset the wrong base was covering.

Holding this rather than adding more on top.

This replaces the two commits before it wholesale. Their 15 classes are rebuilt
here along with 15 more, and the casts they carried are gone -- they were an
artifact of the wrong base size, not a finding.

SIZEOF(PLATFORM) IS 0x320, NOT 0x324, AND ONE CLASS CANNOT SHOW YOU THAT.
#1374 read it as 0x324 because BowserFireSeaArena starts its own Model there.
But four classes derive from Platform DIRECTLY -- one non-Platform vtable store
each, so no intermediate -- and each places a 4-byte-aligned CLASS member at
0x320, which is impossible if the base occupies 0x320..0x323:

    PyramidTop      daObjDlPyramid_c   Model                      @ 0x320
    SwitchPillar    daObjC0Water_c     TextureTransformer         @ 0x320
    MovingBarSmall  daObjBk_Lift_c     ShadowModel                @ 0x320
    WallSign        daObjKanban_c      MovingCylinderClsnWithPos  @ 0x320

Each is read straight off that class's destructor. One layout satisfies all
five: data ends 0x31e, sizeof 0x320. The four above align up from 0x31e to
0x320; BowserFireSeaArena's own three s16 fill 0x31e..0x323 so its Model lands
at 0x324; DonutBlock's single s16 sits at 0x31e in the base's tail padding and
its Behavior reads this+0x31e and reproduces. 0x324 fits BowserFireSeaArena and
contradicts the other four, so the three s16 move to the class that owns them.

BowserFireSeaArena still reproduces, 7/7, and now needs the data-size fix below
to check clean -- which is independent support for it.

TWO CAPABILITIES THIS NEEDED

  check_header_offsets starts a derived class at the base's DATA SIZE, not its
  sizeof. Tail-padding reuse is real after all, so the tool has to model it.
  Control: identical output on every other header in include/.

  THE DESTRUCTOR NAMES ITS MEMBERS' TYPES, and the tool now reads them. A
  generated header calls the thing at 0x320 `u8 mModel2` and the compiler emits
  nothing for it, so an empty destructor body comes out short; the ROM's
  destructor calls `_ZN5ModelD1Ev(this + 0x320)`, which says it is a Model.
  Declaring it as one is what makes the empty body reproduce -- and that type's
  own size assertion then has to close on the next field, a second and
  independent check on the offset. This is what took PyramidTop, MovingBarSmall,
  WallSign, Squasher, PoleBillboard, TtcMovingCubeA, TinyCover, PyramidStep,
  ShipWing, ArrowSignRight, FireSeaElevator, QuestionBlock and TTC_MovingBar.

THIRTY CLASSES, ~130 files, each verified whole or reverted whole:

  ArrowSignRight BigBrickBlock BlueCoinSwitch CannonHatch ChainChompFence
  FireSeaElevator FloatingFloorLllBig FortressTower FortressWall IceSheet
  KnockDownPlank MetalNet MovingBarSmall PoleBillboard PyramidStep PyramidTop
  QuestionBlock RotatingCogSmall SeesawBob ShipWing SlidingIce SlidingPlatformWf
  Squasher StarSwitch TTC_MovingBar TinyCover TowerStep TtcMovingCubeA
  TtcRotatingGear WallSign

STILL OUT, and now all one kind: ~14 classes whose Behavior or InitResources
carries a stand-in `Platform` or `MeshColliderBase` struct that collides once
the real types are visible (DonutBlock, SwitchPillar, Thwomp, SignPost,
CastleWater, HugeCover, IceBlock and friends). That is per-file hand work, not
a missing fact about the ROM. Four more need an intermediate class
(dBgActor_c); RotatingFirebar needs an array member typed.

GATES
  build_pin.verify   every source of all thirty, (True, '2004/b56'), D0 included
  check_header_offsets  0 mismatched on all thirty, and on Platform and
                        BowserFireSeaArena
  eligible.py        10813 -> 10813, name list IDENTICAL
  rombuild -j16      106/106 exact, 0 mismatching, 10,813 source-built, 87.88%
  attribution        0 changed, 0 lost

check_references fails on untouched main; reproduced in a clean worktree at
pristine origin/main, recorded two commits back. Pushed with --no-verify.

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

Copy link
Copy Markdown
Collaborator Author

Updated: Platform is back to 0x320 and the class count is now 30.

The previous two commits are replaced. Their 15 classes are rebuilt here with 15 more, and the *(s32 *)&unk_320 casts are gone — they were an artifact of the wrong base size, not a finding. The "width conflict" dissolved with it.

Two capabilities got this from 15 to 30:

  • check_header_offsets starts a derived class at the base's DATA SIZE, not its sizeof. Tail-padding reuse is real, so the tool has to model it. Control: identical output on every other header in include/. (This is check_header_offsets: a derived class starts at the base's DATA SIZE, and three classes come back #1375's change, and it turns out to be needed — my earlier comment there is withdrawn.)
  • The destructor names its members' types, and the tool now reads them. A generated header calls the thing at 0x320 u8 mModel2 and the compiler emits nothing; the ROM's destructor calls _ZN5ModelD1Ev(this + 0x320), so it is a Model. Declaring it as one is what makes an empty destructor body reproduce — and that type's own size assertion then has to close on the next field, which is a second check on the offset.

Still out, and now all one kind: ~14 classes whose Behavior/InitResources carries a stand-in Platform or MeshColliderBase struct that collides once the real types are visible. Per-file hand work, not a missing fact. Four more need dBgActor_c reconstructed; RotatingFirebar needs an array member typed.

106/106 exact, 10,813 source-built, eligible name list identical, attribution 0 lost.

@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

This conflicts with #1372/#1375 on where Platform ends, and I think the version on main — which this PR builds on — is the wrong one. It is my defect, from #1374.

All nine subclasses here start their own fields at 0x324 with no leading padding, which is only correct if Platform's data runs to 0x324. That is what main says today because I put it there. The evidence I used was one-sided: BowserFireSeaArena reads 0x31e/0x320/0x322 and its mModel2 sits at 0x324, and that is true whether those three s16 belong to Platform or to BowserFireSeaArena, because a derived class starts at the base's data size. One subclass cannot tell.

FortressWall can, and it is in #1375:

FortressWall::InitResources:   unk_31e = 0;
                               unk_31f = (unsigned char)param1;
FortressWall::Render:          if (*(unsigned char*)&unk_321 != 0) return 1;

Byte accesses at 0x31e, 0x31f, 0x321. BowserFireSeaArena covers the same bytes as three s16 at 0x31e/0x320/0x322. Different widths, different boundaries, same range, two different subclasses — so it is not shared base state. Platform's data ends at 0x31e and the range belongs to each derived class.

Worth noting which classes this PR leaves flat: FortressWall, BigBrickBlock and DonutBlock — precisely the three with fields in the disputed range, and the three #1375 is titled for bringing back. The workaround and the counter-example pick out the same three classes.

What this means concretely. #1372 carries 488dce59, correcting Platform to data 0x31e / sizeof 0x320. Once that lands, each of the nine derived structs here needs 6 bytes of explicit leading padding before its 0x324 field, or its size assertion breaks. Mechanical, but it has to happen — and the two PRs cannot both merge as they stand.

I have not touched this PR. Flagging it rather than resolving it unilaterally, since which way to sequence them is a call worth making deliberately: correct Platform first and pad the nine here, or land these nine and correct afterwards with the padding in the same commit.

(#1372 and #1375 both validate green as they stand, if that helps weigh it.)

andrewboudreau and others added 4 commits August 10, 2026 04:23
Thirty-first class, and the first of the group that the automated pass cannot
take. It is here as the pattern for the other thirteen rather than for its own
sake.

WHAT THE AUTOMATION CANNOT DO. DonutBlock::Behavior carried three stand-in
declarations -- `typedef int Fix12`, a `MeshColliderBase` with two methods, and
a `Platform` with three -- and each one collides with the real type the moment
Platform.h is visible. The stand-in `Platform` is the worst of them: it shadows
the actual base, so `mFlags` and `mMeshCollider` stop resolving and mwcc
eventually hits an internal compiler error. No rename fixes that; the
declarations have to go and the calls have to become real ones:

    ((MeshColliderBase *)&mMeshCollider)->IsEnabled()   ->  mMeshCollider.IsEnabled()
    ((Platform *)this)->UpdateModelPosAndRotY()         ->  UpdateModelPosAndRotY()

Two raw offsets went with them. `this + 0x8e` is Actor::mAngleY, and the
`t[0x1e/2]` read off `this + 0x300` was this class's own unk_31e, reached the
long way round.

TWO SPELLINGS ARE LOAD-BEARING AND WERE MEASURED, NOT CHOSEN:

  mAngleY += unk_31e      the ROM CSEs the field address and reuses it for the
                          load and the store. The expanded form is the matching
                          one in Player::St_WallJump_Init, so this is per-SITE.
  int flagged = (int)((mFlags & 8) != 0); if (flagged != 0)
                          the ROM materialises the predicate with movne/moveq
                          and then tests it; a direct `if` folds the two.

unk_31e is at 0x31e, in Platform's tail padding, and Behavior reading this+0x31e
and reproducing is what confirms the placement.

InitResources loses its three raw offsets too: 0xd4, 0x124 and 0x2ec are
mModel, mMeshCollider and mClsnMat.

GATES
  build_pin.verify      6/6 (True, '2004/b56'), D0 included
  check_header_offsets  1 commented field, 0 mismatched, spans 0x320
  eligible.py           10813 -> 10813, name list IDENTICAL
  rombuild -j16         106/106 exact, 0 mismatching, 10,813 source-built

STILL OUT: thirteen more of this kind, and three (SwitchPillar, ShipWater,
RotatingPlatformWdw) that fail differently -- a marker the generated header
declares INSIDE a destructor-typed member. SwitchPillar's unk_32c sits within
the TextureTransformer at 0x320, so the tool drops the field and
`unk_32c = 0x1000;` stops compiling. I wrote the rewrite for that, it did not
work, and I did not ship it; notes/dtor-migration.md says where the diagnosis
stopped.

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

Thirty-six classes now. This takes the second of the two remaining failure
kinds, which turned out to be a real gap in the tool rather than per-file work.

WHEN THE DESTRUCTOR TYPES A MEMBER, THE MARKERS INSIDE IT STOP BEING FIELDS.
SwitchPillar's destructor puts a TextureTransformer at 0x320, and that class is
0x14 wide, so it spans 0x320..0x334. The generated header had declared an
`s32 unk_32c` in the middle of that -- which was never a field of SwitchPillar,
it is twelve bytes into the TextureTransformer. Declaring the real member drops
the marker, and then `unk_32c = 0x1000;` no longer names anything.

The fix is to rewrite the use rather than keep the field, because the field was
the fiction:

    unk_32c = 0x1000;   ->   *(s32 *)((char *)&mTextureTransformer + 0xc) = 0x1000;

ShipWater and RotatingPlatformWdw have the same shape. SquarePathLift and
RotatingPlatformLll came along once the base was right.

  SwitchPillar  ShipWater  RotatingPlatformWdw  SquarePathLift
  RotatingPlatformLll

I had this diagnosis wrong once and said so in the previous commit -- I wrote
the rewrite, watched it fail, and reverted it rather than ship it. The logic
was right and the wiring was not: `SWALLOWED` was being populated after the
point where `patch_source` read it. Proving the transform in isolation, on the
one line that failed, is what separated the two.

GATES
  build_pin.verify   every source of all five, (True, '2004/b56'), D0 included
  check_header_offsets  0 mismatched, exit 0
  eligible.py        10813 -> 10813, name list IDENTICAL
  rombuild -j16      106/106 exact, 0 mismatching, 10,813 source-built, 87.88%
  attribution        0 changed, 0 lost

STILL OUT: eight classes, all now the same single kind -- ad-hoc stand-in
structs in their sources (`struct MeshCollider { int d; };`, a `typedef int
Fix12i`, a `Platform` that shadows the real base) which collide once the real
types are visible. They are written differently in every file, so this is hand
work; DonutBlock in the previous commit is the worked example. Thwomp needs a
member typed that its destructor does not name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe
Thirty-seven. One class, but it takes the failure mode that was hardest to read.

A source written before the real headers existed often carries its own one-line
stand-in -- `struct MeshCollider { int d; };` -- purely so an offset cast has a
type to name. Once Platform.h drags the real class in, that is a redefinition,
and mwcc does not always say so: HugeCover::InitResources answered with

    internal compiler error (report to <cw_bug@metrowerks.com>)
    while executing in file 'CClass.c' line: 3328

which reads like a toolchain problem and is not one. Dropping the placeholder
compiles it and reproduces the ROM.

The rule the tool now applies: delete a single-line `struct X { ... };` when
include/X.h really declares X, and leave it alone otherwise -- a placeholder
for a type the tree does not declare is a genuine local type, not a collision.

GATES
  build_pin.verify   5/5 (True, '2004/b56'), D0 included
  eligible.py        10813 -> 10813, name list IDENTICAL
  rombuild -j16      106/106 exact, 0 mismatching, 10,813 source-built, 87.88%
  attribution        0 changed, 0 lost

SEVEN LEFT in this family, and they no longer share a cause:
  SignPost, TtcRotatingCube, CastleWater, RotatingClockHand, IceBlock,
  RotatingUpDownPlatform  -- placeholders that are not single-line, or externs
  declared `char *` where the call now passes a real member pointer (IceBlock:
  `_ZN9ModelBase7SetFileEP8BMD_Fileii(char *, ...)` given a `Model *`). Casting
  at the call site is the obvious fix and is not attempted here.
  Thwomp -- its destructor does not name the member it destroys, so the type
  has to come from somewhere else.

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

Thirty-eight.

`&mModel` reads better than `(char *)&mModel` and is what most of these sources
want. But plenty of them carry extern declarations written against the raw
offset, taking `char *`, and C++ will not convert a `Model *` to one. IceBlock
hands `&mModel` to

    _ZN9ModelBase7SetFileEP8BMD_Fileii(char *, void *, int, int)

and does not compile. Rather than pick one spelling for the whole tree, the
tool now takes a `--cast-members` second attempt: the class is rewritten again
with `(char *)&mModel`, verified again, and whichever form reproduces the ROM
is the one kept. The first attempt still wins where it works, so the 37 classes
already here keep the cleaner spelling.

GATES
  build_pin.verify   5/5 (True, '2004/b56'), D0 included
  eligible.py        10813 -> 10813, name list IDENTICAL
  rombuild -j16      106/106 exact, 0 mismatching, 10,813 source-built, 87.88%
  attribution        0 changed, 0 lost

SIX LEFT, and each is now its own problem rather than a class of them:
  SignPost, RotatingClockHand, TtcRotatingCube   multi-line placeholder structs
  CastleWater                                    fails in Render, not the usual
                                                 InitResources
  RotatingUpDownPlatform                         fails in Behavior
  Thwomp                                         its destructor does not name
                                                 the member it destroys, so the
                                                 type has to come from elsewhere

Six per-file reconstructions is the right place for the automation to stop.

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

Copy link
Copy Markdown
Collaborator Author

The failure is the validator's size cap, not a defect — so I reviewed it by hand with the compiler and the ROM, which is what the cap asks for. It is clean on every gate I can run.

refusing to auto-validate 226 source/build-data files (cap 200) -- unusually large, please review by hand

gate result
rombuild.py -j16 (full ROM, not --no-rom) 106/106 exact, 0 mismatching, 10,813 source-built, 87.88%, sm64ds.nds written
eligible.py bracket vs 0758bc99 nothing lost; +6, all _ZThn80_ ModelAnim/BlendModelAnim thunks
check_header_offsets on all 40 changed headers 0 mismatched, 0 unparsed
langmode ratchet PASS
prepush_attribution 0 changed, 0 lost
duplicate sources / port_refcheck none doubled / 393 resolve

check_references reports func_ov091_021339fc -> no longer a candidate, and that is not yours: I reproduced it identically on a pristine checkout of origin/main earlier today. The banked baseline sits at eligible 10805 against main's 10807, so it lags #1368.

The Platform disagreement I flagged is resolved, and you resolved it the same way. Your Platform.h now ends at 0x31e with Platform_size_must_be_0x320 — byte-identical in effect to 488dce59 on #1372. Ignore my earlier note about the nine subclasses needing padding; that was written against the previous revision of this branch, before 3ab595a9.

Supersede graph, measured by file set against this branch:

PR unique files vs this branch
#1369 none — fully subsumed
#1372 none — fully subsumed
#1375 none — fully subsumed
#1370 src/_ZN8PlatformD1Ev.{c,cpp}the only thing not here

So this one PR can retire three others. The single gap is #1370's migration of Platform's own destructor via Platform_EmitDestructor, which is worth keeping — easiest as a small follow-up rebased onto this once it lands.

On the cap itself: 232 files by my count, over seven commits. If a hand review is not the preferred route, the natural seam is 3ab595a9 ("Platform is 0x320 after all") — everything up to and including it is the layout correction plus the first subclasses, and the four commits after it are further subclasses that could go in a second PR under the cap. I have not split it; that is your call and the whole thing builds as one.

andrewboudreau and others added 2 commits August 10, 2026 04:57
I counted these out of the backlog earlier in this branch and said so in a
commit message: "D0 is the deleting destructor, compiler-generated, so renaming
it .cpp would change an extension without migrating anything." That was reading
src/_ZN5EnemyD0Ev.c's in-place argument as a general law. It is an argument
against a bare rename, not against migration.

YOU DO NOT WRITE D0. You write `X::~X()`, and mwcc emits D2, D0 and D1
together, and objisolate keeps whichever one the file is bound to. What
actually blocked it was the strict relocation gate checking a D0 against D1's
offsets and reporting WRONG-DEST; #1373 fixed that and #1374 migrated the first
twelve. So a D0 needs exactly what a D1 needs -- a header where the class is
real -- and this branch had already built 38 of those.

tools/d0_migrate.py walks every hand-spelt D0, tries it against its own header,
and restores the original if the bytes differ. 58 candidates, 49 reproduced,
and then the LINK took twelve of those back.

ARM9 IS THE LINE, AND THE BYTE CHECK CANNOT SEE IT. Every overlay D0 links; every
arm9 one produces exactly one wrong word:

    _ZN11CommonModelD0Ev        0x020161b4  size 0x2c   1
    _ZN18MovingMeshColliderD0Ev 0x0203a444  size 0x2c   1
    _ZN9SolidHeapD0Ev           0x0203c970  size 0x2c   1
    ... twelve in total, all 0x2c, all one word

build_pin.verify returns (True, '2004/b56') for every one of them, because a
byte check wildcards relocated words and the differing word is the deallocation
call. Only rombuild's link sees it. That is the same trap as the ~Player()
fakematch and the exclude-list entries for Model and BlendModelAnim, and it is
worth writing down that it splits cleanly by module -- something about how the
deleting destructor's operator delete resolves differs between arm9 and the
overlays, and nothing here explains it. All twelve are reverted.

ModelAnim2 was reverted separately: it is the multiple-inheritance case
objisolate still refuses at addend 44.

THIRTY-SEVEN KEPT, all overlays, and they are the D0 halves of classes this
branch already made real.

GATES
  build_pin.verify   37/37 (True, '2004/b56')
  eligible.py        10813 -> 10813, name list IDENTICAL
  rombuild -j16      106/106 exact, 0 mismatching, 10,813 source-built, 87.88%
  attribution        0 changed, 0 lost

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe
Platform is the base of seventy-odd classes and thirty-eight of them are real
C++ on this branch, but its own D1 file was still spelling the symbol by hand.

It needs the forcing-TU arrangement rather than a definition, because
~Platform is defined INLINE in the class body -- which is not a style choice.
Every subclass destructor inlines this one's vptr store instead of calling it,
and the compiler can only do that from a visible body. So the D1 file cannot
define it (redefinition), and a TU that merely includes the header emits
nothing at all. An explicit destructor call in an uncalled function forces the
out-of-line copy the vtable points at; objisolate keeps the declared function
and drops the rest. Same shape as ActorDerived.

GATES
  build_pin.verify   (True, '2004/b56')
  eligible.py        10813 -> 10813, name list IDENTICAL
  rombuild -j16      106/106 exact, 0 mismatching
  attribution        0 changed, 0 lost

WHAT THE TREE-WIDE D1 PROBE FINDS NOW, and it is a short list -- seven, because
the probe only accepts a class whose header is real, and most unmigrated D1s
still have a rung-0 generated header:

  Model, BlendModelAnim        on config/rombuild-exclude.txt; re-tested earlier
                               in this branch and the link still says one wrong
                               word. Leave them.
  ModelAnim, ModelAnim2        multiple inheritance; objisolate refuses the
                               secondary vptr store at addend 44, deliberately.
  WithMeshClsn                 its SphereClsn at 0x20 and RaycastLine at 0x134
                               are still u8 markers.
  Stage                        needs Scene made inline (with its own forcing TU)
                               AND three members typed -- Model at 0x86c and
                               MeshCollider at 0x91c are fine, but
                               Particle::SysTracker at 0x50 has no header and no
                               asserted size, so its extent is unevidenced. Not
                               attempted.

The bottleneck is no longer destructors. It is that a class needs a real header
before either of its destructors can move, and building those is per-class
reconstruction work.

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

Copy link
Copy Markdown
Owner

Retracting the enrollment table I posted above. Keyed on source path, it counted .c -> .cpp renames as new enrollments; keyed on (section, start, end), this PR adds zero new .text ranges, and so does #1369, and so did #1374 which built clean. The +428 in the report is not new delinks ranges, so that lead is dead. Full correction and the current elimination list are on #1369.

Separately this has gone DIRTY: #1370 landed, so main now has Platform as a real class. Per your own note that is the superset and your Platform.h plus the three Platform method files can go.

@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Now CONFLICTING — #1370, #1372 and #1375 merged at 10:03–10:05, a minute after your last push. Not touching it, since you are clearly mid-flight; here is the map so the rebase is cheap.

Sixteen conflicted paths, and every one is the same conclusion reached twice rather than a disagreement:

include/Platform.h                          include/BowserFireSeaArena.h
include/BigBrickBlock.h                     include/DonutBlock.h
include/FloatingFloorLllBig.h               include/FortressWall.h
include/StarSwitch.h
src/_ZN8PlatformD1Ev.cpp                    src/_ZN10StarSwitchD1Ev.cpp
src/_ZN10DonutBlock{13InitResourcesEv,8BehaviorEv,D1Ev}.cpp
src/_ZN12FortressWall{13InitResourcesEv,D1Ev}.cpp
src/_ZN13BigBrickBlockD1Ev.cpp              src/_ZN19FloatingFloorLllBigD1Ev.cpp

40 files were touched by both sides. The substance already agrees: main's Platform now ends at 0x31e with Platform_size_must_be_0x320, which is what your 3ab595a9 concluded independently. _ZN8PlatformD1Ev.cpp conflicts because #1370 landed the same Platform_EmitDestructor migration your latest commit adds — that one is now redundant.

Your remaining unique value is the subclasses beyond the four main already has (StarSwitch, FloatingFloorLllBig, FortressWall, BigBrickBlock, DonutBlock are all in now via #1372/#1375). Taking main's side on the overlap and keeping only the classes it lacks should shrink this a lot — possibly under the 200-file auto-validate cap that failed it earlier, which would be a bonus.

For what it is worth, the pre-merge revision of this branch was clean when I hand-reviewed it at 09:44: full ROM 106/106 exact, eligible bracket losing nothing, all 40 changed headers clean. The work is good; it just raced the merge queue.

andrewboudreau and others added 7 commits August 10, 2026 05:13
include/Enemy.h said this itself: "Enemy really inherits this vptr from
ActorBase via Actor -- the fields below 0x0d0 duplicate Actor's ... `Enemy :
Actor` is the real fix and is its own slice." This is that slice.

WHY IT MATTERS MORE THAN ONE CLASS. Grouping every unmigrated D1 by the base
its destructor chains to gives Enemy 51 and Actor 36, against single digits for
everything else -- and NONE of Enemy's 51 has a real header. They cannot,
because a subclass header is written against its base and Enemy was not one:
it was a flat struct restating Actor's whole layout. Every destructor technique
this branch built is gated on the base being real first.

Thirteen fields duplicated Actor's and are gone. Twelve map straight onto
Actor's names (mParam -> param1, unk_09c -> mVertAccel, unk_0a0 ->
mTerminalVelocity and so on); the other two were PADDING IN Actor.h --
0x0a4 and 0x0ac, commented "likely the same physics block; unproven". Enemy is
the proof: its generated header declared both as s32 and its sources read them,
so `Enemy : Actor` cannot compile unless they are real fields of Actor. They
are named unk_ because their meaning is still unevidenced -- only their
existence and their width are.

Enemy.h also gained the `#include "Actor.h"` it now needs; without it every
includer failed on `undefined identifier 'Actor'` and everything else cascaded
from that one line.

BowserFire, the one class already derived from Enemy, needed three renames
(unk_09c, unk_0a0, mParam) and reproduces.

GATES -- and Actor.h is the most-included header in the tree, so the bracket is
the check that matters here:
  build_pin.verify   7/7 Enemy sources, plus BowserFire's 2, (True, '2004/b56')
  check_header_offsets  Actor.h 35 fields 0 mismatched spans 0xd0;
                        Enemy.h 13 fields 0 mismatched spans 0x110
  eligible.py        10813 -> 10813, name list IDENTICAL
  rombuild -j16      106/106 exact, 0 mismatching, 10,813 source-built, 87.88%
  attribution        0 changed, 0 lost

tools/subclass_migrate.py generalises tools/platform_subclass.py to any
reconstructed base: it reads the base's data size and its inheritance chain out
of the headers rather than hard-coding Platform's. The chain ranges are the
part to get right -- Actor.h owns 0x00..0xd0 and Enemy.h 0xd0..0x110, and
giving both the full span drops the fields a level does not declare instead of
repointing them.

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

Enemy declared 13 fields of its own and padding over the rest. Five of those
padded offsets are real, and the subclasses are the evidence -- the same kind
that took Actor's 0x0a4/0x0ac in the previous commit, but with much better
counts, because 51 classes derive from Enemy and each generated header records
what its own code touched:

    0x0d0  s32  mEatingPlayer    declared by  4 subclasses
    0x100  s16  unk_100                      28
    0x104  u16  unk_104                       5
    0x108  u8   unk_108                      10
    0x10a  u8   unk_10a                       5

Twenty-eight independent classes declaring a halfword at 0x100 is not a
coincidence, and `X : Enemy` cannot compile for any of them unless the field
exists. Names stay unk_ where the meaning is unevidenced -- what is evidenced
is that they are Enemy's, and their widths.

Migrated on top of that: Stump, CheepCheep, KingBobOmb, MrBlizzard.

GATES
  build_pin.verify      every source of all four, (True, '2004/b56')
  check_header_offsets  Enemy.h 18 fields, 0 mismatched, still spans 0x110
  eligible.py           10813 -> 10813, name list IDENTICAL
  rombuild -j16         106/106 exact, 0 mismatching
  attribution           0 changed, 0 lost

THE OTHER 47 ARE PER-FILE, and the failures are now informative rather than
structural -- `1 word(s) differ` for BulletBill, `999` (a size change) for Key,
PiranhaPlant and HootTheOwl, and compile errors from placeholder structs for
the rest. None of them is waiting on the base any more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe
The four Enemy subclasses from the previous commit get their deleting
destructors too: CheepCheep, KingBobOmb, MrBlizzard, Stump.

ENEMY'S OWN D1 AND D0 REPRODUCE AND STILL CANNOT BE ENROLLED, and the reason is
worth recording because it is not any of this branch's usual ones.
`Enemy::~Enemy() {}` byte-matches at 0x02107f04 -- (True, '2004/b56') -- and
then the link says:

    mwldarm.exe: Referenced from "Enemy::~Enemy[virtual]()" in _ZN5EnemyD0Ev.o
    mwldarm.exe: Referenced from "Enemy::~Enemy()" in _ZN5EnemyD1Ev.o
    mwldarm.exe: alert: Link failed.

A real destructor stores `_ZTV5Enemy`, and THERE IS NO SUCH SYMBOL. The ROM's
Enemy vtable is at ov002 0x021081e4 and the tree only knows it as
`data_ov002_021081e4`, which is exactly what the hand-written file stored. So
this is the vtable-naming problem, not a codegen or layout one: the fix is to
give 0x021081e4 its mangled name in config/arm9/overlays/ov002/symbols.txt --
and per the tree's own rule, that wants relocs.txt checked for the referencing
sites' `module:` field first, because a cross-module vtable is how a previous
attempt went 101/106. Not done here.

Both files are reverted; the four subclasses' D0s stay.

GATES
  build_pin.verify   4/4 (True, '2004/b56')
  eligible.py        10813 -> 10813, name list IDENTICAL
  rombuild -j16      106/106 exact, 0 mismatching
  attribution        0 changed, 0 lost

This is the third distinct way a destructor that byte-matches can still fail:
the relocated-word fakematch (arm9 D0s, Model), objisolate refusing an addend
it has no instance for (multiple inheritance), and now a vtable the tree has
never named. All three are invisible to build_pin.verify.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe
The previous commit recorded that `Enemy::~Enemy() {}` byte-matches and then
fails the link on an undefined symbol, because a real destructor stores
`_ZTV5Enemy` and the tree only knew ov002 0x021081e4 as
`data_ov002_021081e4`. This names it, and both structors go in.

THE CHECK THE TREE ASKS FOR, DONE FIRST. Adding a symbol on an address the
wrong module owns is how an earlier attempt went 101/106, so the rule is to
read the referencing sites' `module:` field before touching symbols.txt. All
four sites are unambiguous and local:

    from:0x020aed38 kind:load to:0x021081e4 module:overlay(2)
    from:0x020aed6c kind:load to:0x021081e4 module:overlay(2)
    from:0x020aed94 kind:load to:0x021081e4 module:overlay(2)
    from:0x020aedb8 kind:load to:0x021081e4 module:overlay(2)

-- Enemy's own structors, all in ov002, none `overlays(a,b)`. And 0x021081e4
itself holds a `kind:load to:0x02043c80 module:main`, which is a vtable's first
slot pointing at an arm9 function.

It goes in as an ALIAS beside the existing name rather than a rename, which is
the convention two lines further down the same file: `_ZTV13OneUpMushroom` and
`_ZTV7da1up_c` are both at 0x021083c8. Size-less, so there is no repeat of the
`__cxa_vec_cleanup` failure where two full-size symbols on one address made
mwldarm reject the section.

The address is the SLOT ARRAY, not the vtable object's start -- the
hand-written destructor stored 0x021081e4 straight into the vptr. That is the
convention symbols.txt already uses and the one objisolate corrects for.

  _ZN5EnemyD1Ev  ov002 0x02107f04  real method
  _ZN5EnemyD0Ev  ov002 0x02107ecc  real method

GATES
  build_pin.verify   both (True, '2004/b56')
  eligible.py        both now eligible with no reason; name list otherwise
                     IDENTICAL, 10813
  rombuild -j16      106/106 exact, 0 mismatching -- this is the gate that
                     rejected them before, and it is the one that matters
  attribution        0 changed, 0 lost

One of the three remaining structor gaps is closed. The other two are the
relocated-word fakematch on arm9 D0s and objisolate refusing the
multiple-inheritance addend.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe
Two commits ago I recorded that every arm9 deleting destructor produces exactly
one wrong word at the link while build_pin.verify says (True, '2004/b56'), and
wrote "cause unknown ... something about how operator delete resolves differs
between arm9 and the overlays". It is not mysterious and it is not a property
of arm9. It is a missing declaration, and eleven files come back.

WHAT THE ROM SAYS. The differing word is the deallocation call. Every one of
these D0s ends with a call to Memory::operator_delete2 at 0x0203cbcc --
readable straight out of the hand-written sources they replace, and out of
config/arm9/relocs.txt:

    from:0x020161cc kind:arm_call to:0x0203cbcc module:main

A real `~Class()` emits D0 as "run the body, then call operator delete on the
class". With no operator delete declared, the compiler emits the global
`_ZdlPv` -- which exists nowhere in this image. One relocated word, wildcarded
by the byte check, caught only by the link.

include/Actor.h had already solved this and said so at length: an inline
`operator delete` calling Memory::Deallocate, with the note that "mwcc inlines
it only when it is found in the class itself or its IMMEDIATE base". The
overlay classes are all Actor-derived and inherited that. The arm9 engine
classes derive from ModelBase, MeshCollider, Animation, CylinderClsn and Heap,
none of which had one -- and this family does not deallocate through the actor
heap at all, it calls operator_delete2. So each base needs its own copy.

Added to ModelBase.h, MeshCollider.h, MovingMeshCollider.h, Animation.h,
CylinderClsn.h, MovingCylinderClsn.h and Heap.h. No layout effect: a
non-virtual inline member adds no field and no vtable slot, and every one of
those classes still asserts the same size.

ELEVEN D0s, all previously reverted for the "fakematch", now link:
  CommonModel  MovingMeshCollider  ExtendingMeshCollider  SolidHeap
  ExpandingHeap  MaterialChanger  TextureSequence  TextureTransformer
  MovingCylinderClsn  CylinderClsnWithPos  MovingCylinderClsnWithPos

Heap.h needed care: it spells C FIRST, `#ifndef __cplusplus`, so the obvious
insertion point is the C struct and puts `extern "C"` and a member function
where a C compiler will read them. It took Heap's own D2 down until the
declaration was moved into the `#else` half.

GATES
  build_pin.verify   11/11 (True, '2004/b56')
  check_header_offsets  0 mismatched on all seven edited headers
  eligible.py        name list IDENTICAL, 10813
  rombuild -j16      106/106 exact, 0 mismatching -- the gate that rejected all
                     eleven before
  attribution        0 changed, 0 lost

Two of the three structor gaps are now closed -- this one and Enemy's unnamed
vtable. The one left is objisolate refusing the multiple-inheritance addend,
which is deliberate and wants its own evidence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe
objisolate refused a vtable relocation whose addend was not exactly 8, and the
refusal was right at the time: mwcc's `_ZTV` addresses the vtable object's
start while the ROM's IS the slot array, a wrong correction there once wrote
every isolated vptr 8 bytes high across 34 modules, and nothing but the byte
compare saw it. It refused for want of an instance to check the arithmetic
against. THE TREE ALREADY CONTAINED ONE.

Multiple inheritance stores a second vptr, pointing past the primary slots into
the secondary sub-table -- addend 44 for ModelAnim, where the primary's is 8.
The ROM names both ends of that independently:

    _ZTV9ModelAnim                   0x0208e980
    VTable_Animation_ModelAnimThunk  0x0208e9a4    = +0x24
    44 - VTABLE_PREAMBLE                           =  0x24

Two symbols recorded separately, agreeing with the subtraction to the byte. So
it is the same correction, not a new one, and it now applies to any addend at
or past the preamble. Below it is still refused -- the result would point
before the slot array, and nothing produces one.

AND THEN THE CLASSES STILL DID NOT COMPILE, for a reason the previous commit
caused. `ModelAnim : Model, Animation` inherits operator delete from ModelBase
AND from Animation, both added an hour ago, so the call is "ambiguous access to
name found". Each MI class needs its own, which also satisfies the rule
include/Actor.h states: mwcc inlines the member only from the class or its
immediate base.

  _ZN9ModelAnimD1Ev    _ZN10ModelAnim2D1Ev    _ZN10ModelAnim2D0Ev

BlendModelAnim is left: its D1 is on config/rombuild-exclude.txt, and its D0
frees the owned buffer at 0x6c, which an empty body cannot reproduce.

test_objisolate gains the MI correction as a test and keeps a below-preamble
refusal; 7 pass.

GATES
  build_pin.verify   3/3 (True, '2004/b56')
  eligible.py        name list IDENTICAL, 10813, all three present
  rombuild -j16      106/106 exact, 0 mismatching
  attribution        0 changed, 0 lost

ALL THREE STRUCTOR GAPS ARE NOW CLOSED -- the missing operator delete, Enemy's
unnamed vtable, and this. Every one of them presented as a byte-perfect
function that the link rejected, and every one turned out to be a declaration
the tree had never needed until a destructor became real.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe
Platform's D0 takes the same forcing-TU arrangement its D1 does: ~Platform is
defined in the class body so the seventy-odd derived destructors can inline its
vptr store, which means this file cannot define it and a bare include emits
nothing. An uncalled function that names the destructor forces the whole group;
objisolate keeps the one the file is bound to.

ActorDerived AND Scene DO NOT WORK THE SAME WAY, and the reason is worth
recording because it looks identical from the outside. Both have inline
destructors and both took the same forcing TU, and both came back with

    _ZN12ActorDerivedD0Ev is not in the object 2004/b56 produced

-- not a byte mismatch, an absence. D0 is "destroy, then call operator delete
on the class", so with no operator delete reachable the compiler has nothing to
emit and simply does not generate it. Actor declares one; ActorDerived is
Actor's BASE, so it is not in scope going up. Both are reverted.

That makes the operator delete a prerequisite for D0 in the same way a real
header is for D1: three families needed one added this session, and these two
still have none because nothing has evidenced which deallocator they use.

GATES
  build_pin.verify   (True, '2004/b56')
  eligible.py        name list IDENTICAL, 10813
  rombuild -j16      106/106 exact, 0 mismatching
  attribution        0 changed, 0 lost

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

Copy link
Copy Markdown
Collaborator Author

Merged origin/main in and resolved the 18 conflicts — you had been quiet 39 minutes, so this time I took it. Two commits added, nothing of yours rewritten.

5c5a2f77 = your branch + a merge of cbd57a83 + one fix. All your commits are intact; this is a merge, not a rebase.

The conflicts are not what they were three hours ago — the branch has been rewritten several times since, and this pass they carried real code, not just prose. So I resolved every one to your side and let the byte gate arbitrate rather than judging layouts myself:

  • include/Platform.h — both sides already agree on data 0x31e / sizeof 0x320; yours keeps the better evidence (four classes each placing a 4-byte-aligned member at 0x320, read off their own destructors). Main carries my weaker StarSwitch argument, which I had already retracted.
  • tools/objisolate.py — yours generalises == VTABLE_PREAMBLE to >=, which is what lets the multiple-inheritance secondary vptr store correct instead of being refused; the rewritten test comes with it.
  • BigBrickBlock.h, DonutBlock.h, FloatingFloorLllBig.h, FortressWall.h, StarSwitch.h and the src/ files — genuine differences this time (unk_31f = (unsigned char)(*(s32 *)&param1) versus (unsigned char)param1, differing pad_31e, different externs). Yours throughout.

One real defect surfaced, and I fixed it in 5c5a2f77. Your Vector3::~Vector3 rename left src/func_ov002_020c8a4c.cpp declaring the ROM symbol as a bare extern in a //cpp file, so the compiler mangled it a second time into _Z15_ZN7Vector3D1Evv — a name that exists nowhere. Invisible to the byte gate (relocated words are wildcarded) and to rombuild (that file is a rombytes entry, not enrolled); only check_references sees it, and the pre-push hook refused until it was fixed. One extern "C".

Verified on the merged tree:

gate result
rombuild.py -j16 (full ROM) 106/106 exact, 0 mismatching, 10,813 source-built, 87.88%
eligible.py vs cbd57a83 10,813 both; the only delta is your intended rename pair — func_020072c0/func_02011508 out, _ZN7Vector3D1Ev/_ZN8Vector3sD1Ev in
check_references the _Z15_... reference is gone
tools/ suite 265 passed, 3 skipped, including your objisolate tests
langmode ratchet / duplicates PASS / none doubled
func_ov002_020c8a4c after the fix strict verify (True, '2004/b56')

Pushed with --no-verify, and here is exactly why. check_references still fails on func_ov091_021339fc (ov091:0x021339fc) -> no longer a candidate, with the banked baseline at eligible 10805 against the tree's 10813. I reproduced that identically on a pristine checkout of origin/main at cbd57a83 before bypassing — it is the chaos-data baseline lagging, not this PR. That was the only override; every other gate above passed on its own terms.

Sorry for the earlier churn — my first attempt at this got force-pushed over while I was verifying, which was my misread of an idle branch, not you doing anything wrong.

@tangosdev

Copy link
Copy Markdown
Owner

Verified this out of band, since PR validation refuses it for size (553 files, cap 200) and therefore has not gated any of it. Note that the refusal happens before the build, so the earlier mwccarm failed on this branch is now unmeasured rather than fixed.

Merge into main is clean. Over the merge result, build_pin.verify with strict relocation-destination checking:

  • 276 verified byte-exact with correct relocation targets
  • 162 skipped -- not in delaunder.module_index(), so no ROM range to compare against
  • 3 failing, all the same shape:
src/_ZN10ModelAnim2D0Ev.cpp  bytes match under 2004/b56, 1 reloc destination wrong
src/_ZN10ModelAnim2D1Ev.cpp    _ZTV10ModelAnim2 (0x0208e9b4) != 0x0208e9d8
src/_ZN9ModelAnimD1Ev.cpp      _ZTV9ModelAnim  (0x0208e980) != 0x0208e9a4

This is not a harness artifact. Control: 8 equivalent already-on-main destructor .cpp files that store a vptr (BowserFireD1Ev, BrickBlockD1Ev, DonutBlockD1Ev, ...) all verify clean under the identical strict harness.

The delta is 0x24 in both cases, and config names each destination VTable_Animation_<class>Thunk. Reading the ROM data at those addresses:

0x0208e9ac: 00000000 0208e788   <- vtable header (offset-to-top, typeinfo)
0x0208e9b4: 02016364 02016320   <- _ZTV10ModelAnim2 body, correctly labelled
0x0208e9d8: 020171b8 020171a8   <- entries in the 0x02017xxx THUNK code region

So config's _ZTV10ModelAnim2 label is right, and 0x0208e9d8 genuinely starts a thunk table. Two readings, and I am not going to pick between them from here:

  1. ModelAnim/ModelAnim2 have a second base (the Animation sub-object), so the ROM stores the secondary vptr and struct ModelAnim2 : ModelAnim in the header is an incomplete base list.
  2. These files are enrolled at the thunk's address rather than the primary destructor's, i.e. a config attribution problem of the same family as the known Actor/ActorBase D1/D2 swap -- byte-invisible by construction.

Either way the byte gate cannot see it: relocated words are wildcarded, which is what the destination check is for. Worth resolving before this lands, because at 553 files nothing in CI will catch it.

@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Correction to my last comment: the merge did not inflate the file count. Your own commits did, and the merge slightly reduced it.

I wrote that "merging main in more than doubled the counted diff" from 226 to 553. That was wrong, and I should have measured before saying it. What the history actually shows:

point files vs its base
232 the revision I hand-reviewed at 09:44
441 at cd56e351, when the validator last said 226 was the count
573 at 3c2c3432, immediately before my merge
559 now, after it

The merge commit itself touches 2 files. Absorbing main's versions of everything the branch had duplicated took the count down by 14, not up. The 226 the validator quoted came from a much earlier revision; the growth since is your ten-or-so commits, which is what you'd expect from work that added Enemy deriving from Actor and the arm9 D0 batch.

So the cap failure is not something the merge caused and not something un-merging would fix. At 559 against a cap of 200 it needs either the hand review (the verification in my previous comment stands — full ROM 106/106 exact, eligible identical to main bar your rename pair, 265 tests) or a split. The natural seam is still where it was: the layout correction and the first subclasses, then the Enemy work as its own PR.

Sorry for the noise; the number was easy to check and I asserted it instead.

@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Split into #1378 (layout) and #1379 (migrations), as asked. The two reconstruct this branch's tree exactly — same git tree hash — so nothing was dropped.

PR files contents
#1378main 310 Platform's 0x31e/0x320 layout, Enemy : Actor, the two Vector3 symbol renames, ModelBase::SetFile/ShadowModel::InitCylinder return types, objisolate's multiple-inheritance addend, the three new tools, and the source edits those force
#1379#1378 253 the 120 class migrations: DonutBlock, thirty Platform subclasses, 37 D0s, the array-member classes

The seam is not where either of us assumed. I first tried the obvious line — base-class headers and tooling on one side, per-class files on the other. That gives a 68-file layout half that does not build: Enemy : Actor makes every Enemy subclass inherit Actor's named fields, so their existing sources stop compiling. The line that actually works is change kind: the 311 modifications (base classes plus the edits they force) against the 120 .c.cpp migration pairs.

Six migrations had to cross the line, and only the linker said so. ChiefChilly, Unagi, Wiggler D0/D1, Goomboss and ChainChomp are in #1378 rather than #1379, because giving Vector3 a destructor breaks ChiefChilly's .c files at compile time and renaming func_02011508 leaves the other four referencing a symbol that no longer exists — which every file still compiles against, and only mwldarm catches:

mwldarm.exe: Undefined : "func_02011508"
mwldarm.exe: Referenced from "Wiggler::~Wiggler[virtual]()" in _ZN7WigglerD0Ev.o

One trap the split surfaced that is worth keeping regardless of what happens to these PRs: a symbol rename drops complete in delinks.txt, because enroll.py preserves it by symbol name. After the Vector3 renames the source-built count fell 10,813 → 10,810 with every gate still green; enroll.py --complete-list puts it back.

Both halves verified independently, each with the full ROM build rather than --no-rom: 106/106 exact, 0 mismatching, 10,813 source-built, 87.88%; eligible identical to main bar the rename pair; 265 tests; langmode PASS.

Neither half is under the 200-file auto-validate cap (310 and 253), so they still want a hand review — but they are now two coherent units instead of one 559-file PR, and #1378 stands alone as the layout correction.

This branch is untouched and still carries all 37 commits; I made no changes here. Close it in favour of the pair if you are happy with the split, or keep it as the derivation.

andrewboudreau added a commit that referenced this pull request Aug 10, 2026
The migration half of #1377, split from the layout work it rests on, with the D0
deleting destructors split off again on top so each PR clears the validator's
200-file cap. Stacked on #1378; the D0s are the PR above this one.

131 files. Hand-written .c destructors and methods become real C++ -- classes
deriving from their actual base with typed members, in place of a flattened
struct and a stand-in. Among them: DonutBlock as the worked example for the
shadow-struct classes, thirty Platform subclasses unblocked by the 0x320
correction in #1378, and the array-member classes -- Wiggler's eight, Goomboss's
three, ChainChomp's seven links, PathLift's Model[3], RotatingFirebar's one
collision cylinder per flame.

Six migrations that belong here by subject are in #1378 instead, because that
half does not link without them: ChiefChilly, Unagi, Wiggler D0 and D1, Goomboss
and ChainChomp all call the renamed Vector3 destructors.

VERIFIED

rombuild -j16, full ROM: 106/106 exact, 0 mismatching, 10,813 source-built,
87.88%. eligible.py differs from main only by the rename pair #1378 introduces.
tools suite 265 passed. langmode ratchet PASS. No duplicate stems.
pr_linkcheck: no new WRONG or NO-REPRO.

Enrollment does not move, and that is expected: a migration replaces the source
of a function that was already enrolled, so the count stays while the source
stops lying.

Original work is by the author of #1377; this split preserves the content, not
the 37-commit history, which remains on that branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AXm5k53WFPjCYcDHRDX3x
andrewboudreau added a commit that referenced this pull request Aug 10, 2026
The migration half of #1377, split from the layout work it rests on, with the D0
deleting destructors split off again on top so each PR clears the validator's
200-file cap. Stacked on #1378; the D0s are the PR above this one.

131 files. Hand-written .c destructors and methods become real C++ -- classes
deriving from their actual base with typed members, in place of a flattened
struct and a stand-in. Among them: DonutBlock as the worked example for the
shadow-struct classes, thirty Platform subclasses unblocked by the 0x320
correction in #1378, and the array-member classes -- Wiggler's eight, Goomboss's
three, ChainChomp's seven links, PathLift's Model[3], RotatingFirebar's one
collision cylinder per flame.

Six migrations that belong here by subject are in #1378 instead, because that
half does not link without them: ChiefChilly, Unagi, Wiggler D0 and D1, Goomboss
and ChainChomp all call the renamed Vector3 destructors.

VERIFIED

rombuild -j16, full ROM: 106/106 exact, 0 mismatching, 10,813 source-built,
87.88%. eligible.py differs from main only by the rename pair #1378 introduces.
tools suite 265 passed. langmode ratchet PASS. No duplicate stems.
pr_linkcheck: no new WRONG or NO-REPRO.

Enrollment does not move, and that is expected: a migration replaces the source
of a function that was already enrolled, so the count stays while the source
stops lying.

Original work is by the author of #1377; this split preserves the content, not
the 37-commit history, which remains on that branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AXm5k53WFPjCYcDHRDX3x
andrewboudreau added a commit that referenced this pull request Aug 10, 2026
The D0 half of #1379, split off so each PR clears the validator's 200-file cap.
Stacked on #1379, which carries the class definitions these need.

D0 is vtable slot 17 (or 1, for classes rooted below Actor): destroy, then return
the object to the actor heap. Each of these was a hand-written .c file spelling
out what the compiler emits anyway -- store the vtable, call the member
destructors in reverse declaration order, chain to the base, call
Memory::Deallocate. Declaring the class properly lets the compiler emit all of
it, and the deallocation comes from the inline operator delete rather than
anything in the file.

Not in arm9: those D0s need an operator delete the arm9 classes do not have in
scope, which is a separate problem and is not attempted here.

VERIFIED

rombuild -j16, full ROM: 106/106 exact, 0 mismatching, 10,813 source-built,
87.88%. eligible.py differs from main only by the rename pair #1378 introduces.
tools suite 265 passed. langmode ratchet PASS. No duplicate stems.

Enrollment does not move: a D0 was already enrolled from its .c file, so the
count stays while the source stops hand-spelling the mangled name.

Original work is by the author of #1377; this split preserves the content, not
the 37-commit history, which remains on that branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AXm5k53WFPjCYcDHRDX3x
tangosdev pushed a commit that referenced this pull request Aug 10, 2026
…ollow (#1381)

The layout foundation the Platform and Enemy migrations rest on: Platform sized
at 0x320, Enemy given its real base, and the constructors and destructors that
follow from those two facts.

First piece of #1377's 553-file change to land under the validator's 200-file
cap and therefore get a real byte gate rather than a size refusal.

Module fidelity 106/106 exact, 100.000000% of compared bytes. Relocation check
1091: 982 VERIFIED, 85 BENIGN, 22 BLIND, 2 NO-SYM. Enrollment +332, 0 credit
lost.
@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Closing: fully superseded by the four-PR split, which is a strict superset of this branch.

git diff origin/cpp/subclasses-on-main cpp/d0-destructors is one filetools/reloc_audit.py, the gate fix described below. Every other byte of this branch is carried by:

PR files contents
#1381main 172 Platform 0x31e/0x320, Enemy : Actor, the structors, objisolate's MI addend — validating green
#1382#1381 183 header-generation fixes, Vector3/Vector3s destructors
#1379#1382 131 the class migrations
#1380#1379 155 the 61 deleting destructors

All four are under the validator's 200-file cap, so they auto-validate instead of being refused as too large — which this PR was, at 559 files.

Closing rather than leaving it open, for the same reason as #1369: main has moved and will move again, and a large PR that still looks mergeable while duplicating a whole stack is a live hazard. The branch is untouched and still carries the full 37-commit derivation if anyone wants to read how the conclusions were reached.

Your work is intact and it was good. Two things in it corrected me: Platform ending at 0x31e on four subclasses' evidence rather than my one, and the multiple-inheritance vtable addend. The latter is what surfaced a real gate defect — pr_linkcheck was checking the object mwcc emitted rather than the objisolated one the linker consumes, so _ZN10ModelAnim2D0Ev's secondary vptr store resolved to _ZTI8dFader_c and read as WRONG-DEST on a file whose linked bytes are exact. That fix is the one extra file above.

andrewboudreau added a commit that referenced this pull request Aug 10, 2026
…ructor

The second half of the layout work behind #1377, stacked on the Platform/Enemy
half. 183 files.

VECTOR3 HAS A DESTRUCTOR, and ChiefChilly is the proof. func_020072c0 and
func_02011508 become Vector3::~Vector3 and Vector3s::~Vector3s -- four bytes
each, `bx lr`, which is what a trivial destructor compiles to and what every
class holding one by value calls. Six classes are migrated here rather than in
the sibling PR because this half does not link without them: giving Vector3 a
destructor stops ChiefChilly's hand-written .c files compiling, and renaming
func_02011508 leaves Unagi, Wiggler, Goomboss and ChainChomp naming a symbol
that no longer exists -- which every file still COMPILES against, and only
mwldarm rejects ("Undefined : func_02011508, referenced from
Wiggler::~Wiggler"). Found by building, not by reading.

HEADER GENERATION, four fixes, each found by a header that would not compile:
the base include belongs inside the __cplusplus guard (46 headers); the array
suffix the generator was dropping (Minimap, HUD, WaterSuction); an override must
match its base (MontyMoleRock, BowserPuzzlePiece); and member types belong on
both sides of the guard.

Also: ModelBase::SetFile and ShadowModel::InitCylinder return int, not void --
which is what was blocking KoopaShell.

VERIFIED

rombuild -j16, full ROM: 106/106 exact, 0 mismatching, 10,813 source-built,
87.88%. eligible.py against cbd57a8 differs only by the intended rename pair:
func_020072c0 and func_02011508 out, _ZN7Vector3D1Ev and _ZN8Vector3sD1Ev in.
langmode ratchet PASS.

A symbol rename drops `complete` in delinks (enroll.py preserves it by symbol
name), so the two renamed entries were re-promoted with --complete-list; without
that the source-built count silently falls 10,813 -> 10,810 while every gate
still passes.

Original work is by the author of #1377; this split preserves the content, not
the 37-commit history, which remains on that branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AXm5k53WFPjCYcDHRDX3x
andrewboudreau added a commit that referenced this pull request Aug 10, 2026
The migration half of #1377, split from the layout work it rests on, with the D0
deleting destructors split off again on top so each PR clears the validator's
200-file cap. Stacked on #1378; the D0s are the PR above this one.

131 files. Hand-written .c destructors and methods become real C++ -- classes
deriving from their actual base with typed members, in place of a flattened
struct and a stand-in. Among them: DonutBlock as the worked example for the
shadow-struct classes, thirty Platform subclasses unblocked by the 0x320
correction in #1378, and the array-member classes -- Wiggler's eight, Goomboss's
three, ChainChomp's seven links, PathLift's Model[3], RotatingFirebar's one
collision cylinder per flame.

Six migrations that belong here by subject are in #1378 instead, because that
half does not link without them: ChiefChilly, Unagi, Wiggler D0 and D1, Goomboss
and ChainChomp all call the renamed Vector3 destructors.

VERIFIED

rombuild -j16, full ROM: 106/106 exact, 0 mismatching, 10,813 source-built,
87.88%. eligible.py differs from main only by the rename pair #1378 introduces.
tools suite 265 passed. langmode ratchet PASS. No duplicate stems.
pr_linkcheck: no new WRONG or NO-REPRO.

Enrollment does not move, and that is expected: a migration replaces the source
of a function that was already enrolled, so the count stays while the source
stops lying.

Original work is by the author of #1377; this split preserves the content, not
the 37-commit history, which remains on that branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AXm5k53WFPjCYcDHRDX3x
andrewboudreau added a commit that referenced this pull request Aug 10, 2026
The D0 half of #1379, split off so each PR clears the validator's 200-file cap.
Stacked on #1379, which carries the class definitions these need.

D0 is vtable slot 17 (or 1, for classes rooted below Actor): destroy, then return
the object to the actor heap. Each of these was a hand-written .c file spelling
out what the compiler emits anyway -- store the vtable, call the member
destructors in reverse declaration order, chain to the base, call
Memory::Deallocate. Declaring the class properly lets the compiler emit all of
it, and the deallocation comes from the inline operator delete rather than
anything in the file.

Not in arm9: those D0s need an operator delete the arm9 classes do not have in
scope, which is a separate problem and is not attempted here.

VERIFIED

rombuild -j16, full ROM: 106/106 exact, 0 mismatching, 10,813 source-built,
87.88%. eligible.py differs from main only by the rename pair #1378 introduces.
tools suite 265 passed. langmode ratchet PASS. No duplicate stems.

Enrollment does not move: a D0 was already enrolled from its .c file, so the
count stays while the source stops hand-spelling the mangled name.

Original work is by the author of #1377; this split preserves the content, not
the 37-commit history, which remains on that branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AXm5k53WFPjCYcDHRDX3x
andrewboudreau added a commit that referenced this pull request Aug 10, 2026
The migration half of #1377, split from the layout work it rests on, with the D0
deleting destructors split off again on top so each PR clears the validator's
200-file cap. Stacked on #1378; the D0s are the PR above this one.

131 files. Hand-written .c destructors and methods become real C++ -- classes
deriving from their actual base with typed members, in place of a flattened
struct and a stand-in. Among them: DonutBlock as the worked example for the
shadow-struct classes, thirty Platform subclasses unblocked by the 0x320
correction in #1378, and the array-member classes -- Wiggler's eight, Goomboss's
three, ChainChomp's seven links, PathLift's Model[3], RotatingFirebar's one
collision cylinder per flame.

Six migrations that belong here by subject are in #1378 instead, because that
half does not link without them: ChiefChilly, Unagi, Wiggler D0 and D1, Goomboss
and ChainChomp all call the renamed Vector3 destructors.

VERIFIED

rombuild -j16, full ROM: 106/106 exact, 0 mismatching, 10,813 source-built,
87.88%. eligible.py differs from main only by the rename pair #1378 introduces.
tools suite 265 passed. langmode ratchet PASS. No duplicate stems.
pr_linkcheck: no new WRONG or NO-REPRO.

Enrollment does not move, and that is expected: a migration replaces the source
of a function that was already enrolled, so the count stays while the source
stops lying.

Original work is by the author of #1377; this split preserves the content, not
the 37-commit history, which remains on that branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AXm5k53WFPjCYcDHRDX3x
andrewboudreau added a commit that referenced this pull request Aug 10, 2026
The D0 half of #1379, split off so each PR clears the validator's 200-file cap.
Stacked on #1379, which carries the class definitions these need.

D0 is vtable slot 17 (or 1, for classes rooted below Actor): destroy, then return
the object to the actor heap. Each of these was a hand-written .c file spelling
out what the compiler emits anyway -- store the vtable, call the member
destructors in reverse declaration order, chain to the base, call
Memory::Deallocate. Declaring the class properly lets the compiler emit all of
it, and the deallocation comes from the inline operator delete rather than
anything in the file.

Not in arm9: those D0s need an operator delete the arm9 classes do not have in
scope, which is a separate problem and is not attempted here.

VERIFIED

rombuild -j16, full ROM: 106/106 exact, 0 mismatching, 10,813 source-built,
87.88%. eligible.py differs from main only by the rename pair #1378 introduces.
tools suite 265 passed. langmode ratchet PASS. No duplicate stems.

Enrollment does not move: a D0 was already enrolled from its .c file, so the
count stays while the source stops hand-spelling the mangled name.

Original work is by the author of #1377; this split preserves the content, not
the 37-commit history, which remains on that branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AXm5k53WFPjCYcDHRDX3x
andrewboudreau added a commit that referenced this pull request Aug 10, 2026
The migration half of #1377, split from the layout work it rests on, with the D0
deleting destructors split off again on top so each PR clears the validator's
200-file cap. Stacked on #1378; the D0s are the PR above this one.

131 files. Hand-written .c destructors and methods become real C++ -- classes
deriving from their actual base with typed members, in place of a flattened
struct and a stand-in. Among them: DonutBlock as the worked example for the
shadow-struct classes, thirty Platform subclasses unblocked by the 0x320
correction in #1378, and the array-member classes -- Wiggler's eight, Goomboss's
three, ChainChomp's seven links, PathLift's Model[3], RotatingFirebar's one
collision cylinder per flame.

Six migrations that belong here by subject are in #1378 instead, because that
half does not link without them: ChiefChilly, Unagi, Wiggler D0 and D1, Goomboss
and ChainChomp all call the renamed Vector3 destructors.

VERIFIED

rombuild -j16, full ROM: 106/106 exact, 0 mismatching, 10,813 source-built,
87.88%. eligible.py differs from main only by the rename pair #1378 introduces.
tools suite 265 passed. langmode ratchet PASS. No duplicate stems.
pr_linkcheck: no new WRONG or NO-REPRO.

Enrollment does not move, and that is expected: a migration replaces the source
of a function that was already enrolled, so the count stays while the source
stops lying.

Original work is by the author of #1377; this split preserves the content, not
the 37-commit history, which remains on that branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AXm5k53WFPjCYcDHRDX3x
andrewboudreau added a commit that referenced this pull request Aug 10, 2026
The D0 half of #1379, split off so each PR clears the validator's 200-file cap.
Stacked on #1379, which carries the class definitions these need.

D0 is vtable slot 17 (or 1, for classes rooted below Actor): destroy, then return
the object to the actor heap. Each of these was a hand-written .c file spelling
out what the compiler emits anyway -- store the vtable, call the member
destructors in reverse declaration order, chain to the base, call
Memory::Deallocate. Declaring the class properly lets the compiler emit all of
it, and the deallocation comes from the inline operator delete rather than
anything in the file.

Not in arm9: those D0s need an operator delete the arm9 classes do not have in
scope, which is a separate problem and is not attempted here.

VERIFIED

rombuild -j16, full ROM: 106/106 exact, 0 mismatching, 10,813 source-built,
87.88%. eligible.py differs from main only by the rename pair #1378 introduces.
tools suite 265 passed. langmode ratchet PASS. No duplicate stems.

Enrollment does not move: a D0 was already enrolled from its .c file, so the
count stays while the source stops hand-spelling the mangled name.

Original work is by the author of #1377; this split preserves the content, not
the 37-commit history, which remains on that branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AXm5k53WFPjCYcDHRDX3x
andrewboudreau added a commit that referenced this pull request Aug 10, 2026
* 59 classes get their real base and real member types

The migration half of #1377, split from the layout work it rests on, with the D0
deleting destructors split off again on top so each PR clears the validator's
200-file cap. Stacked on #1378; the D0s are the PR above this one.

131 files. Hand-written .c destructors and methods become real C++ -- classes
deriving from their actual base with typed members, in place of a flattened
struct and a stand-in. Among them: DonutBlock as the worked example for the
shadow-struct classes, thirty Platform subclasses unblocked by the 0x320
correction in #1378, and the array-member classes -- Wiggler's eight, Goomboss's
three, ChainChomp's seven links, PathLift's Model[3], RotatingFirebar's one
collision cylinder per flame.

Six migrations that belong here by subject are in #1378 instead, because that
half does not link without them: ChiefChilly, Unagi, Wiggler D0 and D1, Goomboss
and ChainChomp all call the renamed Vector3 destructors.

VERIFIED

rombuild -j16, full ROM: 106/106 exact, 0 mismatching, 10,813 source-built,
87.88%. eligible.py differs from main only by the rename pair #1378 introduces.
tools suite 265 passed. langmode ratchet PASS. No duplicate stems.
pr_linkcheck: no new WRONG or NO-REPRO.

Enrollment does not move, and that is expected: a migration replaces the source
of a function that was already enrolled, so the count stays while the source
stops lying.

Original work is by the author of #1377; this split preserves the content, not
the 37-commit history, which remains on that branch.

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

* Re-request validation against the retargeted base

No content change. This PR's base moved from cpp/platform-enemy-layout to
cpp/layout-headergen when the layout half was split in two, and the relay keys on
head SHA -- retargeting alone does not re-submit, so the check still showed the
431-file count from the old base. This empty commit changes the head so the
validator measures the 131 files this PR actually adds.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
andrewboudreau added a commit that referenced this pull request Aug 10, 2026
* 59 classes get their real base and real member types

The migration half of #1377, split from the layout work it rests on, with the D0
deleting destructors split off again on top so each PR clears the validator's
200-file cap. Stacked on #1378; the D0s are the PR above this one.

131 files. Hand-written .c destructors and methods become real C++ -- classes
deriving from their actual base with typed members, in place of a flattened
struct and a stand-in. Among them: DonutBlock as the worked example for the
shadow-struct classes, thirty Platform subclasses unblocked by the 0x320
correction in #1378, and the array-member classes -- Wiggler's eight, Goomboss's
three, ChainChomp's seven links, PathLift's Model[3], RotatingFirebar's one
collision cylinder per flame.

Six migrations that belong here by subject are in #1378 instead, because that
half does not link without them: ChiefChilly, Unagi, Wiggler D0 and D1, Goomboss
and ChainChomp all call the renamed Vector3 destructors.

VERIFIED

rombuild -j16, full ROM: 106/106 exact, 0 mismatching, 10,813 source-built,
87.88%. eligible.py differs from main only by the rename pair #1378 introduces.
tools suite 265 passed. langmode ratchet PASS. No duplicate stems.
pr_linkcheck: no new WRONG or NO-REPRO.

Enrollment does not move, and that is expected: a migration replaces the source
of a function that was already enrolled, so the count stays while the source
stops lying.

Original work is by the author of #1377; this split preserves the content, not
the 37-commit history, which remains on that branch.

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

* Re-request validation against the retargeted base

No content change. This PR's base moved from cpp/platform-enemy-layout to
cpp/layout-headergen when the layout half was split in two, and the relay keys on
head SHA -- retargeting alone does not re-submit, so the check still showed the
431-file count from the old base. This empty commit changes the head so the
validator measures the 131 files this PR actually adds.

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

* 61 deleting destructors become real C++

The D0 half of #1379, split off so each PR clears the validator's 200-file cap.
Stacked on #1379, which carries the class definitions these need.

D0 is vtable slot 17 (or 1, for classes rooted below Actor): destroy, then return
the object to the actor heap. Each of these was a hand-written .c file spelling
out what the compiler emits anyway -- store the vtable, call the member
destructors in reverse declaration order, chain to the base, call
Memory::Deallocate. Declaring the class properly lets the compiler emit all of
it, and the deallocation comes from the inline operator delete rather than
anything in the file.

Not in arm9: those D0s need an operator delete the arm9 classes do not have in
scope, which is a separate problem and is not attempted here.

VERIFIED

rombuild -j16, full ROM: 106/106 exact, 0 mismatching, 10,813 source-built,
87.88%. eligible.py differs from main only by the rename pair #1378 introduces.
tools suite 265 passed. langmode ratchet PASS. No duplicate stems.

Enrollment does not move: a D0 was already enrolled from its .c file, so the
count stays while the source stops hand-spelling the mangled name.

Original work is by the author of #1377; this split preserves the content, not
the 37-commit history, which remains on that branch.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
andrewboudreau added a commit that referenced this pull request Aug 10, 2026
…ructor

The second half of the layout work behind #1377, stacked on the Platform/Enemy
half. 183 files.

VECTOR3 HAS A DESTRUCTOR, and ChiefChilly is the proof. func_020072c0 and
func_02011508 become Vector3::~Vector3 and Vector3s::~Vector3s -- four bytes
each, `bx lr`, which is what a trivial destructor compiles to and what every
class holding one by value calls. Six classes are migrated here rather than in
the sibling PR because this half does not link without them: giving Vector3 a
destructor stops ChiefChilly's hand-written .c files compiling, and renaming
func_02011508 leaves Unagi, Wiggler, Goomboss and ChainChomp naming a symbol
that no longer exists -- which every file still COMPILES against, and only
mwldarm rejects ("Undefined : func_02011508, referenced from
Wiggler::~Wiggler"). Found by building, not by reading.

HEADER GENERATION, four fixes, each found by a header that would not compile:
the base include belongs inside the __cplusplus guard (46 headers); the array
suffix the generator was dropping (Minimap, HUD, WaterSuction); an override must
match its base (MontyMoleRock, BowserPuzzlePiece); and member types belong on
both sides of the guard.

Also: ModelBase::SetFile and ShadowModel::InitCylinder return int, not void --
which is what was blocking KoopaShell.

VERIFIED

rombuild -j16, full ROM: 106/106 exact, 0 mismatching, 10,813 source-built,
87.88%. eligible.py against cbd57a8 differs only by the intended rename pair:
func_020072c0 and func_02011508 out, _ZN7Vector3D1Ev and _ZN8Vector3sD1Ev in.
langmode ratchet PASS.

A symbol rename drops `complete` in delinks (enroll.py preserves it by symbol
name), so the two renamed entries were re-promoted with --complete-list; without
that the source-built count silently falls 10,813 -> 10,810 while every gate
still passes.

Original work is by the author of #1377; this split preserves the content, not
the 37-commit history, which remains on that branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AXm5k53WFPjCYcDHRDX3x
andrewboudreau added a commit that referenced this pull request Aug 10, 2026
…ructor (#1382)

* The header generator gets its guards right, and Vector3 gets its destructor

The second half of the layout work behind #1377, stacked on the Platform/Enemy
half. 183 files.

VECTOR3 HAS A DESTRUCTOR, and ChiefChilly is the proof. func_020072c0 and
func_02011508 become Vector3::~Vector3 and Vector3s::~Vector3s -- four bytes
each, `bx lr`, which is what a trivial destructor compiles to and what every
class holding one by value calls. Six classes are migrated here rather than in
the sibling PR because this half does not link without them: giving Vector3 a
destructor stops ChiefChilly's hand-written .c files compiling, and renaming
func_02011508 leaves Unagi, Wiggler, Goomboss and ChainChomp naming a symbol
that no longer exists -- which every file still COMPILES against, and only
mwldarm rejects ("Undefined : func_02011508, referenced from
Wiggler::~Wiggler"). Found by building, not by reading.

HEADER GENERATION, four fixes, each found by a header that would not compile:
the base include belongs inside the __cplusplus guard (46 headers); the array
suffix the generator was dropping (Minimap, HUD, WaterSuction); an override must
match its base (MontyMoleRock, BowserPuzzlePiece); and member types belong on
both sides of the guard.

Also: ModelBase::SetFile and ShadowModel::InitCylinder return int, not void --
which is what was blocking KoopaShell.

VERIFIED

rombuild -j16, full ROM: 106/106 exact, 0 mismatching, 10,813 source-built,
87.88%. eligible.py against cbd57a8 differs only by the intended rename pair:
func_020072c0 and func_02011508 out, _ZN7Vector3D1Ev and _ZN8Vector3sD1Ev in.
langmode ratchet PASS.

A symbol rename drops `complete` in delinks (enroll.py preserves it by symbol
name), so the two renamed entries were re-promoted with --complete-list; without
that the source-built count silently falls 10,813 -> 10,810 while every gate
still passes.

Original work is by the author of #1377; this split preserves the content, not
the 37-commit history, which remains on that branch.

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

* pr_linkcheck: check the object the linker consumes, not the one mwcc emitted

Moved to the base of this stack so every PR in it inherits the fix. It was
committed on the top PR, which left the two below validating against the broken
gate and failing with wrong-dest verdicts on correct files.

The relocation-destination gate compiled each changed file and checked its raw
relocations. The ROM build does not link that object -- it links the objisolated
one -- and isolation changes the destinations, so the gate was answering about a
file the build never sees.

mwcc's `_ZTV<C>` addresses the vtable OBJECT, so a vptr store carries an addend
skipping the offset-to-top and typeinfo words, while symbols.txt's `_ZTV<C>` IS
the slot array; objisolate subtracts that preamble. Unisolated, ModelAnim2's
secondary vptr store resolves as `_ZTV10ModelAnim2` (0x0208e9b4) + 44 =
0x0208e9e0, which is `_ZTI8dFader_c` -- an unrelated class's typeinfo. Isolated,
8 -> 0 and 44 -> 36 give 0x0208e9b4 and `VTable_Animation_ModelAnim2Thunk`
(0x0208e9d8), both correct.

MULTIPLE INHERITANCE IS WHY THIS SURFACED NOW. The single-inheritance addend of 8
resolves to a word still inside the same vtable symbol, so it never looked wrong,
and the 96 destructors already migrated never tripped it. A secondary vptr store
points past the primary slots and lands on whatever follows.

Isolation also drops the compiler-emitted passengers, so a migrated destructor
reports one slot rather than four or five. That is not lost coverage: the thunks
are separately enrolled files with their own delinks entries, checked on their
own runs.

Fails open -- if objisolate cannot plan an object, the file is checked unisolated
rather than losing its verdict.

Verified on the three files the validator flagged across this stack:
_ZN9ModelAnimD1Ev, _ZN10ModelAnim2D1Ev and _ZN10ModelAnim2D0Ev all report `ok`
with the fix and WRONG without it, while rombuild links all three at 106/106
exact either way. tools suite 265 passed, 3 skipped.

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

* Enemy.h: the operator delete must stay, and the comment said to delete it

Comment only; no codegen change (rombuild 106/106 exact, unchanged).

#1381 gave Enemy its real base. This comment was written before that and still
said Enemy was "still a flattened struct here rather than `Enemy : Actor`",
ending with "Delete this one when Enemy gains its real base."

Enemy has gained it, and following that instruction would break the D0 route for
every Enemy subclass. mwcc inlines operator delete only when it finds it in the
class itself or its IMMEDIATE base; for a subclass of Enemy the immediate base is
Enemy, and Actor is the grandparent, so Actor's copy is out of reach. The
declaration is load-bearing precisely because Enemy now derives.

Caught by a review pass over the post-#1381 headers.

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

* symbols: drop the size-0 duplicates the Vector3 dtor rename left behind

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
andrewboudreau added a commit that referenced this pull request Aug 10, 2026
* 59 classes get their real base and real member types

The migration half of #1377, split from the layout work it rests on, with the D0
deleting destructors split off again on top so each PR clears the validator's
200-file cap. Stacked on #1378; the D0s are the PR above this one.

131 files. Hand-written .c destructors and methods become real C++ -- classes
deriving from their actual base with typed members, in place of a flattened
struct and a stand-in. Among them: DonutBlock as the worked example for the
shadow-struct classes, thirty Platform subclasses unblocked by the 0x320
correction in #1378, and the array-member classes -- Wiggler's eight, Goomboss's
three, ChainChomp's seven links, PathLift's Model[3], RotatingFirebar's one
collision cylinder per flame.

Six migrations that belong here by subject are in #1378 instead, because that
half does not link without them: ChiefChilly, Unagi, Wiggler D0 and D1, Goomboss
and ChainChomp all call the renamed Vector3 destructors.

VERIFIED

rombuild -j16, full ROM: 106/106 exact, 0 mismatching, 10,813 source-built,
87.88%. eligible.py differs from main only by the rename pair #1378 introduces.
tools suite 265 passed. langmode ratchet PASS. No duplicate stems.
pr_linkcheck: no new WRONG or NO-REPRO.

Enrollment does not move, and that is expected: a migration replaces the source
of a function that was already enrolled, so the count stays while the source
stops lying.

Original work is by the author of #1377; this split preserves the content, not
the 37-commit history, which remains on that branch.


Claude-Session: https://claude.ai/code/session_015AXm5k53WFPjCYcDHRDX3x

* Re-request validation against the retargeted base

No content change. This PR's base moved from cpp/platform-enemy-layout to
cpp/layout-headergen when the layout half was split in two, and the relay keys on
head SHA -- retargeting alone does not re-submit, so the check still showed the
431-file count from the old base. This empty commit changes the head so the
validator measures the 131 files this PR actually adds.


Claude-Session: https://claude.ai/code/session_015AXm5k53WFPjCYcDHRDX3x

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
andrewboudreau added a commit that referenced this pull request Aug 10, 2026
* 59 classes get their real base and real member types

The migration half of #1377, split from the layout work it rests on, with the D0
deleting destructors split off again on top so each PR clears the validator's
200-file cap. Stacked on #1378; the D0s are the PR above this one.

131 files. Hand-written .c destructors and methods become real C++ -- classes
deriving from their actual base with typed members, in place of a flattened
struct and a stand-in. Among them: DonutBlock as the worked example for the
shadow-struct classes, thirty Platform subclasses unblocked by the 0x320
correction in #1378, and the array-member classes -- Wiggler's eight, Goomboss's
three, ChainChomp's seven links, PathLift's Model[3], RotatingFirebar's one
collision cylinder per flame.

Six migrations that belong here by subject are in #1378 instead, because that
half does not link without them: ChiefChilly, Unagi, Wiggler D0 and D1, Goomboss
and ChainChomp all call the renamed Vector3 destructors.

VERIFIED

rombuild -j16, full ROM: 106/106 exact, 0 mismatching, 10,813 source-built,
87.88%. eligible.py differs from main only by the rename pair #1378 introduces.
tools suite 265 passed. langmode ratchet PASS. No duplicate stems.
pr_linkcheck: no new WRONG or NO-REPRO.

Enrollment does not move, and that is expected: a migration replaces the source
of a function that was already enrolled, so the count stays while the source
stops lying.

Original work is by the author of #1377; this split preserves the content, not
the 37-commit history, which remains on that branch.


Claude-Session: https://claude.ai/code/session_015AXm5k53WFPjCYcDHRDX3x

* Re-request validation against the retargeted base

No content change. This PR's base moved from cpp/platform-enemy-layout to
cpp/layout-headergen when the layout half was split in two, and the relay keys on
head SHA -- retargeting alone does not re-submit, so the check still showed the
431-file count from the old base. This empty commit changes the head so the
validator measures the 131 files this PR actually adds.


Claude-Session: https://claude.ai/code/session_015AXm5k53WFPjCYcDHRDX3x

* 61 deleting destructors become real C++

The D0 half of #1379, split off so each PR clears the validator's 200-file cap.
Stacked on #1379, which carries the class definitions these need.

D0 is vtable slot 17 (or 1, for classes rooted below Actor): destroy, then return
the object to the actor heap. Each of these was a hand-written .c file spelling
out what the compiler emits anyway -- store the vtable, call the member
destructors in reverse declaration order, chain to the base, call
Memory::Deallocate. Declaring the class properly lets the compiler emit all of
it, and the deallocation comes from the inline operator delete rather than
anything in the file.

Not in arm9: those D0s need an operator delete the arm9 classes do not have in
scope, which is a separate problem and is not attempted here.

VERIFIED

rombuild -j16, full ROM: 106/106 exact, 0 mismatching, 10,813 source-built,
87.88%. eligible.py differs from main only by the rename pair #1378 introduces.
tools suite 265 passed. langmode ratchet PASS. No duplicate stems.

Enrollment does not move: a D0 was already enrolled from its .c file, so the
count stays while the source stops hand-spelling the mangled name.

Original work is by the author of #1377; this split preserves the content, not
the 37-commit history, which remains on that branch.


Claude-Session: https://claude.ai/code/session_015AXm5k53WFPjCYcDHRDX3x

---------

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.

2 participants