GC2D/GCConsole2: inline boundaries, bounds accessors, and the water gauges (27 -> 33) - #154
Open
KakarottoCake wants to merge 1129 commits into
Open
KakarottoCake wants to merge 1129 commits into
KakarottoCake wants to merge 1129 commits into
Conversation
* Match TAnimalBase ctor, receiveMessage, calcRootMatrix Decompiles three functions in the previously-empty AnimalBase.cpp: - TAnimalBase(u32, const char*): base TSpineEnemy(name) then stores the actor type into mActorType (0x4c). - receiveMessage: unhandled, returns 0. - calcRootMatrix: empty override. All three verified 100% via tools/decomp-diff.py. No stack-padding tricks; the file remains NonMatching pending the rest of the class. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * AnimalBase: add loadAfter (99.7%, stack padding) Correct logic: calls base loadAfter, registers a positional sound when mActorType == 0x800001. Left non-matching (no volatile hack) due to a 16-byte MWCC stack-padding artifact from an unresolved inline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * AnimalBase: review fixes (return FALSE) Address review on doldecomp#117: use FALSE in receiveMessage. mActorType stays a body assignment (inherited from THitActor, so MWCC rejects it in the initializer list). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * AnimalBase: add load (95.6%) Reads the child count and constructs count-1 TAnimalBase instances via an inlined ctor, handing each to initNoLoad_. Declares initNoLoad_ in the header. Correct logic; residual is register allocation from the shared 16-byte stack-padding artifact (no volatile hack). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * AnimalBase: match destructor + adjustment thunk Add virtual ~TAnimalBase() to the header; an empty body reproduces the compiler-generated deleting destructor (108B) and its @32@ this-adjust thunk (8B), both 100%. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * AnimalBase: use MSD_SE_OBJ_KAMOME_SOLO for the sound id Review on doldecomp#117: replace the 0x3813 literal in loadAfter with its named constant from MSound/SoundEffects.hpp. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * AnimalBase: address review (remove explicit dtor, stream>>, UNUSED decls) - Remove the explicit virtual ~TAnimalBase(); the compiler generates the deleting destructor automatically since the base has a virtual dtor (still 100%). - load: read the count via `stream >> count` per review. - Declare the two UNUSED methods present in mario.MAP (animalWalkIn, flyToCurPathNode) that were fully inlined in the original. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * AnimalBase: clang-format (one-line receiveMessage) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * AnimalBase: getRotationFlyToDir returns TVec3<f> by value Per Mrkol's confirmation, the real signature returns a JGeometry::TVec3<f> by value (this shifts to r4). Correct the declaration; body to follow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * AnimalBase: mark getRotationFlyToDir static (per Mrkol) It is a static method (no this). Keep the confirmed static + by-value-return signature; body deferred -- the TVec3 copy-count codegen is the known-unsolved part per maintainer, so not committing a nonmatching guess. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * AnimalBase: add resetRandomCurPathNode (95.7%) Resets the goal path node to a random jitter of the current point when no node is set: +/-500 on x/z via MsRandF, mActorType-specific y handling, then setGoalPath(). Correct logic; residual is register allocation + a 0x10 frame shift (no hacks). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * AnimalBase: match init (99.8%) Decompile init(TLiveManager*): sets up the MActor/keeper, hit params, body/march/turn params, spine nerve (TNerveAnimalGraphWander), graph tracer, frame timer and animation phase (staggered via the manager's TAnimalSaveIndividual params). Reverse-engineered manager->0x5C as ((TAnimalManagerBase*)mManager)->mAnimalSave (confirmed via the manager hierarchy). Logic byte-identical; left non-matching only due to a 0x28 MWCC stack-padding artifact (no volatile hack). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Animal: reverse SMS_Eular2Quat (61.8%, logic-correct) Derive the full Euler-to-quaternion math: result = qy * (qx * qz) built from half-angle axis quaternions, using JGeometry::TQuat4::mul(a, b). Logic is byte-exact; remaining diff is MWCC FP scheduling (cosf/sinf order and qy register retention). Declared in MarioUtil/MathUtil.hpp. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Animal: reverse execWalk body (27%, logic-complete) Full logic reversed and verified against the target: accelerate/ decelerate mMarchSpeed via CLBChase, pick wait/walk turn speed, turn toward unkF4.getPoint(), bank with CLBChase, then rotate the (0,0,marchSpeed) forward vector by SMS_Eular2Quat(mRotation) into mLinearVelocity. Remaining diff is MWCC register allocation / eval order (the original spills far more vector temporaries). TODO'd. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Animal: reverse perform body (79.6%) Full logic reversed for all three perform phases: move (control() + integrate mLinearVelocity into mPosition + kamome SE), update (updateAnmSound/frameUpdate/calcRootMatrix + conditional MActor calc), and draw (build world matrix from rotation+position, then either MActor::viewCalc or the shared-animation path that borrows a flock member's animated joint matrices into this model's draw-matrix buffers). Mapped vtable slots (control=0xC8, updateAnmSound=0xF4, calcRootMatrix=0xC0), J3DModelData draw-mtx data, and the manager's shared-actor lookup. Remaining diff is MWCC register allocation / addressing-mode scheduling. TODO'd. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Animal: reverse initNoLoad_ body (89.3%) Spawn-a-flock-member helper: jitter the new animal's position (and rotation, degree-wrapped) off this one, copy scaling/character ptr/ graph tracer, assign the illegal ground-check data, init() it with our manager, and register it into the enemy name group. Mapped fields 0x24 mScaling, 0x3C unk3C, 0xC4 mGroundPlane. Remaining diff is MWCC spilling 'this' to the stack. TODO'd. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Animal/boid: scaffold TBoidLeader/TBoid + constructors Start the boid flocking unit: derive the TBoidLeader (JDrama::TViewObj subclass, owns a TBoid[] array) and TBoid layouts from the constructors. TBoid::TBoid() 99.9% (only zero-store scheduling differs); TBoidLeader ctor 88.5% (correct logic; residual is MWCC zero-vec temp allocation). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Animal/boid: match ~TBoidLeader (100%) and setGraph (86.8%) Destructor is a byte-exact chained virtual dtor. setGraph is logic- correct: creates the TGraphTracer, snaps to the nearest graph node and caches its point. Residual is the same unsolved MWCC TVec3 by-value copy-elision seen elsewhere (indexToPoint's return copy). * Animal/boid: body calcGoalForce (90.8%) + fold TPathNode into layout calcGoalForce is logic-complete: graph-goal branch normalizes toward the cached node point (setLength 1); node branch adds the path-node point + offset and scales by weight unk48. Residual is MWCC set-scheduling and the by-value TVec3 copy-count. Also folded unk38/unk3C into a TPathNode member. * Animal/boid: body calcForces (84.1%) + expand TBoid/leader layout calcForces is logic-complete: sums the boid's accumulated cohesion/ alignment/separation vecs (weighted), adds the goal force, applies a random 95-100% jitter, normalizes, then applies an avoidance override. Expanded TBoid accumulator vecs (0x24/0x30/0x3C) and folded the leader's 0x5C TPathNode. Residual is the by-value TVec3 copy-count wall. * Animal/boid: body perform (92.4%), correct TBoid rotation/velocity layout perform advances the graph goal point along the rail (re-randomizing the next node when close, else stepping toward it) then runs the flock update. Corrected TBoid layout: unkC is the rotation vec, unk18 the velocity vec (init 0,0,1). calcBoids still stubbed. * Animal/boid: body calcBoids (66.3%) - TU now complete The flocking core: reset accumulators, a pairwise neighbour loop computing separation/alignment/cohesion, per-boid normalization, then an apply pass that runs calcForces, steers pitch/yaw toward the resulting force, rebuilds each boid's velocity from a rotation matrix and advances its position. All 8 TBoidLeader/TBoid functions are now bodied; residuals are the by-value TVec3 copy-count wall shared with PR doldecomp#117. * Reorder functions in boid.cpp and AnimalBase.cpp to match retail map and declare unused functions * Optimize TBoidLeader constructor by using explicit TPathNode() temporaries in initializer list, boosting matching from 77.9% to 88.4% * Animal/fishoid: scaffold TRealoid + TRealoidActor bases (ctors 100%) Reverse the TRealoid (: TSpineEnemy) and TRealoidActor (: TTakeActor) base classes that Butterfly/fishoid/BeeHive all depend on. Both constructors match 100%, validating the multiple-inheritance layout (secondary vtable at 0x20). This header unblocks the three TRealoid- derived Animal units. * Animal/fishoid: dtors, getTakingMtx, perform (100%), checkHitActors TRealoid/TRealoidActor dtors, getTakingMtx and TRealoidActor::perform all match 100%. checkHitActors is logic-complete (48%, residual is register allocation of the 0x80000001 compare constant). Added the Mtx taking-matrix field (0x78) and hit-collision walk. * Animal/fishoid: clipBoids (97.2%) + TFishoidManager (ctor/dtor/vtable 100%, createModelData 99.2%) Type TRealoid::unk150 as TBoidLeader* and add unk154 (TRealoidActor**); vendor boid.hpp for the TBoidLeader layout. clipBoids logic is byte-exact (residual = inline frame padding + induction-var regalloc). TFishoidManager uses fishA-D.bmd model entries (flags 0x10210000). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Animal/fishoid: calcRootMatrix (91.6%) - look-at basis from boid direction Builds the actor root matrix: copies boid pos, holder branch uses getTakingMtx()+PSMTXCopy, else constructs an orthonormal basis via three inline cross products + PSVECNormalize (col0 negated). Logic byte-exact; residual is frame padding + stfsu store scheduling. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Animal/fishoid: TRealoid::perform (92.7%) - inlines clipBoids perform runs the boid sim (unk150->perform), then when flag 2 is set clips boids (inlined clipBoids) and recalcs each actor root matrix, then performs each actor. Reordered clipBoids before perform so MWCC inlines it (while it stays an out-of-line symbol, matching the inline-deferred TU). Residual inherits clipBoids' frame-padding wall. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Animal/fishoid: TRealoid::loadDefault (89.9%) + createRealoidActor pure virtual Loader: TSpineEnemy::load, reads boid count, allocates TMActorKeeper + TBoidLeader, seeds the leader path node from the actor position, calls setGraph, then per-boid creates an MActor and a realoid actor (virtual createRealoidActor) stacking them 10 units apart. Residual is frame padding + TPathNode struct-copy staging. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Animal/fishoid: TFishoid + TFish cluster - TU functionally complete Adds the TFishoid (: TRealoid, TBoidLeader* member) and TFish (: TRealoidActor) classes and all their methods. 100%: both ctors/dtors + secondary dtors, TFish::init, TFishoid::init, TFishoid vtable. Logic-complete with TODOs: perform 81.3% (inlines TRealoid::perform + Y-clamp + item track), load 66.2% (model select, event-obj/coin spawn, boid params, mario target, fish_swim anim), createRealoidActor 71.9% (new TFish, ctor inlined). Every fishoid function is now implemented; residuals are the frame/regalloc wall. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Animal/fishoid: reorder definitions to match retail .text order Reverse-emission (inline-deferred TU) source order so the linked function layout matches the retail map. No behavior change; all per-function matches preserved (inline chains clipBoids->perform->TFishoid::perform intact). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Animal/fishoid: clang-format 21 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Enemy/egggen: TEggGenerator + TEggGenManager (whole TU) Greenfield unit. 100%: both ctors/dtors (+@32@ thunk), TEggGenManager::load and vtable. control 99.8% / init 99.9% (byte-exact logic, residual = MWCC stack-padding bug). createModelData 99.2% (identical data, only the entry$ local-symbol number differs). control reads !gpMarioOriginal->mYoshi-> isHatched() (recovered from the li0/li1 bool-normalize asm); init normalizes mRotation.x-90 to [0,360). Model gene_egg_model1.bmd, params /enemy/egggen.prm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Enemy/generator: TGenerator + TOneShotGenerator (whole TU) Greenfield unit. 100%: both ctors/dtors (+@32@ thunk), TOneShotGenerator::load, both vtables. loadAfter 99.9% (registers into 敵グループ via TNameRefGen::search<TIdxGroupObj>(...)->getChildren().push_back(this) + Conductor::registerOtherObj). receiveMessage 99.8% (isActorType(0x01000001) + resetSRTV spawn). perform 99.8% (timer-driven getFarOutEnemy spawn via resetSRTV, velocity rotated by MsMtxSetRotRPH). load 89.9% (byte-exact logic; discarded-read stack coalescing residual, TODO'd). Applied method-based technique from Mrkol's cleanup: stream>>, isActorType, getTracer()->setGraph. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Enemy/egggen: pull in MActor infectious strings for .rodata match Add the M3DUtil/InfectiousStrings.hpp rogue include so the MActorMtxCalcType_* strings (@1490,@1526,@1598-1601) land in egggen's .rodata like the original static-init web. Takes .rodata from 13.6% to ~100%; only entry$ local-symbol numbering (createModelData 99.2%) remains. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* GC2D/SunGlass: TSunGlass + TSunShine (whole TU) 11 funcs 100% incl draw() and both vtables. loadAfter/load/startFade/ TSunShine::perform all 99.7-99.9% (frame/scheduling). TSunGlass::perform 81.9% (int->float conversion scheduling in alpha interp, TODO'd). No hacks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * GC2D/SunGlass: TSunGlass + TSunShine (whole TU) 100%: TSunGlass::perform, TSunShine dtor, both vtables; draw() byte-100%. loadAfter/load/startFade/TSunShine::perform 99.7-99.9% (frame-padding residual). Uses MTX*/VEC* macros + JDrama::TViewObj base calls per AGENTS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…reassociate the throw velocity (doldecomp#172)
…and four reset functions (doldecomp#174)
…nd the death nerves (doldecomp#181)
… fix the shine model name (doldecomp#184)
…t's angle arguments (doldecomp#178)
Restores the 29 manager entries with the classes the map proves: every manager is named "?", the per-species managers get the in-class constructors their weak symbols imply, and TMareMManager/TMareWManager exist as weak vtables in this TU.
…doldecomp#156) Both units' retail objects carry the four `MActorMtxCalcType_*` strings and the `DummyStrings.hpp` pair ahead of them, byte for byte, and neither of our sources emitted them. `M3DUtil/InfectiousStrings.hpp` already exists for exactly this and pulls in `System/DummyStrings.hpp` itself, in that order. DebuTelesa's `.rodata` goes from 241 bytes of missing compiler-generated constants to 24. MarNameRefGen_Map's drops to none. **No change to matched functions or matched data** -- 8131 and 353,763 both before and after. This is a source-fidelity fix, not a scoring one. It closes the head of those two `.rodata` sections so that whatever is fixed there next is not sitting at the wrong offset. On picking the units: the retail MAP lists `MtxCalcTypeName` for 181 `.cpp` files, but that is the wrong list to work from. 164 of those are `UNUSED` with a `........` address -- dead-stripped, so absent from the extracted objects that objdiff actually compares against. Emitting the strings in those units would put bytes in our `.rodata` that the target can never have. The right list is the 142 units whose retail *object* still carries them, and the existing 87 carriers in our tree agree: 86 of them are in that set. That leaves 56 units still to do. 54 of them have pre-existing `check-changed-symbol-order.py` failures on unmodified main, so touching them would redden a PR over breakage they already had -- I have left those alone rather than bury this in unrelated red. These two are the ones that are clean. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ecomp#183) * Enemy/tobiPuku: match 93 of the TU's 116 functions from the map * Enemy/tobiPuku: use existing idioms per review
…odes (doldecomp#190) * Enemy/smallEnemy: match TSmallEnemy::kill * Enemy/bgtentacle: match changeStateAndFixNodes
…aitForAnyKey (doldecomp#180) * GC2D/CardLoad and CardSave: match the score screen, drawMessage and waitForAnyKey * GC2D/CardLoad: drop the frame note in changeScene * GC2D/CardSave: keep the getCurMessageID getter
Remove getInitialX1, getInitialY1, getInitialX2, getInitialY2 from ExPane. Use mInitialBounds directly at all sites. The getOffsetFor helpers stay and read the fields directly.
KakarottoCake
pushed a commit
to KakarottoCake/sms
that referenced
this pull request
Sep 26, 2026
The comment block above `enum MActorMtxCalcType` landed in cb616fe at 82 and 81 columns, against the repo's `ColumnLimit: 80`, so `check-format-and-tidy` fails on it. This is not confined to one branch. The clang-format action walks the whole tree rather than the diff, so every open pull request goes red the moment it merges main, for a violation none of them introduced. I hit it on doldecomp#154 and confirmed it is not specific to that branch. It is also the only one: after this reflow, `clang-format --dry-run -Werror` across all of `src/` and `include/` reports zero failing files. Only the line breaks move. The wording is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
KakarottoCake
force-pushed
the
gcconsole2-inline-boundaries
branch
from
September 26, 2026 15:57
53f58bb to
3aa259d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #152, continuing GCConsole2 as asked.
mario/GC2D/GCConsole2 27/60 -> 33/60. Project 8098 -> 8104. Six functions now
match byte for byte:
startDisappearTimer,startAppearCoin,startAppearRedCoin,startInsertJetBalloon,startDisappearStar,startDisappearCoin.After that I went back over the ones you called out as poorly matched. Those did not
land byte matches, but they moved a long way and the reasons are concrete:
drawWaterdrawWaterBackprocessAppearStarstartAppearBalloonprocessDownCoinprocessAppearCoinprocessDisappearBalloonFull disclosure below, including the things I got wrong and the things I am still
stuck on, so you do not have to re-derive any of it.
Four mechanisms did most of the work
1. Retail doing arithmetic in more steps than us means an inline boundary
Where we wrote
525 - y1, retail emits two instructions:not the folded
subfic r0, r0, 0x20d. Writing465 - y1 + 60by hand does notreproduce it -- that folds straight back. The constant can only arrive still waiting
for its
+ 60if it crossed an inline boundary, because folding happens beforeinlining and there is no re-fold afterwards.
That is
getOffsetForBelowScreen, which @TheAzack9 asked me to hold off on during the#152 review as unproven. The codegen now argues for it, so I have restored it under his
name with his
// Possibly inlinemarker.getOffsetForAboveScreenis the sameargument: retail emits
negthenaddwhere we folded to a singlesubf.Both are used only where an operation sits outside the boundary --
+ 60,+ unk26A,- getHeight(). That is the only place the boundary is observable.Routing the plain
465 - y1sites through the same helper costs matches; I measuredit and did not do it. So retail genuinely has both spellings, which is a bit
unsatisfying but is what the object says.
He also thought this belonged on
TExPanerather than as a file-local. I have not donethat -- it is his call, and this PR should not grow a new API on his behalf.
2. Each inlined scalar accessor call costs exactly 8 bytes of stack
Measured across six functions before changing anything -- frame deficit against number
of direct
->mInitialBoundsreads:Six for six, and those functions were otherwise byte-identical -- pad the frame by the
delta and the instruction diff goes completely empty.
The catch was the return type. The existing
const JUTRect& getInitialBounds()getsevery frame exactly right but emits an extra address computation:
A per-field
int getInitialY1() constgives both the reserved slot and the directload.
JUTRectalready exposesgetWidth()/getHeight()that way, so this followswhat is already there rather than inventing a shape. Returning
JUTRectby value isdecisively wrong here -- 8099 -> 8091 project, 28 -> 21 unit -- which fits, since
JUTRectis 16 bytes and every delta is 8.This landed four matches at once and also corrected frames I was not aiming at:
startDisappearCoin+24 -> +8,endCameraDemo+32 -> +24,startAppearTelop+48 -> +40, and
startAppearTank/startAppearStarto exact.3. The register save mask says which values were floats
This is what cracked
drawWater, and it is the one I would reach for first next time.The target saves
f28-f31and onlyr20-r31; we were saving two float registersand three more integer ones. Retail was holding values in float registers that we
held in integer registers, which for a function whose only floating-point work is
GXPosition2f32points straight at the quad corners.Three separate things fell out of that, and all three were needed:
topandbottomaref32locals. The target converts each exactly once,before
GXBegin, and keeps them in f29/f28 across all four vertices.leftandrightare not locals at all. The target re-loadsunk2BC[layer].x1/.x2from memory and re-converts them at every vertex -- fourloads and four
xorisconversions for two values. Hoisting them, as eitherintorf32, is exactly what was costing the two extra float registers. Inline(f32)unk2BC[layer].x1at each call site is what the target does.+0x20 off
unk2A0[layer], which isJ2DPane::mBounds.y1/.y2-- i.e.J2DPane::getHeight()-- where we were subtracting insideunk2BC[layer]. The tworects hold the same numbers here, so this is not a behaviour change, but it is the
one retail actually reads.
83.82% -> 99.12%. This does not transfer to
drawWaterBack, and I checked beforeassuming it would: there the target converts all eight position components separately
at each vertex, which is what
drawGaugeQuadF32'sint top, int bottomparametersalready produce. The two functions genuinely differ.
4. An assigned
JUTRectinlines; a constructed one callsJUTRect::copyCopy-construction goes through the out-of-line
JUTRect::copy. Assigning to a rectthat already exists inlines member-wise as four
lwz/stwpairs. That asymmetry isvisible in the object and it identifies which of the two the source used.
processAppearStarended withJUTRect bounds(...)andJUTRect bounds2(...)for itstwo emitter-centring blocks. The target has only one rect: it calls
JUTRect::copyforthe first and inlines the second copy into the same stack slot. Reusing the one
variable took it 92.09% -> 95.99%.
drawWateralready shows both forms side by sidefor the same reason, so this is the shape the file was written in.
Read the other way round, the same rule settles the
getContentsBoundsTODO that wasalready sitting in
startAppearBalloon. The target copies the contents rect twicethere -- once out of
mContentsBoundsat +0xec into a stack temporary, then again fromthat temporary into the named local. A
const JUTRect&return cannot produce that; aby-value return does. So
J2DWindow::getContentsBounds()returnsJUTRectby value.92.46% -> 94.17%.
processDisappearBalloonlooked like it contradicted that, because it copies onlyonce. It does not: the target copies straight into a slot it reads
getHeight()out oftwo instructions later and never touches again -- an unnamed temporary, not a local.
The rect was only ever there to be measured, so the local goes and the call reads as
one expression. 99.72% -> 99.89%.
The rest of the commits
loadAfterwas calling the wrong virtual. The target dispatches through the pane'svtable at +0x14 --
J2DPane::resize, the fourth virtual after the destructor,moveand
add, and the oneJ2DTextBoxoverrides. We calledsetFontSize, which is notvirtual at all, and read
gpSystemFont's +0x24 (getHeight) where the target reads+0x28, annotated in
JUTFont.hppasgetWidth(), shifted left by 10. The bounds alsocome from a stack copy via
JUTRect::copy, and one height serves both boxes.92.33% -> 95.24%.
I left the
<< 10as the shift the code performs. I do not know what unit that widthis in and would rather leave it plain than name it wrongly -- flagging it in case you do.
loadwas filling only half the life-pane array.unk17CisJ2DPane*[18]holding nine pairs and
unk1D0isJUTRect[9], one per pair, but the loop indexedunk17C[i]andunk17C[i + 1]for i in 0..8. Each iteration overwrote the previouspair's second pane, indices 9 through 17 were never written, and
unk1D0[i]took itsbounds from whichever pane landed at
[i]. Everywhere else already indexes[n * 2]and
[n * 2 + 1]. This is match-neutral --loadis dominated by a 344-byte framedifference -- and is in here because the code is wrong as written, not because it moves
a number.
drawWaterBackwas drawing the full gauge twice. Theelse if (unk48)andelsearms both ended in the same
drawGaugeQuadF32(bounds, bounds.y1, bounds.y2, 0.0f, 1.0f)call. The target's
unk48 == 0andunk30C == 0tests both branch to the sameGXBegin, which is a single guarded block followed by an unconditional draw.The pressure flash resets its own counter.
if (unk30C >= 25) unk30C = 0;sat inthe caller ahead of the colour computation. It is the final
elseof the colour chainitself: the target's
cmpwi r4, 0x19 / bgelands onli r0, 0 / stb r0, 0x30c(r29),which then falls into the shared
color + 0xc8. The counter is cleared instead ofpicking a fade colour, not before picking one. The two are equivalent -- resetting
first meant the frame-0 branch ran with
frame == 0and both of its terms are(f32)0 * k, so the colour came out unmodified either way -- which is why it was easyto miss. The helper now takes the counter by reference so the whole cycle reads in one
place. Its comparisons are also signed in the target (
cmpwi, notcmplwi) and thefade's int-to-float conversion is
xorisrather than aclrlwizero-extend, so theframe index is an
intthere, not au8.drawWaterBackpicks its texture throughgetTexture. The lookup was written outlonghand as an if/else over
mTextureNuminto a local. That is exactlyJ2DPicture::getTexture(0), which already exists. Using it also explains a mnemonicthat had been bothering me in two functions: the target emits
bleaftercmplwi r0, 0where we emittedbeq.mTextureNum > 0canonicalises to!= 0andgives
beq; the accessor's0 < mTextureNum, with the constant on the left, does notcanonicalise and gives
ble. Same test, just which side the zero is on.waterGunis read before the rect copy, not after -- the target'slwz r31, 0x3e4(r5)sits between the copy's argument setup and thebl, and a loadcannot be scheduled across a call.
processAppearStartests the shine count first. I originally rejected(shines > 100 && unk50)as a fakematch and was wrong; the target's branch layouttests the count before the flag. Short-circuit order is semantically meaningful and
directly visible in the object, so it is not the same kind of change as swapping
commutative operands.
The coin emitters go through the existing helper.
processDownCoinandprocessAppearCoinhad the centring written longhand next to asetEmitterToPaneCenterthat does exactly that.processAppearStarkeeps its longhandbecause all four variants measured worse there (90.27 / 90.27 / 90.75 / 90.53 against
91.20), which I cannot explain and am flagging rather than papering over.
setTimerhad two logic holes: the non-sentinel path never assignedtimerValue,so the argument was silently dropped, and the field was then written back from the raw
argument rather than the clamped value.
checkChangeTelopArrayselected the wrongtwo Dolpic news tables -- the
5:0001 && 5:0002branch takes5_4and the neither-flagbranch takes
5_1. That fix is at the call sites deliberately: permuting the tabledefinitions corrects the code offsets but breaks
.datasymbol ordering, so it fixesone thing and breaks another.
startDisappearCoinhid its two panes with two different spellings of the samearithmetic; retail added the height before the
+ 1and we added the+ 1first.Measured and rejected
Stating these so nobody repeats them:
JUTRect getInitialBounds()by value -- 8099 -> 8091, 28 -> 21.getOffsetForAboveScreento all ten-(y2 + 1)sites --startAppearCoinfalls 100% -> 81%.
465 - y1throughgetOffsetForBelowScreen-- costs a match.getTextureNum()accessor returningint, to try to getdrawWater'sblethesame way
getTexturedoes -- 99.12% -> 98.85%.drawWater'syas a ternary at the use site rather than an in-placeif.The target keeps
yand the passed value in separate registers, which is what aternary produces, but it measured 99.12% -> 95.83%. The
ifis right and the extramrcomes from somewhere else.drawWater'sleft/rightto locals of any type -- that is what the twospurious float register saves were.
drawWater'sheight/topDiffstatics below theGXSetChanAmbColorcall.This also zeroes the
.sdata2offset and looks like a tidy declaration-order fix.It is not -- it just displaces the section head by 4 bytes, the same 4 that the
SMS_NO_MEMORY_MESSAGEconst below accounts for. I committed it, found the realcause, and dropped it. Neither is in this PR, and this TU's
.sdata2is thereforestill off by 4.
endCameraDemo's body inside theunk50test, which is what the TODO in thatfunction predicts. The compiler collapses it to the same single
beq, so it buysnothing and costs a 40-line body indented two levels. The TODO stands.
SMS_NO_MEMORY_MESSAGEinSystem/DummyStrings.hpp. I had this inthe PR and have removed it -- flagging it because it looked right and was not. That
symbol appears in
mario.MAP293 times and is in.sdata2, the small const section,in every one; we declare
static const char*, a mutable pointer, which lands in.sdata. Adding the second const does put it where the map says, and it cleans up thisTU's data sections. But
DummyStrings.hppis included very widely, and across the treeit costs 4,416 bytes of matched data for zero functions -- 315,187 -> 310,771 --
breaking
.rodatain MapStaticObject, MapObjFloat and MapObjSirena and.sdatainNpcInitPrg and MapMirror, all of which were at 100%. So the map is right about where the
symbol ends up and the one-word change is still the wrong way to get there; something
else in that header is carrying the difference. I only checked matched functions
before pushing, which is how it got in.
<System/DummyStrings.hpp>include (no match change,.rodatagoes from matching to a uniform +32) and removing
<M3DUtil/InfectiousStrings.hpp>(four sections differ instead of two, 185 bytes of compiler-generated constants
vanish). Both includes are correct.
What I am still stuck on
I classified all remaining failures by padding out each frame delta so the diff was
readable, then splitting what was left into three kinds: a real difference (different
opcode, different immediate, an instruction on one side only), register-allocation
permutation, and stack-slot displacement. The pad was removed before committing;
nothing like it survives in this branch.
Frame size only, no real instruction differences --
checkChangeTelopArray,processAppearLife,startAppearLife,startDownLeftBot,startInsertLife,processAppearBalloon,processDisappearBalloon,pauseOut,processDrawTelop,entryHelpActor,processMoveNozzle,startAppearTelop,startAppearTank,startAppearStar.checkChangeTelopArrayis the frustrating one: 99.94%, every instruction identical,and 48 bytes of stack unaccounted for with no
mInitialBoundsuse to explain it.processAppearLife's frame is already the right size -- its threeJUTPointtemporaries just sit 4 bytes higher than retail's, so retail has one more 4-byte local
than we do and I cannot work out what it is.
startDownLeftBotandentryHelpActorare the other direction, 16 and 8 bytes too big.
I deliberately stopped rather than guess at these.
processDrawTelopneeds +24 andalready carries an unused
textBoundslocal; getting there means inventing two morelocals, which is the line I am not crossing.
Real code differences left, in the ones I worked this round:
drawWater99.12% -- frame 72 short. The two texture-count tests still emitbeqwhere the target emits
bleafter the samecmplwi r0, 0. Both calls there areguarded rather than ternaries, so
getTexturedoes not fit and I have not found thespelling that does.
drawWaterBack87.20% -- frame 32 short with one extra saved GPR; the targetre-reads
bounds.y1from the stack at each use where we cache it. It also compares< 15and< 25signed but< 10unsigned, on the same register in thesame chain. Mixing the spellings in source to reproduce that would make the chain
read arbitrarily, so I left the one instruction wrong rather than write it that way.
processAppearStar95.99% -- frame 56 short, andblueCoinValuegets an extramr r25, r0because the target allocates the subtraction straight into the registeralready holding
blueCoins. Collapsing the two into one variable produces that, butthe name would then be wrong for what it holds.
startAppearBalloon94.17% -- theunk3E0 == unk3E0term (already commented as aprobable copy-paste slip in the original) is folded away by our compiler but survives
in the target as
cmplw r4, r4, so retail's two operands must have been textuallydifferent expressions that CSE'd to one load. I could not find a spelling that keeps
the compare without inventing something.
loadAfter95.24% -- frame 352 short. The target re-evaluates(int)(value * 0.01f)where we common-subexpression it, and re-loads the member frommemory in between, which says its operand is a memory lvalue there rather than
something held in a register. I do not have the shape yet.
perform27.94% -- 3634 real instruction differences. Nowhere near aligned, so itspercentage is not measuring anything useful. It went down 0.15% in this PR from the
getContentsBoundschange, which affects a helper inlined into it; I would rather notmake that line read worse to chase noise in a function that is this far off.
Three things I could not resolve at all:
The four UNUSED functions in this TU --
changeNum(312 bytes),startDisappearLife(240),resetMoveTank(224),startUpLeftBot(148) -- arestill empty stubs. You said UNUSED functions have usually been inlined into other
functions rather than being genuinely dead, so I went looking. For these four it does
not hold: they are absent from the extracted retail object entirely, their MAP
addresses are
........so they were never linked, and no surviving function in theunit calls anything we do not already call. There is no disassembly to read and no
call site to read them from, so anything I write is invention. If you know a build
where these are linked, that would unblock them.
This TU's
.sdatais untouched by this PR and still wrong: two symbols retaildoes not have,
dummyMactorStringValue1andSMS_NO_MEMORY_MESSAGE, and every sharedsymbol at a uniform -8.
dummyMactorStringValue1appears inmario.MAPzero times-- it is ours, not retail's, added to force a 12-byte null literal into
.rodata.Removing it fixes this TU and breaks
.rodatain about twenty others. As above, theconst fix for the other symbol is also a net loss tree-wide. Both need solving in
DummyStrings.hppitself, by someone who knows what that header originally was -- thecomment in it still says nobody does.
Two raw offset casts remain in this file:
+ 0x68offunkC4and+ 0xCCoffthe current nozzle.
unkC4searches for the Peach actor, andunkBC/unkC0next toit are properly typed
TBathtub*/TBossEel*-- but Peach's class is not decompiledanywhere in the tree, so I cannot type it without guessing. Leaving them ugly and
visible rather than dressing them up.
Verification
mario.dol: OK. Whole-tree clang-format clean. Symbol order clean on the changed file.CRLF preserved.
Checked project-wide, not just this unit: when the branch was opened this was
8098 -> 8104 functions with matched data unchanged at 315,187 bytes, the +6 being
exactly this unit's +6. I am calling out the data figure specifically because I did not
check it the first time round, which is how the
SMS_NO_MEMORY_MESSAGEregression got in.Since then
mainhas been merged in, so the absolute figures now read 8137 functionsand 353,763 bytes of matched data. That jump is #130's and the M3DUtil work's, not this
branch's -- this branch's contribution is still the same +6 functions in GCConsole2, with
matched data unchanged. GCConsole2 is 33/60 after the merge, exactly as before it.
J2DWindow.hppis the one header outside GC2D that this touches. It has three callers,all in
GCConsole2.cpp.mainwas merged in rather than rebased, so the changed-file set is just this file plusthe two headers.
Correction to the first version of this PR: it also contained the
SMS_NO_MEMORY_MESSAGEconst change described under "measured and rejected" above. Theprogress report caught five broken data matches in other units; I have measured it,
confirmed the cause and dropped the commit.