ov060: the Bowser fight becomes real C++, and D0 destructors become migratable - #1376
ov060: the Bowser fight becomes real C++, and D0 destructors become migratable#1376andrewboudreau wants to merge 1 commit into
Conversation
…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
✅ PR validation — PassedCommitted merge introduces no reconstruction or attribution regression. Full merge validation
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. |
|
This one is already in: #1374 was recovered and merged as My fault for the confusion, and the sequence is worth stating plainly: merging #1373 with Closing this as superseded by its own merged twin. Nothing is lost -- same head Your note about #1370 and this PR reconstructing |
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 promotesPlatformto a real class independently of this PR; its header and mine match field for field, includingMatrix4x3 mClsnMatat0x2ecand 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_ZN8PlatformD1Evand declares theIsClsnInRangepair — so if #1370 lands first I will drop myPlatform.hand the threePlatformmethod files and rebase onto it. OnlyBowserFireSeaArenahere needsPlatformat 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 deleteon 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 newis a real function at0x02043444, nooperator deletesymbol exists anywhere, and every deleting destructor ends with the same load-the-heap / call-Memory::Deallocatepair rather than a call to a shared helper. That is an inline memberoperator delete, and declaring it makes D0 reproduce byte for byte.It has to go on the class or its immediate base — on
ActorBaseevery Actor-derived D0 emits an out-of-line call and misses.ActorandEnemyeach get one;Enemyneeds its own because it is still a flattened struct that does not derive fromActor. This also hands everyPlatformsubclass in #1372/#1375 its D0 for free.What the classes look like now
Bowser,BowserTail,BowserSkyPlatformandBowserShockwaveshad 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 (ShadowModelis0x28, Bowser's copy said0x58) and nothing could tell. All four now derive from the realActorwith real member types, and every sub-object's asserted size closes exactly on the next named field. Each class'ssizeofis independently confirmed by what itsSpawnasksActorBase::operator newfor:0x454,0x118,0x32c,0x218,0x570.BowserFireSeaArenaderives fromPlatform, which its destructor proves by rewriting the vptr to_ZTV8Platformmid-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::UpdateClsnPosAndRotlost three stand-in structs.0x2ecis aMatrix4x3, and the generated header'sunk_310/314/318were 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.hincludescommon.hfirst, deliberately.Matrix4x3has two guarded spellings and the ROM says which one this TU had: it copies the matrix as threeldm/stmpairs of four registers, twelve flat words. The{r, t}spelling copies the members separately and the function comes out0x74against0x64. Both were built.Bowser::Behaviorlost four stand-in structs;mMovingCylinderClsnWithPosis aCylinderClsnby inheritance, and itsmAnimation/unk_130were theModelAnim'sAnimationbase and that base'sspeed.Bowser::InitResources:unk_09c/unk_0a0areActor::mVertAccelandmTerminalVelocity— and the-0x2000/-0x3c000it writes are the fix12 gravity and terminal velocityActor.halready cites BooCage and MadPiano for.BowserTail::Behavior'sstruct Actor { static Actor* FindWithID(...); }was never needed —Actor.halready declared it.Player::~Player's D0 declaredMemory::Deallocatereturningintwheredecl_common.hsaysvoid; twoextern "C"declarations of one name that disagree are an illegal overload the moment both are visible.BowserFireSeaArena::InitResourcescarried a localtypedef int Fix12;that collides with the realFix12<>template. It was already ineligible for unresolved references, so it is absent from both sides of the eligible name list and onlycheck_referencesnoticed.Not everything readable was free
BowserTail::Behavior's pointer bump to+0x5cand itsvolatileare load-bearing — the obviousbowser->mPosXspelling compiles and does not reproduce. The file now says so, measured rather than assumed.Verification (re-run after the rebase onto
2c05ba29)eligible.pybracketrombuild.py -j16 --no-romcheck_header_offsetsprepush_attributionport_refcheck/ duplicates /check_data_definitionscheck_referencesfails identically on untouchedorigin/main—func_ov091_021339fc -> no longer a candidate, with the banked baseline at eligible 10805 against main's 10807. Reproduced on a pristine checkout at2c05ba29before pushing; it is the banked baseline lagging #1368, not this PR.No
--no-verifyanywhere.🤖 Generated with Claude Code
https://claude.ai/code/session_015AXm5k53WFPjCYcDHRDX3x