Skip to content

ov060: the Bowser fight becomes real C++, and D0 destructors become migratable - #1376

Closed
andrewboudreau wants to merge 1 commit into
mainfrom
cpp/ov060-bowser
Closed

ov060: the Bowser fight becomes real C++, and D0 destructors become migratable#1376
andrewboudreau wants to merge 1 commit into
mainfrom
cpp/ov060-bowser

Conversation

@andrewboudreau

Copy link
Copy Markdown
Collaborator

Recreated: this was #1374, stacked on #1373. #1373 merged, its branch was deleted, and GitHub closed #1374 rather than retargeting it. Same commit, rebased onto 2c05ba29.

Overlaps #1370 on Platform, and the two derivations agree. #1370 promotes Platform to a real class independently of this PR; its header and mine match field for field, including Matrix4x3 mClsnMat at 0x2ec and the inline ~Platform() {}. That is two independent reconstructions landing on the same layout, which is worth more than either alone. #1370's is the superset — it also migrates _ZN8PlatformD1Ev and declares the IsClsnInRange pair — so if #1370 lands first I will drop my Platform.h and the three Platform method files and rebase onto it. Only BowserFireSeaArena here needs Platform at all.


Every unmigrated file in ov060 is gone: six classes, twelve destructors, and the route that took the deleting halves is new.

The D0 route

258 of the tree's 261 D0 files were unmigrated against 74 migrated D1s, and the reason was one missing declaration. The compiler generates D0 as "run the destructor body, then call operator delete on the class"; with none declared it emits a call to the global _ZdlPv, which exists nowhere in this image, and D0 comes out three instructions short.

The ROM shows what was there instead: operator new is a real function at 0x02043444, no operator delete symbol exists anywhere, and every deleting destructor ends with the same load-the-heap / call-Memory::Deallocate pair rather than a call to a shared helper. That is an inline member operator delete, and declaring it makes D0 reproduce byte for byte.

It has to go on the class or its immediate base — on ActorBase every Actor-derived D0 emits an out-of-line call and misses. Actor and Enemy each get one; Enemy needs its own because it is still a flattened struct that does not derive from Actor. This also hands every Platform subclass in #1372/#1375 its D0 for free.

What the classes look like now

Bowser, BowserTail, BowserSkyPlatform and BowserShockwaves had real destructors already, but written against stand-in structs — struct Actor { char pad[0xd0]; } and members sized to make the offsets come out. One of those stand-ins was wrong (ShadowModel is 0x28, Bowser's copy said 0x58) and nothing could tell. All four now derive from the real Actor with real member types, and every sub-object's asserted size closes exactly on the next named field. Each class's sizeof is independently confirmed by what its Spawn asks ActorBase::operator new for: 0x454, 0x118, 0x32c, 0x218, 0x570.

BowserFireSeaArena derives from Platform, which its destructor proves by rewriting the vptr to _ZTV8Platform mid-teardown and destroying two more members at Platform's offsets.

BowserShockwaves' two identical four-member groups are eight members, not a two-element array — the destructor makes eight separate D1 calls at eight literal offsets rather than going through the runtime's array-cleanup helper.

The collateral, which is most of the diff

Absorbing markers into their real members cost eight files their eligibility. They are better for it:

  • Platform::UpdateClsnPosAndRot lost three stand-in structs. 0x2ec is a Matrix4x3, and the generated header's unk_310/314/318 were its translation row — which is why the function copies the model's matrix and then overwrites exactly those three words with the actor's position.
  • Platform.h includes common.h first, deliberately. Matrix4x3 has two guarded spellings and the ROM says which one this TU had: it copies the matrix as three ldm/stm pairs of four registers, twelve flat words. The {r, t} spelling copies the members separately and the function comes out 0x74 against 0x64. Both were built.
  • Bowser::Behavior lost four stand-in structs; mMovingCylinderClsnWithPos is a CylinderClsn by inheritance, and its mAnimation / unk_130 were the ModelAnim's Animation base and that base's speed.
  • Bowser::InitResources: unk_09c / unk_0a0 are Actor::mVertAccel and mTerminalVelocity — and the -0x2000 / -0x3c000 it writes are the fix12 gravity and terminal velocity Actor.h already cites BooCage and MadPiano for.
  • BowserTail::Behavior's struct Actor { static Actor* FindWithID(...); } was never needed — Actor.h already declared it.
  • Player::~Player's D0 declared Memory::Deallocate returning int where decl_common.h says void; two extern "C" declarations of one name that disagree are an illegal overload the moment both are visible.
  • BowserFireSeaArena::InitResources carried a local typedef int Fix12; that collides with the real Fix12<> template. It was already ineligible for unresolved references, so it is absent from both sides of the eligible name list and only check_references noticed.

Not everything readable was free

BowserTail::Behavior's pointer bump to +0x5c and its volatile are load-bearing — the obvious bowser->mPosX spelling compiles and does not reproduce. The file now says so, measured rather than assumed.

Verification (re-run after the rebase onto 2c05ba29)

gate result
strict verify (bytes and relocation destinations) all 12 destructors + all 8 collateral files
eligible.py bracket name list identical, 10807 both ways
rombuild.py -j16 --no-rom 106/106 exact, 0 mismatching, 10,806 source-built, 87.86%
check_header_offsets nonzero field count on all 8 headers, every span matching the operator-new size
prepush_attribution 0 changed, 0 lost
langmode ratchet PASS against the banked chaos-data baseline
port_refcheck / duplicates / check_data_definitions 393/393, none doubled, clean

check_references fails identically on untouched origin/mainfunc_ov091_021339fc -> no longer a candidate, with the banked baseline at eligible 10805 against main's 10807. Reproduced on a pristine checkout at 2c05ba29 before pushing; it is the banked baseline lagging #1368, not this PR.

No --no-verify anywhere.

🤖 Generated with Claude Code

https://claude.ai/code/session_015AXm5k53WFPjCYcDHRDX3x

…igratable

Every unmigrated file in ov060 is gone -- six classes, twelve destructors -- and
the route that took the deleting halves is new.

THE D0 ROUTE. 258 of the tree's 261 D0 files were unmigrated against 74 migrated
D1s, and the reason was one missing declaration. The compiler generates D0 as "run
the destructor body, then call operator delete on the class"; with none declared it
emits a call to the global _ZdlPv, which exists nowhere in this image, and D0 comes
out three instructions short. The ROM shows what was there instead: operator new is
a real function at 0x02043444, no operator delete symbol exists anywhere, and every
deleting destructor ends with the same load-the-heap / call-Memory::Deallocate pair
rather than a call to a shared helper. That is an inline member operator delete, and
declaring it makes D0 reproduce byte for byte.

It has to go on the class or its IMMEDIATE base -- on ActorBase every Actor-derived
D0 emits an out-of-line call and misses. Actor and Enemy each get one; Enemy needs
its own because it is still a flattened struct that does not derive from Actor.

WHAT THE CLASSES LOOK LIKE NOW. Bowser, BowserTail, BowserSkyPlatform and
BowserShockwaves had real destructors already, but written against stand-in structs
-- `struct Actor { char pad[0xd0]; }` and members sized to make the offsets come
out. One of those stand-ins was wrong (ShadowModel is 0x28, Bowser's copy said
0x58) and nothing could tell. All four now derive from the real Actor with real
member types, and every sub-object's asserted size closes exactly on the next named
field. Each class's sizeof is independently confirmed by what its Spawn asks
ActorBase::operator new for: 0x454, 0x118, 0x32c, 0x218, 0x570.

Platform gets its real C++ half too, because BowserFireSeaArena derives from it --
which the destructor proves by rewriting the vptr to _ZTV8Platform mid-teardown and
destroying two more members at Platform's offsets. Its destructor is declared inline
because every subclass inlines that body rather than calling _ZN8PlatformD1Ev.

Absorbing markers into their real members cost seven files their eligibility, and
fixing them is most of this diff. They are better for it:

  * Platform::UpdateClsnPosAndRot lost three stand-in structs. 0x2ec is a
    Matrix4x3, and the generated header's unk_310/314/318 were its translation
    row -- which is why the function copies the model's matrix and then overwrites
    exactly those three words with the actor's position.
  * Platform.h now includes common.h FIRST, deliberately. Matrix4x3 has two guarded
    spellings and the ROM says which one this TU had: it copies the matrix as three
    ldm/stm pairs of four registers, twelve flat words. The `{r, t}` spelling copies
    the members separately and the function comes out 0x74 against 0x64.
  * Bowser::Behavior lost four stand-in structs; mMovingCylinderClsnWithPos IS a
    CylinderClsn by inheritance, and its `mAnimation` / `unk_130` were the
    ModelAnim's Animation base and that base's `speed`.
  * Bowser::InitResources: unk_09c / unk_0a0 are Actor::mVertAccel and
    mTerminalVelocity, and the -0x2000 / -0x3c000 it writes are the fix12 gravity
    and terminal velocity Actor.h already cites BooCage and MadPiano for.
  * BowserTail::Behavior's `struct Actor { static Actor* FindWithID(...); }` was
    never needed -- Actor.h already declared it.
  * Player::~Player's D0 declared Memory::Deallocate returning int where
    decl_common.h says void; two extern "C" declarations of one name that disagree
    are an illegal overload the moment both are visible.

ONE REGRESSION THE ELIGIBLE BRACKET COULD NOT SEE, and check_references caught it:
BowserFireSeaArena::InitResources stopped compiling. It carried a local
`typedef int Fix12;` that now collides with the real Fix12<> template the header
reaches through Actor.h, and it read unk_08e, which is Actor::mAngleY. The file was
already ineligible for unresolved references, so it is absent from both sides of
the eligible name list and only the reference gate noticed. Fixed, and its three
`((char*)this)+0xNNN` offsets are named members now -- 0x324 mModel2, 0x374
mMovingMeshCollider2, 0x2ec the inherited mClsnMat.

Not everything readable was free. BowserTail::Behavior's pointer bump to +0x5c and
its volatile are load-bearing -- the obvious `bowser->mPosX` spelling compiles and
does not reproduce -- and the file now says so, measured rather than assumed.

Rebased onto 30475b3 so this is gated together with #1362, which gave real
destructors to eleven engine base classes -- including ShadowModel,
MovingCylinderClsn, MovingCylinderClsnWithPos, MovingMeshCollider, TextureSequence,
MaterialChanger and TextureTransformer, every one of them a member of a class here.
All 96 real-C++ destructors in the combined tree verify strictly.

Verified: all twelve destructors and all seven collateral files pass strict verify,
bytes AND relocation destinations, which the previous commit made trustworthy for
multi-.text objects. eligible.py name list IDENTICAL across the bracket, 10805 both
ways. rombuild -j16 --no-rom: 106/106 exact, 0 mismatching, 10,805 source-built,
87.82%. check_header_offsets reports a nonzero field count for all eight headers
with every span matching the operator-new size. port_refcheck 393/393,
check_data_definitions clean, no duplicate stems, check_references OK (unresolved
241, unchanged), attribution 0 changed / 0 lost, langmode ratchet PASS against the
banked chaos-data baseline, tools suite 263 passed.

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

tangos-validator Bot commented Aug 10, 2026

Copy link
Copy Markdown

✅ PR validation — Passed

Committed merge introduces no reconstruction or attribution regression.

Full merge validation

Check Result
Committed test merge yes
Matched functions 11,178 / 11,347 (98.5%, +0)
Matched code bytes 2,078,800 / 2,211,124 (94.0%, +0)
Tracked source enrollment 10,778 functions, 1,935,768 bytes (87.55%, +0)
Perfect source moves 0 R100
Contributor credit 0 added, 0 changed, 0 lost
Relocation check 0 checked; no affected slots
Port reference check 393 checked; 0 stale
Module fidelity 106/106 exact; 100.000000% compared bytes
Code linked from verified source 10,806 functions, 1,942,656 bytes (87.86%)

The private worker commits a test merge, builds the stock ROM profile, compares every executable module, measures matched and source-built code, checks contributor lineage, and verifies affected relocations. The mod profile is opt-in and is not part of this merge gate.

@tangosdev

Copy link
Copy Markdown
Owner

This one is already in: #1374 was recovered and merged as f105b8a97 before you recreated it. Its whole content delta against current main is now a single contributions.json line.

My fault for the confusion, and the sequence is worth stating plainly: merging #1373 with --delete-branch is what closed #1374, since #1373's branch was its base. Rather than replace it I pushed the old base tip back so #1374 could reopen, retargeted it to main, and deleted the temporary branch again -- so #1374 kept its number, description and discussion, validated clean against main (106/106 exact, 335 relocations VERIFIED, 0 credit lost) and merged. I have stopped passing --delete-branch on anything with PRs stacked above it.

Closing this as superseded by its own merged twin. Nothing is lost -- same head cpp/ov060-bowser, same commit content.

Your note about #1370 and this PR reconstructing Platform independently and agreeing field for field, Matrix4x3 mClsnMat at 0x2ec and the inline ~Platform() {} included, is the more interesting result. Since #1370's is the superset, that plan still holds for the rest of the stack; #1370 is validated clean and waiting only on #1369's full-ROM failure, which I have not touched further -- the evidence table is on #1369.

@tangosdev tangosdev closed this Aug 10, 2026
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