Skip to content

docs(ledger): localise Banjo-Tooie to a two-instruction loop (R-18) - #188

Merged
doublegate merged 2 commits into
mainfrom
docs/r18-banjo-tooie-lead
Jul 29, 2026
Merged

docs(ledger): localise Banjo-Tooie to a two-instruction loop (R-18)#188
doublegate merged 2 commits into
mainfrom
docs/r18-banjo-tooie-lead

Conversation

@doublegate

Copy link
Copy Markdown
Owner

Docs-only. Records where Banjo-Tooie actually stops, plus a sampling artefact caught on the way.

The finding

Banjo-Tooie's CPU is executing — retired advances by a perfectly constant 781,250 per frame (exactly one instruction per 2 CPU cycles) — while the fetch PC never leaves two instructions:

0x800329A4  42000018  ERET
0x800329A8  0C00BA70  JAL 0x8002E9C0     <- 50% of samples
0x800329AC  00002025  OR a0,zero,zero    <- 50% of samples (delay slot)

At step = 2 master ticks (one CPU cycle, the finest granularity there is) those are the only two PCs across 20,000 samples. No exception vector appears, so nothing is faulting — this is not an exception loop.

A sampling artefact, recorded because the correction matters

The first measurement sampled every 400 master ticks (~100 instructions) and reported one distinct PC over 60,000 samples — which reads as a hard deadlock, and I reported it that way. The sample period was commensurate with the loop, so it landed on the same instruction every time.

step (master ticks) distinct PCs
400 1 ← aliased
397, 1009, 2, 3, 7 2, exactly 50/50

Same class as "a test whose success and failure paths converge".

Evidence, stated without a cause attached

  1. Control never reaches 0x8002E9C0, which does contain a valid prologue (ADDIU sp,sp,-0x38 / SW ra,0x24(sp) / …) — the target is loaded; the transfer does not happen.
  2. ra = 0xFFFF_FFFF_8003_29A8 — the JAL's own address. MIPS requires r31 = PC + 8 = 0x8003_29B0. A concrete discrepancy against the architecture, not an interpretation.
  3. The preceding instruction is ERET, whose successor is normally unreachable — so arriving at 0x8003_29A8 is itself suspect.
  4. The RSP is halted throughout.

Caveat kept deliberately: Cpu::pc is the fetch PC of a five-stage pipeline (ADR 0007), not the retiring position.

Both obvious candidates were weakened by checking, not theorising

jal_links_past_the_delay_slot_and_jr_returns_there, a_jal_in_a_delay_slot_links_past_the_outer_target, and eret_resumes_at_epc_and_has_no_delay_slot are already green, and n64-systemtest's CPU category passes at Failed: 0.

So neither JAL nor ERET is simply broken — the behaviour is conditional on state those unit tests do not set up, which is a materially different search than "fix JAL". Next step is to measure which instruction actually retires at 0x8003_29A8; the fetch PC cannot distinguish "the JAL does not transfer" from "something else is fetched there", and the I-cache is an untested intermediary (CACHE Index/Hit is already a recorded instance of a comment disagreeing with its code).

Naming a cause before measuring its boundary has been wrong four times in this ledger, so this lands as a localisation with its caveats, not a diagnosis.

Gates

cargo fmt --all --check · cargo test --workspace · pre-commit run markdownlint --all-files — green. Docs only, no behaviour change.

Banjo-Tooie's CPU is executing -- retired advances by a perfectly constant
781,250 per frame -- while the fetch PC never leaves two instructions:
0x800329A8 (JAL 0x8002E9C0) and its delay slot. No exception vector appears at
one-cycle sampling, so nothing is faulting.

The first measurement of this was a sampling artefact and is recorded as such.
Sampling every 400 master ticks (~100 instructions) reported ONE distinct PC
over 60,000 samples, which reads as a deadlock; the period was commensurate
with the loop. Re-sampled at coprime periods and at one CPU cycle, the answer
is stable and different: exactly two PCs, 50/50.

Evidence recorded without a cause attached: control never reaches the target,
which does contain a valid function prologue; ra holds the JAL's own address
rather than the architecturally required PC+8; the preceding instruction is
ERET, whose successor should be unreachable; the RSP is halted throughout.

Both obvious candidates were then weakened by checking rather than
theorising. jal_links_past_the_delay_slot_and_jr_returns_there and
eret_resumes_at_epc_and_has_no_delay_slot are already green, and
n64-systemtest's CPU category passes at Failed: 0. So neither instruction is
simply broken; the behaviour is conditional on state those tests do not set
up, which is a different search than "fix JAL".

Naming a cause before measuring its boundary has been wrong four times in this
ledger, so this lands as a localisation with its caveats, not a diagnosis.

Gates: fmt, cargo test --workspace, markdownlint -- green. Docs only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@doublegate, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 8 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c496e8a0-e413-4c92-a60e-6051f3cb0722

📥 Commits

Reviewing files that changed from the base of the PR and between 0629422 and 0c7ae04.

📒 Files selected for processing (1)
  • docs/accuracy-ledger.md

Comment @coderabbitai help to get the list of available commands.

… (R-18)

Answers the open question this PR left, and retracts one of its own claims.

Dumping all four inter-stage latches at one-cycle granularity: NOTHING retires
at 0x800329A8. The JAL enters IC/RF and is discarded there, never reaching
RF/EX. The only instruction to reach DC/WB in 400 cycles is the ERET at
0x800329A4, 200 times -- once every two cycles, which is exactly the constant
781,250/frame retirement rate.

EPC = 0xFFFFFFFF800329A4, the ERET's own address. With Status = 0x0000FF01
(EXL=0, ERL=0), ERET correctly takes the ERL=0 path and sets PC <- EPC,
landing back on itself. So ERET is behaving exactly to specification; the
defect is that EPC points at the ERET. Cause.ExcCode is 0 and EXL is clear, so
no exception is taken in the loop -- the corruption happens once at frame 12
and the machine then settles into a deterministic self-loop.

Retracted: this PR recorded "ra holds the JAL's own address rather than the
required PC+8" as a concrete architectural discrepancy. The JAL never
executes, so ra was never its business and the observation carried no
information. Struck rather than deleted, since the reasoning it invited is
worth seeing.

Eliminated: the I-cache hypothesis. The fetched words match RDRAM exactly.

Remaining: what wrote that EPC. Two candidate sources -- libultra's dispatcher
writes EPC via MTC0 before its ERET, or an exception was taken on the ERET
itself, which sets EPC to the ERET's address. One probe distinguishes them.

Gates: fmt, cargo test --workspace, markdownlint -- green. Docs only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@doublegate

Copy link
Copy Markdown
Owner Author

Adjudication — Antigravity review, plus the open question is now answered

The measurement this PR left open

Nothing retires at 0x8003_29A8. Dumping all four inter-stage latches at one-cycle granularity:

cyc0  fetch=0x800329AC | IC/RF JAL@29A8 | RF/EX ERET@29A4 | DC/WB ERET@29A4
cyc1  fetch=0x800329A8 | IC/RF ERET@29A4 | EX/DC ERET@29A4 | DC/WB ----
cyc2  (identical to cyc0, forever)

instructions reaching DC/WB over 400 cycles:
  pc=0x800329A4 word=0x42000018  ERET  n=200

The JAL enters IC/RF and is discarded there, never reaching RF/EX. The only instruction that commits is the ERET, 200 times in 400 cycles — exactly the constant 781,250/frame rate.

EPC = 0xFFFF_FFFF_8003_29A4, the ERET's own address. With Status = 0x0000_FF01 (EXL=0, ERL=0), ERET correctly takes the ERL=0 path and sets PC <- EPC, landing on itself. ERET is behaving exactly to specification — the defect is that EPC points at the ERET.

Retracted from this PR: I recorded "ra holds the JAL's own address rather than the required PC+8" as a concrete architectural discrepancy. The JAL never executes, so ra was never its business. Struck rather than deleted, since the reasoning it invited is worth seeing. Eliminated: the I-cache hypothesis — fetched words match RDRAM exactly.

Suggestion: split the investigation log out of the ledger cell — REJECTED, with the reason

Fair observation, and I agree the cell is long. Rejecting because it contradicts a deliberate structure here:

  • docs/accuracy-ledger.md is defined as the place where a residual's provenance lives, and its rows are treated as append-only — superseded findings are struck or marked historical in place, never rewritten (see R-5, which carries an explicit [HISTORICAL BASELINE — SUPERSEDED] marker for the same reason). Moving the refutations elsewhere would break the one property that makes the row trustworthy: that every claim ever made about R-18, including the wrong ones, is visible next to the claim that replaced it.
  • The refutations are not session notes — they are the falsification record. This PR is a live example: the ra observation above would read as a solid architectural finding if the retraction lived in a separate document that a future reader might not open.
  • An issue thread is the wrong home: ref-docs/ and the ledger are the durable references, and issues are not part of the repo's doc-as-spec gate.

If the row later needs to be readable as a narrative I would rather extract all long residuals into docs/residuals/R-NN.md with the table linking to them, as one deliberate change — not migrate R-18 alone and leave the ledger inconsistent about where provenance lives.

Nitpick: dense paragraphs in a table cell hurt diffs — ACKNOWLEDGED, not acted on

True, and it is the real cost of the append-only choice above. Mitigated in practice by each append being a single-line change to one row, so the diff is noisy in width but not in line count. Worth revisiting under the docs/residuals/ split rather than piecemeal.

Gates re-run: fmt, cargo test --workspace, markdownlint — green.

@github-actions

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This documentation-only PR updates the entry for item R-18 in docs/accuracy-ledger.md to document diagnostic measurements localizing Banjo-Tooie's execution stall to an ERET self-loop targeting its own address via EPC.

Blocking issues

None found.

Suggestions

  • docs/accuracy-ledger.md:413: The newly added text appends ~4.5 KB of dense diagnostic prose as a single continuous line inside a Markdown table cell. This makes raw git diffs and table formatting very difficult to read and maintain. Consider using standard list items or <br> tags to structure multi-stage investigation findings into distinct logical blocks.
  • docs/accuracy-ledger.md:413: The update includes real-time retractions inline (e.g., ~~ra holds the JAL's own address...~~). Summarizing final empirical findings instead of retaining step-by-step intermediate hypotheses would make the accuracy ledger cleaner and more readable for future engineers.

Nitpicks

  • The PR title follows conventional commits and accurately reflects a documentation-only update; however, no companion test or probe script was checked in to preserve the single-cycle pipeline latch measurement setup used to diagnose EPC.

Automated first-pass review by agy on a self-hosted runner -- not a human review.

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