GC2D/GCConsole2: inline boundaries, bounds accessors, and the water gauges (27 -> 33) - #154
Open
KakarottoCake wants to merge 15 commits into
Open
GC2D/GCConsole2: inline boundaries, bounds accessors, and the water gauges (27 -> 33)#154KakarottoCake wants to merge 15 commits into
KakarottoCake wants to merge 15 commits into
Conversation
Three corrections in the HUD update path, found by reading the instruction diffs rather than by sweeping respellings. 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 instead of the value actually computed and clamped. The low-time colour test is also a nested check on the materialised pane colour, not a short-circuit conjunction. checkChangeTelopArray selected the wrong two Dolpic news tables: the 5:0001 && 5:0002 branch takes scDolpicNewsDolpic5_4 and the neither-flag branch takes 5_1, not the other way round. This is a selection fix at the call sites; the definition order of the tables is unchanged, since permuting those corrects the code offsets but breaks .data symbol order. updateCoinBlendPaneState tests the pane flag with retail's branch polarity. No new byte-identical functions. setTimer 96.12% -> 99.81%, processDownCoin 93.91% -> 95.82%, checkChangeTelopArray 99.93% -> 99.94% (now frame-only).
startDisappearTimer now matches byte for byte, and startDownLeftBot goes
98.68% -> 99.78%.
Both were blocked by the same construct. Where we compute `525 - y1`
retail emits two instructions:
subfic r3, r0, 0x1d1 (465 - y1)
addi r0, r3, 0x3c (+ 60)
not the folded `subfic r0, r0, 0x20d` (525 - y1) we were producing. MWCC
folds constants before it inlines and does not re-fold afterwards, so the
465 can only reach the caller un-added if it arrives across an inline
boundary. Writing the arithmetic out as `465 - y1 + 60` does not
reproduce it -- that folds -- which is what makes this codegen evidence
for a real function rather than a spelling choice.
This is the helper TheAzack9 asked to hold off on during the doldecomp#152 review,
restored under his own name and marked with his `// Possibly inline`
convention, because the disassembly now argues for it. He also suggested
it belongs on TExPane rather than here; that call is his, and this keeps
it file-local until he makes it.
Two things measured and deliberately not done. Routing the sites that
want a plain `465 - y1` through the same helper costs a match, so retail
really does have both forms and the helper is only used where the `+ 60`
appears. And startDownLeftBot's frame is now 16 bytes too large, which is
a frame-size question, not a code one -- left alone rather than fitted.
Report for GMSJ01 (39ad624 - 172258c)📈 Matched code: 37.53% (+0.08%, +2980 bytes) ✅ 6 new matches
📈 15 improvements in unmatched items
📉 3 regressions in unmatched items
|
Four functions match byte for byte: startAppearCoin, startAppearRedCoin,
startInsertJetBalloon and startDisappearStar.
Every `->mInitialBounds` use costs exactly 8 bytes of stack in retail and
none in ours. Measured across six functions before touching anything:
startAppearCoin short by 8 1 use
startAppearTank short by 8 1 use
startAppearRedCoin short by 16 2 uses
startInsertJetBalloon short by 16 2 uses
startAppearStar short by 16 2 uses
startDisappearStar short by 16 2 uses
Those functions were otherwise byte-identical -- cancel the frame delta
and the instruction diff goes completely empty -- so the 8 bytes are the
footprint of an inlined accessor, which MWCC reserves and never reclaims.
The existing `getInitialBounds()` returns `const JUTRect&`, which gets the
frame right but emits an extra `addi` to form the address:
target: lwz r3, 8(r28)
ours: addi r3, r28, 4 ; lwz r3, 4(r3)
A per-field accessor returning `int` gives both the reserved slot and the
direct load. JUTRect already exposes getWidth() and getHeight() the same
way, so this follows the existing shape rather than inventing one.
Returning JUTRect by value was tried first and is decisively wrong --
8099 -> 8091 project, 28 -> 21 unit -- which fits, since JUTRect is 16
bytes and the deltas are 8.
This also corrects frames we were not aiming at: startAppearTank and
startAppearStar now match exactly (both are left unmatched only by
register allocation), startDisappearCoin went +24 -> +8, endCameraDemo
+32 -> +24, startAppearTelop +48 -> +40.
getOffsetForAboveScreen is the same constant-folding argument as
getOffsetForBelowScreen in 0bf0081. Retail emits `neg` then `add` where
we fold to a single `subf`, which means the negation happened behind an
inline boundary. It is used at exactly the two sites where an operation
sits outside that boundary -- `+ unk26A` and `- getHeight()` -- because
those are the only places the boundary is observable; applying it to all
ten sites costs four matches and was measured and rejected.
Matches byte for byte.
The two calls in this function did the same thing in two different
spellings: one subtracted the pane height inside the negation as
`-(y2 + height + 1)`, the other took the above-screen offset and
subtracted the height from it. They are equal arithmetic, but the
compiler kept the difference -- retail adds the height before the +1,
we added the +1 first:
target: subf r0, r3, r0 ; add r3, r4, r0 ; addi r0, r3, 1
ours: subf r3, r3, r0 ; addi r0, r3, 1 ; add r0, r4, r0
Writing both the same way is what a person would have done, and it is
what retail compiled.
unk17C is J2DPane*[18] holding nine pairs, and unk1D0 is JUTRect[9], one
per pair. The loop that populates them indexed unk17C[i] and unk17C[i+1]
for i in 0..8, so each iteration overwrote the previous one's second pane,
indices 9 through 17 were never written at all, and unk1D0[i] took its
bounds from whichever pane happened to be at [i].
Everywhere else in the file already indexes this array correctly, e.g.
console->unk17C[console->unk1CC[0] * 2]->show();
console->unk17C[console->unk1CC[0] * 2 + 1]->setBounds(...);
so the pairing was already established; only the loop that fills it was
wrong.
No match change -- load() is 99.66% and dominated by a 344-byte frame
difference, and this region is a few instructions of it. Committing it
because the code is wrong as written, not because it moves a number.
…FontSize
92.33% -> 95.24%. Still unmatched: loadAfter's frame is 344 bytes short,
which is a separate question and is left alone.
We were calling the wrong method. The target dispatches through the pane's
vtable at +0x14, which is 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 separately called
gpSystemFont's vtable +0x24 for its height. The target instead calls
gpSystemFont's +0x28, annotated in JUTFont.hpp as getWidth(), and shifts
it left by 10.
The bounds also come from a copy, not a direct read: the target builds a
JUTRect on the stack via JUTRect::copy and takes y2 - y1 from it, and uses
that same one height for both boxes rather than reading each box's own
bounds.
lwz r3, 0x528(r31) ; unk528
stb r29, 0xc(r3) ; hide
lwz r4, gpSystemFont
bl J2DTextBox::setFont
addi r3, r1, 0x550
addi r4, r4, 0x14
bl JUTRect::copy ; JUTRect bounds(unk528->mBounds)
lwz r12, 0x28(r12) ; gpSystemFont->getWidth()
subf r23, r4, r0 ; bounds.getHeight()
slwi r4, r3, 10
lwz r12, 0x14(r12) ; unk528->resize(...)
The `<< 10` is left 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.
Also note the ordering: each box is hidden, given its font and resized in
turn, rather than the two being hidden together and then configured
together.
KakarottoCake
force-pushed
the
gcconsole2-inline-boundaries
branch
from
August 27, 2026 21:03
166d0c1 to
efb8a85
Compare
Mrkol
requested changes
Aug 27, 2026
| int getInitialX1() const { return mInitialBounds.x1; } | ||
| int getInitialY1() const { return mInitialBounds.y1; } | ||
| int getInitialX2() const { return mInitialBounds.x2; } | ||
| int getInitialY2() const { return mInitialBounds.y2; } |
Collaborator
There was a problem hiding this comment.
I don't feel like these could ever be real so lets leave them out of the PR for now
processDownCoin 95.82% -> 98.88%, processAppearCoin 95.80% -> 98.86%.
setEmitterToPaneCenter already existed and was already used three times in
perform, but the identical body was also written out longhand in three
other places. Two of them wanted the helper:
JUTRect bounds(unkCC->getPane()->mGlobalBounds);
unk124->mGlobalTranslation.set(bounds.x1 + bounds.getWidth() * 0.5f,
bounds.y1 + bounds.getHeight() * 0.5f,
0.0f);
is character for character the helper's body, and routing it through the
helper gains three points in each function.
processAppearStar has two of these back to back and does NOT want it -- it
drops 91.20% -> 90.27%. I tried the obvious variants: helper for both
(90.27%), swapping their order (90.27%), helper for only the first
(90.75%), only the second (90.53%). All are worse than leaving both
longhand, so that is how they stay, and the file now has the operation in
two spellings. I do not like it either, but that is what the object says
and I would rather leave the inconsistency visible than pick the tidier
version and lose the match.
Marker changed from `// fabricated` to `// Possibly inline`. `fabricated`
asserts "I made this up", and that is no longer the honest claim about
something that measurably improves two functions when used.
91.20% -> 92.09%.
The 100-shine check reads
(!unk50 && shines >= 100) || (unk50 && shines > 100)
but retail evaluates the second arm the other way round. The branch
structure shows it plainly -- on the unk50 path it compares the count
before it re-tests the flag:
708c: cmpwi r25, 0x64 ; shines vs 100
7090: ble -> skip
7094: cmplwi r0, 0 ; unk50
7098: beq -> skip
We emitted the flag test first and the count second.
Worth being explicit about why this is not a fake match, because it looks
like one: `&&` operand order is short-circuit evaluation order, so it is
semantically meaningful and directly visible in the branch layout. That is
different from swapping the operands of a commutative arithmetic
expression to nudge a frame, which produces identical semantics and is
exactly the thing this project rejects. I had this change in front of me
earlier, called it a fake match, and reverted it. That was wrong, and the
disassembly above is why.
It does cost the parallel structure of the two arms, which is a real
readability loss and reads oddly. I am keeping it because the object says
so, but it is the sort of thing worth a second opinion.
83.82% -> 99.12%.
Three separate things, all readable off the register save mask. The target saves
f28-f31 and only r20-r31; we were saving f30/f31 and r17-r31. Retail holds two
more values in float registers and three fewer in integer ones, which says the
quad's vertical extent was computed as f32 and ours was not.
- `top` and `bottom` are `f32` locals. Retail converts each exactly once,
before `GXBegin`, and keeps them in f29/f28 across all four vertices.
- `left` and `right` are *not* locals. The target re-loads `unk2BC[layer].x1`
and `.x2` from memory and re-converts them at every vertex -- four loads and
four `xoris` conversions for two values. That is what an inline
`(f32)unk2BC[layer].x1` at each call site does, and hoisting them to locals
(either int or f32) is what was costing the two extra float registers.
- 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.
The first `GXSetTevColor` also needed the same `JUtility::TColor(u32)`
construction as the second. The target stores the colour to one stack slot and
copies it to a second before the call; passing the array element as an lvalue
only produces one. Both calls now read the same way, which is how they should
have been written regardless.
Left unresolved: the frame is still 72 bytes short, and the two texture-count
tests emit `beq` where the target emits `ble` after the same `cmplwi r0, 0`.
Those are equivalent for an unsigned compare, so `mTextureNum > 0` is folding to
`!= 0` for us and did not for retail. Adding a `getTextureNum()` accessor
returning `int` does not do it -- measured, 99.12% -> 98.85%, reverted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
92.09% -> 95.99%. The two emitter-centring blocks at the end were `JUTRect bounds(...)` and `JUTRect bounds2(...)`. The target only has one rect: it calls `JUTRect::copy` for the first and then inlines the second copy member-wise -- four `lwz`/`stw` pairs -- into the *same* stack slot the first one used. That asymmetry is the tell. Copy-construction goes through the out-of-line `JUTRect::copy`; assigning to an existing rect inlines. `drawWater` in this same file already shows both forms side by side for the same reason, so this is the shape the file was written in. Still off: the frame is 56 bytes 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 would produce that, but the name would then be wrong for what it holds, so I have left it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
74.68% -> 83.86%.
The `else if (unk48)` and `else` arms both ended in the same
`drawGaugeQuadF32(bounds, bounds.y1, bounds.y2, 0.0f, 1.0f)` call, so we emitted
two identical quads and the target emits one. Its `unk48 == 0` and
`unk30C == 0` tests both branch to the *same* `GXBegin`, which is a single
guarded block followed by an unconditional draw:
} else {
if (unk48 && unk30C != 0) { ... }
drawGaugeQuadF32(bounds, bounds.y1, bounds.y2, 0.0f, 1.0f);
}
Worth noting for anyone applying the `drawWater` change to this function: it does
*not* transfer. `drawWater` hoists the quad's top and bottom into f32 locals
because the target converts them once and keeps them in f29/f28. Here 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.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
83.86% -> 84.83%. Two things. The background texture lookup was written out longhand as an if/else over `mTextureNum` into a local. That is exactly `J2DPicture::getTexture(0)`, which already exists and reads `idx < mTextureNum ? mTextures[idx] : nullptr`. Using it collapses five lines to one and also explains a mnemonic that had been bothering me: 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 either way -- it is just which side the zero is on. `drawWater` has the same `ble`, but its two calls are guarded, not a ternary, so the accessor does not fit there and those two branches are still wrong. Second, `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, so that statement precedes it in source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
84.83% -> 87.20%. `if (unk30C >= 25) unk30C = 0;` sat in the caller ahead of the colour computation. It is actually 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`. So the counter is cleared *instead of* picking a fade colour, not before picking one. The two are equivalent, which is why this was easy to miss. 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 as the unmodified base either way. The difference is only in where the reset lives. The helper now takes the counter by reference so the whole flash cycle -- fade in, hold, fade out, reset -- 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 our `clrlwi` zero-extend, so the frame index is an `int` there, not the `u8` we were passing. One thing I cannot explain: the target 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 have left the one instruction wrong rather than write it that way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
startAppearBalloon 92.46% -> 94.17%, processDisappearBalloon 99.72% -> 99.89%. This is the 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 -- which is what a by-value return does and what a `const JUTRect&` return cannot produce. `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. That is 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. `perform` moves 28.09% -> 27.94%. It has 3634 real instruction differences and is nowhere near aligned, so its percentage is not measuring anything useful yet; the third call site is inlined into it and I would rather not make that line read worse to chase noise. Project function count and matched data are both unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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: 8098 -> 8104 functions, and matched data
unchanged at 315,187 bytes. The +6 is 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.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.