Skip to content

GC2D/GCConsole2: inline boundaries, bounds accessors, and the water gauges (27 -> 33) - #154

Open
KakarottoCake wants to merge 1129 commits into
doldecomp:mainfrom
KakarottoCake:gcconsole2-inline-boundaries
Open

KakarottoCake wants to merge 1129 commits into
doldecomp:mainfrom
KakarottoCake:gcconsole2-inline-boundaries

Conversation

@KakarottoCake

@KakarottoCake KakarottoCake commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

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:

function before after
drawWater 83.82% 99.12%
drawWaterBack 74.67% 87.20%
processAppearStar 92.09% 95.99%
startAppearBalloon 92.46% 94.17%
processDownCoin 95.82% 98.88%
processAppearCoin 95.80% 98.86%
processDisappearBalloon 99.72% 99.89%

Full 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:

subfic r3, r0, 0x1d1   ; 465 - y1
addi   r0, r3, 0x3c    ; + 60

not the folded subfic r0, r0, 0x20d. Writing 465 - y1 + 60 by hand does not
reproduce it -- that folds straight back. The constant can only arrive still waiting
for its + 60 if it crossed an inline boundary, because folding happens before
inlining 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 inline marker. getOffsetForAboveScreen is the same
argument: retail emits neg then add where we folded to a single subf.

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 - y1 sites through the same helper costs matches; I measured
it 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 TExPane rather than as a file-local. I have not done
that -- 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 ->mInitialBounds reads:

function short by uses
startAppearCoin 8 1
startAppearTank 8 1
startAppearRedCoin 16 2
startInsertJetBalloon 16 2
startAppearStar 16 2
startDisappearStar 16 2

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() gets
every frame exactly right but emits an extra address computation:

target:  lwz  r3, 8(r28)
ours:    addi r3, r28, 4 ; lwz r3, 4(r3)

A per-field int getInitialY1() const gives both the reserved slot and the direct
load. JUTRect already exposes getWidth()/getHeight() that way, so this follows
what is already there rather than inventing a shape. Returning JUTRect by value is
decisively wrong here -- 8099 -> 8091 project, 28 -> 21 unit -- which fits, since
JUTRect is 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/startAppearStar to 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-f31 and only r20-r31; we were saving two float registers
and 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
GXPosition2f32 points straight at the quad corners.

Three separate things fell out of that, and all three were needed:

  • top and bottom are f32 locals. The target converts each exactly once,
    before GXBegin, and keeps them in f29/f28 across all four vertices.
  • left and right are not locals at all. The target re-loads
    unk2BC[layer].x1/.x2 from memory and re-converts them at every vertex -- four
    loads and four xoris conversions for two values. Hoisting them, as either int or
    f32, is exactly what was costing the two extra float registers. Inline
    (f32)unk2BC[layer].x1 at each call site is what the target does.
  • The height is the picture's, not the cached rect's. The target reads +0x18 and
    +0x20 off unk2A0[layer], which is J2DPane::mBounds.y1/.y2 -- i.e.
    J2DPane::getHeight() -- where we were subtracting inside unk2BC[layer]. The two
    rects 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 before
assuming it would: there the target converts all eight position components separately
at each vertex, which is what drawGaugeQuadF32's int top, int bottom parameters
already produce. The two functions genuinely differ.

4. An assigned JUTRect inlines; a constructed one calls JUTRect::copy

Copy-construction goes through the out-of-line JUTRect::copy. Assigning to a rect
that already exists inlines member-wise as four lwz/stw pairs. That asymmetry is
visible in the object and it identifies which of the two the source used.

processAppearStar ended with JUTRect bounds(...) and JUTRect bounds2(...) for its
two emitter-centring blocks. The target has only one rect: it calls JUTRect::copy for
the first and inlines the second copy into the same stack slot. Reusing the one
variable took it 92.09% -> 95.99%. drawWater already shows both forms side by side
for the same reason, so this is the shape the file was written in.

Read the other way round, the same rule settles the getContentsBounds TODO that was
already sitting in startAppearBalloon. The target copies the contents rect twice
there -- once out of mContentsBounds at +0xec into a stack temporary, then again from
that temporary into the named local. A const JUTRect& return cannot produce that; a
by-value return does. So J2DWindow::getContentsBounds() returns JUTRect by value.
92.46% -> 94.17%.

processDisappearBalloon looked like it contradicted that, because it copies only
once. It does not: the target copies straight into a slot it reads getHeight() out of
two 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

loadAfter was calling the wrong virtual. The target dispatches through the pane's
vtable at +0x14 -- J2DPane::resize, the fourth virtual after the destructor, move
and add, and the one J2DTextBox overrides. We called setFontSize, which is not
virtual at all, and read gpSystemFont's +0x24 (getHeight) where the target reads
+0x28, annotated in JUTFont.hpp as getWidth(), shifted left by 10. The bounds also
come from a stack copy via JUTRect::copy, and one height serves both boxes.
92.33% -> 95.24%.

I left the << 10 as the shift the code performs. I do not know what unit that width
is in and would rather leave it plain than name it wrongly -- flagging it in case you do.

load was filling only half the life-pane array. unk17C is J2DPane*[18]
holding nine pairs and unk1D0 is JUTRect[9], one per pair, but the loop indexed
unk17C[i] and unk17C[i + 1] for i in 0..8. Each iteration overwrote the previous
pair's second pane, indices 9 through 17 were never written, and unk1D0[i] took its
bounds from whichever pane landed at [i]. Everywhere else already indexes [n * 2]
and [n * 2 + 1]. This is match-neutral -- load is dominated by a 344-byte frame
difference -- and is in here because the code is wrong as written, not because it moves
a number.

drawWaterBack was drawing the full gauge twice. The else if (unk48) and else
arms both ended in the same drawGaugeQuadF32(bounds, bounds.y1, bounds.y2, 0.0f, 1.0f)
call. The target's unk48 == 0 and unk30C == 0 tests both branch to the same
GXBegin, which is a single guarded block followed by an unconditional draw.

The pressure flash resets its own counter. if (unk30C >= 25) unk30C = 0; sat in
the caller ahead of the colour computation. It is the final else of the colour chain
itself: the target's cmpwi r4, 0x19 / bge lands on li r0, 0 / stb r0, 0x30c(r29),
which then falls into the shared color + 0xc8. The counter is cleared instead of
picking a fade colour, not before picking one. The two are equivalent -- resetting
first meant the frame-0 branch ran with frame == 0 and both of its terms are
(f32)0 * k, so the colour came out unmodified either way -- which is why it was easy
to 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, not cmplwi) and the
fade's int-to-float conversion is xoris rather than a clrlwi zero-extend, so the
frame index is an int there, not a u8.

drawWaterBack picks its texture through getTexture. The lookup was written out
longhand as an if/else over mTextureNum into a local. That is exactly
J2DPicture::getTexture(0), which already exists. Using it also explains a mnemonic
that had been bothering me in two functions: the target emits ble after
cmplwi r0, 0 where we emitted beq. mTextureNum > 0 canonicalises to != 0 and
gives beq; the accessor's 0 < mTextureNum, with the constant on the left, does not
canonicalise and gives ble. Same test, just which side the zero is on.

waterGun is read before the rect copy, not after -- the target's
lwz r31, 0x3e4(r5) sits between the copy's argument setup and the bl, and a load
cannot be scheduled across a call.

processAppearStar tests the shine count first. I originally rejected
(shines > 100 && unk50) as a fakematch and was wrong; the target's branch layout
tests 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. processDownCoin and
processAppearCoin had the centring written longhand next to a
setEmitterToPaneCenter that does exactly that. processAppearStar keeps its longhand
because 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.

setTimer had two logic holes: the non-sentinel path never assigned timerValue,
so the argument was silently dropped, and the field was then written back from the raw
argument rather than the clamped value. checkChangeTelopArray selected the wrong
two Dolpic news tables -- the 5:0001 && 5:0002 branch takes 5_4 and the neither-flag
branch takes 5_1. That fix is at the call sites deliberately: permuting the table
definitions corrects the code offsets but breaks .data symbol ordering, so it fixes
one thing and breaks another.

startDisappearCoin hid its two panes with two different spellings of the same
arithmetic; retail added the height before the + 1 and we added the + 1 first.


Measured and rejected

Stating these so nobody repeats them:

  • JUTRect getInitialBounds() by value -- 8099 -> 8091, 28 -> 21.
  • Applying getOffsetForAboveScreen to all ten -(y2 + 1) sites -- startAppearCoin
    falls 100% -> 81%.
  • Routing plain 465 - y1 through getOffsetForBelowScreen -- costs a match.
  • A getTextureNum() accessor returning int, to try to get drawWater's ble the
    same way getTexture does -- 99.12% -> 98.85%.
  • Clamping drawWater's y as a ternary at the use site rather than an in-place if.
    The target keeps y and the passed value in separate registers, which is what a
    ternary produces, but it measured 99.12% -> 95.83%. The if is right and the extra
    mr comes from somewhere else.
  • Hoisting drawWater's left/right to locals of any type -- that is what the two
    spurious float register saves were.
  • Moving drawWater's height/topDiff statics below the GXSetChanAmbColor call.
    This also zeroes the .sdata2 offset 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_MESSAGE const below accounts for. I committed it, found the real
    cause, and dropped it. Neither is in this PR, and this TU's .sdata2 is therefore
    still off by 4.
  • Nesting endCameraDemo's body inside the unk50 test, which is what the TODO in that
    function predicts. The compiler collapses it to the same single beq, so it buys
    nothing and costs a 40-line body indented two levels. The TODO stands.
  • Const-qualifying SMS_NO_MEMORY_MESSAGE in System/DummyStrings.hpp. I had this in
    the PR and have removed it -- flagging it because it looked right and was not.
    That
    symbol appears in mario.MAP 293 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 this
    TU's data sections. But DummyStrings.hpp is included very widely, and across the tree
    it costs 4,416 bytes of matched data for zero functions -- 315,187 -> 310,771 --
    breaking .rodata in MapStaticObject, MapObjFloat and MapObjSirena and .sdata in
    NpcInitPrg 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.
  • Removing the duplicate <System/DummyStrings.hpp> include (no match change, .rodata
    goes 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.

checkChangeTelopArray is the frustrating one: 99.94%, every instruction identical,
and 48 bytes of stack unaccounted for with no mInitialBounds use to explain it.
processAppearLife's frame is already the right size -- its three JUTPoint
temporaries 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. startDownLeftBot and entryHelpActor
are the other direction, 16 and 8 bytes too big.

I deliberately stopped rather than guess at these. processDrawTelop needs +24 and
already carries an unused textBounds local; getting there means inventing two more
locals, which is the line I am not crossing.

Real code differences left, in the ones I worked this round:

  • drawWater 99.12% -- frame 72 short. The two texture-count tests still emit beq
    where the target emits ble after the same cmplwi r0, 0. Both calls there are
    guarded rather than ternaries, so getTexture does not fit and I have not found the
    spelling that does.
  • drawWaterBack 87.20% -- frame 32 short with one extra saved GPR; the target
    re-reads bounds.y1 from the stack at each use where we cache it. It also compares
    < 15 and < 25 signed but < 10 unsigned, on the same register in the
    same 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.
  • processAppearStar 95.99% -- frame 56 short, and blueCoinValue gets an extra
    mr r25, r0 because the target allocates the subtraction straight into the register
    already holding blueCoins. Collapsing the two into one variable produces that, but
    the name would then be wrong for what it holds.
  • startAppearBalloon 94.17% -- the unk3E0 == unk3E0 term (already commented as a
    probable 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 textually
    different expressions that CSE'd to one load. I could not find a spelling that keeps
    the compare without inventing something.
  • loadAfter 95.24% -- frame 352 short. The target re-evaluates
    (int)(value * 0.01f) where we common-subexpression it, and re-loads the member from
    memory 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.
  • perform 27.94% -- 3634 real instruction differences. Nowhere near aligned, so its
    percentage is not measuring anything useful. It went down 0.15% in this PR from the
    getContentsBounds change, which affects a helper inlined into it; I would rather not
    make that line read worse to chase noise in a function that is this far off.

Three things I could not resolve at all:

  1. The four UNUSED functions in this TU -- changeNum (312 bytes),
    startDisappearLife (240), resetMoveTank (224), startUpLeftBot (148) -- are
    still 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 the
    unit 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.

  2. This TU's .sdata is untouched by this PR and still wrong: two symbols retail
    does not have, dummyMactorStringValue1 and SMS_NO_MEMORY_MESSAGE, and every shared
    symbol at a uniform -8. dummyMactorStringValue1 appears in mario.MAP zero times
    -- it is ours, not retail's, added to force a 12-byte null literal into .rodata.
    Removing it fixes this TU and breaks .rodata in about twenty others. As above, the
    const fix for the other symbol is also a net loss tree-wide. Both need solving in
    DummyStrings.hpp itself, by someone who knows what that header originally was -- the
    comment in it still says nobody does.

  3. Two raw offset casts remain in this file: + 0x68 off unkC4 and + 0xCC off
    the current nozzle. unkC4 searches for the Peach actor, and unkBC/unkC0 next to
    it are properly typed TBathtub*/TBossEel* -- but Peach's class is not decompiled
    anywhere 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_MESSAGE regression got in.

Since then main has been merged in, so the absolute figures now read 8137 functions
and 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.hpp is the one header outside GC2D that this touches. It has three callers,
all in GCConsole2.cpp.

main was merged in rather than rebased, so the changed-file set is just this file plus
the two headers.

Correction to the first version of this PR: it also contained the
SMS_NO_MEMORY_MESSAGE const change described under "measured and rejected" above. The
progress report caught five broken data matches in other units; I have measured it,
confirmed the cause and dropped the commit.

Mrkol and others added 30 commits June 29, 2026 01:04
* 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>
Mrkol and others added 28 commits September 16, 2026 01:54
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
KakarottoCake force-pushed the gcconsole2-inline-boundaries branch from 53f58bb to 3aa259d Compare September 26, 2026 15:57
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.

8 participants