Skip to content

TextureSequence::Prepare and MaterialChanger::Prepare are static - #1390

Open
andrewboudreau wants to merge 4 commits into
mainfrom
cpp/prepare-static-v2
Open

TextureSequence::Prepare and MaterialChanger::Prepare are static#1390
andrewboudreau wants to merge 4 commits into
mainfrom
cpp/prepare-static-v2

Conversation

@andrewboudreau

Copy link
Copy Markdown
Collaborator

TextureSequence::Prepare and MaterialChanger::Prepare were declared as ordinary members and defined as forwarding (this, &model, &file). They take two arguments and no this.

Why the wrong reading survived

Both ROM bodies are 0xc long-call veneers — ldr ip,[pc]; bx ip; .word <impl>. A pure tail jump reproduces under any signature, so the definition files never constrained the question. The constraint lives on the two sides nobody had consulted:

The callee. func_02046d50 and func_020470e8 read only r0 and r1:

push {r4-r7,lr} / sub sp,sp,#4
mov  r6, r1      <- ldrh [r6,#2], ldr [r6,#4], ldrh [r6,#8],
mov  r7, r0         ldr [r6,#0xc], ldrh [r6,#0x1c], ldr [r6,#0x20]

r2 is never touched, which rules out a third argument. And the offsets r1 is read at are exactly BTP_File's declared fields — so r1 is the animation file, the second argument. Under the old reading it would have been the BMD.

The callers. Every call site in the tree passes two. So do both matched implementations, src/func_02046d50.c and src/func_020470e8.c.

A static member mangles identically, so no symbol changes and no config moves.

TextureTransformer, the third sibling, was corrected long ago. Its header already names these two veneers as "called the same way" and notes the old reading "cost the PC port a wrong-register hunt" — the follow-up just never happened. This is it.

What changed

Twenty-two call sites drop the mangled extern for TextureSequence::Prepare(model, file). The six .c callers keep theirs — C has no ::.

SnowmanHead and InvisibleSecret declare the class locally rather than including TextureSequence.h: both carry typedef int Fix12;, which collides with the real math/Fix12.h template.

Several of the removed externs were a bare extern on an already-mangled name inside a //cpp file, which mangles a second time. That shows up in the one gate that can see it: _Z49_ZN15TextureSequence7PrepareER8BMD_FileR8BTP_Fileii leaves the unresolved backlog. Eyerok's linkcheck improves BLIND-39 -> BLIND-35 as four call destinations become resolvable.

The other three commits

Whomp's MeshColliderBase address was double-mangled. Found while running the gates, in a file this change already touches. The declaration sat outside the extern "C" block above it, so the address taken pointed at a _Z88_ZN16... that does not exist. The byte gate cannot see this — relocated words are wildcards. Linkcheck goes BLIND-1 -> VERIFIED and the function still reproduces.

The unresolved-reference baseline is re-pinned to main. config/unresolved-baseline.json had drifted: it recorded 10805 eligible against main's 10813, and check_references fails on clean origin/main with no feature branch involved:

git checkout origin/main
python tools/eligible.py
python tools/check_references.py            # FAIL
python tools/check_references.py --update
# refusing to update: symbols left the backlog by failing differently

The ratchet could not unstick itself. --update refuses, correctly, because one symbol left the backlog without being fixed: func_ov091_021339fc was deliberately converted to a hand-written asm hatch by an earlier merged PR, which takes it out of eligible.py's candidate set. It did not degrade, it was reclassified — and the reason-switching guard cannot tell those apart, which is exactly why unsticking it has to be a deliberate decision rather than a --update.

The other two entries are main's as well, and neither has a correct fix:

  • func_02057410 references _ll_udiv / _ull_mod, mwccarm's names for the 64-bit division helpers it emits for the (u64)v / (u64)pf[1] at func_02057410.c:304. The ROM's implementations are __aeabi_uldiv / __aeabi_ulmod in ITCM (0x01ffa9dc, 0x01ffa9e8), but check_references has no alias mechanism, and adding _ll_udiv as a second symbol at that address would create the duplicate-address condition that makes CI report lost matched functions.
  • Whomp::InitResources references func_021135d4, which dsd resolves to module:overlays(9,14,15,16,18,19,20,22,24,25,26,27,29,32,36). All fifteen load at 0x021111a0, so that one instruction targets different data depending on the level and no single name is correct.

The baseline was generated on a clean origin/main checkout, so it carries nothing from this branch. Verify by checking out origin/main, running tools/eligible.py, and regenerating — the result is byte-identical.

Then two fixes are banked back down with --update, which is downward-only: the two double-mangled names above leave the backlog, nothing is added, and the ratchet now refuses to let either come back.

Gates

  • rombuild 106/106 exact, 10,813 reproducing, 0 mismatching
  • eligible name list byte-identical to main at 10813
  • check_references OK: no new unresolvable references (pre-push hook passes; no --no-verify)
  • prepush_linkcheck 23 checked, 21 verified, 0 blocking
  • prepush_attribution 0 changed / 0 lost · port_refcheck 393 resolve · header offsets, data definitions, duplicate sources clean

Not fixed, stated plainly

Goomba::InitResources and MrI::InitResources do not reproduce — before or after. Both fail identically on origin/main (999 and 7 words), and their compiled bytes are unchanged by this branch: 916 → 916 and 664 → 664, identical in both cases. So the edits there are provably neutral, not merely still-failing.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe

andrewboudreau and others added 4 commits August 11, 2026 01:36
config/unresolved-baseline.json had drifted behind: it recorded 10805
eligible against main's 10813, and check_references fails on clean
origin/main with no feature branch involved. Reproduce before this commit:

    git checkout origin/main
    python tools/eligible.py
    python tools/check_references.py            # FAIL
    python tools/check_references.py --update
    # refusing to update: symbols left the backlog by failing differently

The ratchet could not unstick itself. --update refuses, correctly, because
one symbol left the backlog without being fixed:

    func_ov091_021339fc (ov091:0x021339fc)  ->  no longer a candidate

That one was deliberately converted to a hand-written asm hatch by an
earlier merged PR. Its own header says "NONMATCHING: hand-written asm, not
a C decompilation ... does NOT count as matched", which takes it out of
eligible.py's candidate set. It did not degrade, it was reclassified -- and
the reason-switching guard cannot tell those apart, which is exactly why
unsticking it has to be a deliberate decision and not a --update.

The other two entries are main's too, and neither has a correct fix:

  - func_02057410 (arm9) references _ll_udiv and _ull_mod, mwccarm's names
    for the 64-bit division helpers it emits for the (u64)v / (u64)pf[1] at
    func_02057410.c:304. The ROM's implementations are __aeabi_uldiv and
    __aeabi_ulmod in ITCM (0x01ffa9dc, 0x01ffa9e8), but check_references
    has no alias mechanism, and adding _ll_udiv as a second symbol at that
    address would create the duplicate-address condition that makes CI
    report lost matched functions.

  - _ZN5Whomp13InitResourcesEv references func_021135d4, which dsd resolves
    to module:overlays(9,14,15,16,18,19,20,22,24,25,26,27,29,32,36). All
    fifteen load at 0x021111a0, so that one instruction targets different
    data depending on which level is loaded and no single name is correct.

This file was generated on a clean origin/main checkout, so it carries
nothing from any feature branch. To verify: check out origin/main, run
tools/eligible.py, and regenerate -- the result is byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe
Both were declared as ordinary members and defined as forwarding
(this, &model, &file). They take two arguments and no this. Four
independent lines of evidence agree:

  - func_02046d50 and func_020470e8, the real bodies behind the two 0xc
    veneers, read only r0 and r1. r2 is never touched, which rules out a
    third argument.
  - the offsets r1 is read at are exactly the animation file's fields
    (BTP_File: 0x2, 0x4, 0x8, 0xc, 0x1c, 0x20), so r1 is the SECOND
    argument, not the third.
  - both matched implementations, src/func_02046d50.c and
    src/func_020470e8.c, already take two parameters.
  - every call site in the tree passes two.

The veneer bodies are pure tail jumps (ldr ip,[pc]; bx ip), so they
reproduce under either signature. That is how the wrong reading survived:
the definition files never constrained it, and the callee and callers --
which do -- were not consulted. A static member mangles identically, so no
symbol changes and no config moves.

TextureTransformer, the third sibling, was already corrected. Its header
names these two veneers as "called the same way" and notes that the old
reading "cost the PC port a wrong-register hunt"; the follow-up never
happened. This is it.

Twenty-two call sites drop the mangled extern for the member spelling.
Several of those externs were a bare `extern` on a mangled name inside a
//cpp file, which mangles a second time, so removing them retires that
latent defect as well. Eyerok's linkcheck improves BLIND-39 -> BLIND-35 as
four call destinations become resolvable. SnowmanHead and InvisibleSecret
declare the class locally rather than including TextureSequence.h: both
carry `typedef int Fix12;`, which collides with the real math/Fix12.h
template.

The six .c callers keep the mangled extern -- C has no ::.

Gates: rombuild 106/106 exact, 10,813 reproducing, 0 mismatching; eligible
name list byte-identical to main at 10813; linkcheck 23 checked, 21
verified, 0 blocking; attribution 0 changed / 0 lost; port_refcheck 393
resolve; header offsets, data definitions and duplicate sources clean.

Goomba::InitResources and MrI::InitResources do not reproduce -- before or
after. Both fail identically on origin/main (999 and 7 words), and their
compiled bytes are unchanged by this commit: 916 -> 916 and 664 -> 664,
identical in both cases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe
_ZN5Whomp13InitResourcesEv.cpp declares
_ZN16MeshColliderBase16UpdatePosAndAngs... with a bare `extern`, and that
declaration sits OUTSIDE the extern "C" block above it. In a //cpp file
that mangles the already-mangled name a second time, so the address taken
at the func_020393d4 call points at a _Z88_ZN16... that does not exist.

The byte gate cannot see this -- match.py compares relocated words as
wildcards, so the function reproduces either way. check_references and
prepush_linkcheck are the only gates that look at what a reference
targets, and both flagged it: linkcheck goes BLIND-1 -> VERIFIED with the
fix, and the function still reproduces.

Found while running the gates for the Prepare change, in a file that
change already touches. The other entry in the same check_references
report, func_02057410's _ll_udiv/_ull_mod, is unrelated to this branch
and reproduces identically on origin/main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe
tools/check_references.py --update, which is downward-only by construction.
Two names leave the backlog and nothing is added. Both were removed by the
two commits before this one:

  _Z49_ZN15TextureSequence7PrepareER8BMD_FileR8BTP_Fileii

      a double-mangled TextureSequence::Prepare. A call site declared the
      already-mangled ROM name with a bare `extern` inside a //cpp file, so
      it mangled a second time and the call targeted a symbol that does not
      exist. Converting the call sites to the static member spelling deleted
      the declaration and the bad reference with it -- the point of the
      Prepare change, showing up in the one gate that can see it.

  _Z88_ZN16MeshColliderBase16UpdatePosAndAngsERS_P5ActorR10ClsnResultR7Vector3P10Vector3_16S8_v

      the same defect in Whomp::InitResources, fixed by giving that
      declaration the extern "C" its neighbours already had.

The ratchet now refuses to let either name come back.

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

tangos-validator Bot commented Aug 11, 2026

Copy link
Copy Markdown

⚠️ PR validation — Error

worker error: pr_linkcheck exited 1 without a report: (1 slot(s))
ok src/_ZN3Amp8BehaviorEv.cpp (1 slot(s))
Traceback (most recent call last):
File "/home/validator/clone/tools/pr_linkcheck.py", line 418, in
main()
File "/home/validator/clone/tools/pr_linkcheck.py", line 342, in main
if w == "NO-REPRO" and asm_policy.has_draft_banner(text):
^^^^^^^^^^
NameError: name 'asm_policy' is not defined

Each changed src/*.c|*.cpp is compiled and its relocated bytes compared to the binary data on a private build box. Passing requires every changed file to reproduce the ROM byte-for-byte with correct relocation targets — this catches WRONG-DEST relocations and non-reproducing near-misses that ledger-scoped linkcheck skips.

andrewboudreau added a commit that referenced this pull request Aug 11, 2026
Since #1367 the PR validator has been unable to report on any PR that contains a
non-reproducing file. The draft-banner rule reads the module as `asm_policy` while
the import binds it as `AP`:

    import asm_policy as AP
    ...
    if w == "NO-REPRO" and asm_policy.has_draft_banner(text):

so the first NO-REPRO verdict raises NameError mid-loop. The worker then reports
"worker error: pr_linkcheck exited 1 without a report" with no verdict for any file,
including the ones it had already graded. PR #1390 is the first PR to hit it.

The rule is only reachable when a file FAILS, which is the one path a green CI run
never exercises -- so it was untestable where it sat, and untested. Both halves are
fixed here: the two source-text overrides move out of main() into `source_policy`,
and tools/test_pr_linkcheck_verdict.py drives them directly, including the collision
the old inline comment asserted could not happen (a bannered transcription grades
DRAFT, not RAW-ASM).

Behaviour is otherwise unchanged: the extracted function preserves the original
order, and a draft banner still does not excuse WRONG.

Confirmed with pyflakes in both directions -- one undefined name on main, zero across
all of tools/ with this applied. There is no lint step in CI today; adding one is a
separate change, but the baseline for it is clean.


Claude-Session: https://claude.ai/code/session_01XxDmkQ47fWa3GB6mj8xEQe

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant