Skip to content

docs(residuals): the real-PIF re-census refutes my own boot-path concern - #202

Merged
doublegate merged 2 commits into
mainfrom
docs/pif-census-result
Jul 29, 2026
Merged

docs(residuals): the real-PIF re-census refutes my own boot-path concern#202
doublegate merged 2 commits into
mainfrom
docs/pif-census-result

Conversation

@doublegate

Copy link
Copy Markdown
Owner

Ran the census the previous entry demanded. It refutes the concern I raised.

Result

Full corpus, both boot paths, 420 frames each, same two-term rule (≥1,000 RDP commands and ≥1,000 lit pixels):

=== 66 titles: HLE renders 29, real-PIF renders 29 ===
GAINED under real-PIF (0):
LOST under real-PIF (0):
real-PIF boot setup failed (0):

The boot path makes no difference to the render count, and the rendering set is identical.

So the "24 silent titles" figure is not an artefact of hle_boot. The warning I added one commit earlier — that it was "a count under the HLE boot path, not a count of emulation defects" — was overstated. It stays in the record as written (append-only), because the reasoning was sound and only the conclusion was wrong: DK64 is boot-path-sensitive, and generalising from one title to the corpus was the error.

What the paths actually differ on

HLE real-PIF
titles rendering 29 29
titles issuing zero RDP commands 24 25
zero only under one path World Driver Championship
  • DK64 stays boot-sensitive but still does not render. Under the real PIF it clears its check, programs VI_CTRL = 0x0320E and scans out 625×237 with ~13.5k lit pixels — versus never programming the VI at all under HLE. But zero RDP commands either way, so it fails the two-term rule on both paths. Lit pixels with no RDP work behind them is the Rayman 2 / Namco Museum signature; that frame needs the look-at test before it counts as progress.
  • World Driver Championship regresses under the real PIF: 176,085 RDP commands → zero. It rendered nothing on either path so the headline is unaffected, but it is a genuine defect in the real-PIF path and the one title where HLE is strictly better.
  • Everything else is timing noise — 0.7–1.0 % command-count deltas with identical lit-pixel totals (Majora's Mask 231,244 → 229,014; Banjo-Kazooie 189,526 → 187,725). The extra IPL1/IPL2 execution shifts the sample points.

Consequence for the plan

The 24 silent titles are genuinely silent under both boot paths, so each does need its own root-cause hunt. The shortcut I hoped for does not exist, and saying so is more useful than leaving a plausible caveat standing that would have shaped the next 23 investigations.

docs/STATUS.md's figures stand as written; the hle_boot caveat is withdrawn.

Gates

cargo fmt --all --check · cargo test --workspace · markdownlint — green. Docs only.

Ran the census through both boot paths over all 66 titles, 420 frames each,
same two-term rule:

  === 66 titles: HLE renders 29, real-PIF renders 29 ===
  GAINED under real-PIF (0):
  LOST under real-PIF (0):
  real-PIF boot setup failed (0):

The boot path makes no difference to the render count and the rendering set is
identical. So the "24 silent titles" figure is NOT an artefact of hle_boot, and
the warning I added in the previous commit -- that it was "a count under the HLE
boot path, not a count of emulation defects" -- was overstated. Retained above
as written per the append-only rule: the reasoning was sound, the conclusion was
wrong, and the error was generalising from one title to the corpus.

What the paths do differ on:

  titles rendering                 29  ->  29
  titles issuing zero RDP commands 24  ->  25
  zero only under one path             ->  World Driver Championship

DK64 remains boot-path-sensitive but still does not render: under the real PIF
it clears its check, programs VI_CTRL = 0x0320E and scans out 625x237 with ~13.5k
lit pixels, versus never programming the VI at all under HLE -- but it issues
zero RDP commands either way, so it fails the two-term rule on both paths. Lit
pixels with no RDP work behind them is the Rayman 2 / Namco Museum signature and
needs the look-at test before it counts as progress.

World Driver Championship REGRESSES under the real PIF: 176,085 RDP commands to
zero. It rendered nothing on either path so the headline count is unaffected,
but it is a real defect in the real-PIF path and the one title where HLE is
strictly better.

Everything else is timing noise -- 0.7-1.0% command-count deltas with identical
lit-pixel totals, i.e. the extra IPL1/IPL2 execution shifting sample points.

Consequence: the 24 silent titles are genuinely silent under both paths, so each
does need its own root-cause hunt. The hoped-for shortcut does not exist. The
hle_boot caveat is withdrawn and STATUS's figures stand as written.

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

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

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Documentation
    • Updated the R-18 record with results from a completed re-census across both boot paths.
    • Confirmed that rendering sets and render-count totals remain unchanged.
    • Clarified differences between boot paths, including timing-related noise and one titled regression.
    • Removed the outdated caveat and open action regarding the previously limited census.

Walkthrough

R-18 records a completed re-census across hle_boot and real_pif_boot, documents their measured differences, and withdraws the previous HLE-only caveat for the silent-title count.

Changes

R-18 boot-path re-census

Layer / File(s) Summary
Boot-path census results
docs/residuals/R-18.md
Records matching render-count totals across both boot paths, identifies the zero-RDP count divergence, and documents the DK64 and World Driver Championship observations.
Residual interpretation update
docs/residuals/R-18.md
States that the remaining 24 silent titles are silent under both boot paths, withdraws the earlier caveat, and confirms the docs/STATUS.md figures.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 8 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title uses the right Conventional Commits shape, but the subject is declarative rather than imperative as required. Rewrite it as an imperative Conventional Commit, for example: "docs(residuals): document the real-PIF re-census".
Measured, Never Tuned ⚠️ Warning FAIL: the new R-18 census numbers (e.g. 420 frames, 176,085 RDP cmds, 0x0320E/0x13046) are uncited and not recorded in docs/accuracy-ledger.md with measurement provenance. Add manual/wiki citations or docs/accuracy-ledger.md entries for each new measured value, stating how it was measured, or remove the unsupported numbers from docs/residuals/R-18.md.
✅ Passed checks (8 passed)
Check name Status Explanation
Description check ✅ Passed The description is clearly about the same re-census and resulting documentation update.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Oracle Number Is Stated ✅ Passed PASS — docs-only; R-18 says n64-systemtest impact: none and defers the oracle figure to docs/STATUS.md (suite-wide 90).
Docs-As-Spec Sync ✅ Passed PASS: HEAD only changes docs/residuals/R-18.md; no rustyn64-{cpu,rsp,rdp,audio,cart,core} code or matching docs/.md changed.
Changelog Entry For User-Visible Changes ✅ Passed PASS: The PR only changes docs/residuals/R-18.md; no user-facing code or CHANGELOG.md change, so the Unreleased rule does not apply.
Unsafe Stays Out Of The Chip Crates ✅ Passed PASS: diff is docs-only; no Rust sources changed, forbid(unsafe_code) remains in chip crates/core, and structural search found no unsafe blocks/impls/fns.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/residuals/R-18.md`:
- Around line 185-188: Qualify the attribution in the “Everything else is timing
noise” statement as an inference rather than an established cause. Retain the
measured command-count and lit-pixel data, but avoid asserting that extra
IPL1/IPL2 execution caused every delta unless the required correlated
differential evidence is documented.
- Around line 190-193: Update the silent-cohort conclusion in the “Consequence
for the plan” section to state that the original 24 common zero-RDP titles
remain unchanged, while World Driver Championship is an additional regression
affecting only real-PIF boot. Preserve the existing conclusion that each silent
title requires its own root-cause investigation and that the hle_boot caveat is
withdrawn.
- Line 149: Update the fenced code block at the indicated location in R-18.md by
adding the text language tag to its opening fence, preserving the block’s
contents.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 67dc2295-5653-451a-b41b-54090444fedb

📥 Commits

Reviewing files that changed from the base of the PR and between c1c610a and 950c371.

📒 Files selected for processing (1)
  • docs/residuals/R-18.md

Comment thread docs/residuals/R-18.md Outdated
Comment thread docs/residuals/R-18.md Outdated
Comment thread docs/residuals/R-18.md
Adopts three CodeRabbit findings and one from Antigravity.

The timing attribution was an inference presented as a finding. What is measured
is that the remaining deltas are 0.7-1.0% of the command count with identical
lit-pixel totals; that this is caused by extra IPL1/IPL2 execution shifting
sample points is the plausible reading, not something these runs isolate.
Establishing it needs a differential trace on one title, which has not been run.
Marked as inference, with the reason: "it is only timing" is exactly the kind of
dismissal that, taken on faith, hides a real per-title divergence inside a small
percentage.

The cohort arithmetic was ambiguous. Stated explicitly now: the original
24-title silent cohort is UNCHANGED -- all 24 issue zero RDP commands under both
paths and none left the cohort -- and the 25th is World Driver Championship, an
additional real-PIF-only regression rather than a member of that cohort.

WDC is now a tracked work item instead of "wants its own investigation", with
the method named.

MD040: the result fence is tagged `text`.

Rejected: "generalising" -> "generalizing". The repository is consistently
en-GB -- 37 "behaviour" to 3 "behavior", 41 "modelled" to 0 "modeled" -- so
en-GB is the convention and the suggestion inverts it.

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

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

Copy link
Copy Markdown
Owner Author

Adjudication — review round

CI 10/10. Three CodeRabbit findings adopted, one Antigravity finding adopted, one rejected on evidence.

Qualify the timing attribution — adopted, and it was the right catch

I wrote "Everything else is timing noise … which is the extra IPL1/IPL2 execution shifting the sample points". That is an inference stated as a finding. What these runs measure is that the deltas are 0.7–1.0 % with identical lit-pixel totals; the causal attribution is the plausible reading of that, not something isolated per title.

Rewritten to say so, with the reason recorded: "it is only timing" is exactly the kind of dismissal that, taken on faith, hides a real per-title divergence inside a small percentage. Establishing the cause would need a differential trace on one title, which has not been run — and saying that is more useful than a confident sentence.

Clarify the silent-cohort conclusion — adopted

The arithmetic was genuinely ambiguous: 24 → 25 could read as "the cohort grew". It did not. Now explicit:

the original 24-title silent cohort is unchanged — every one of those 24 issues zero RDP commands under both paths, and no title left the cohort. The 25th is World Driver Championship, an additional real-PIF-only regression, not a member of the original cohort.

MD040 on the fenced block — adopted, tagged text.

Antigravity: track the WDC regression — adopted

Fair; "wants its own investigation" is how findings evaporate. It is now a tracked work item, with the method named (dump what retires under real-PIF, find where it diverges from the HLE run, read what the branch tested — the approach that worked for SM64 and DK64).

Antigravity: "generalising" → "generalizing" — rejected, it inverts the convention

The repository is consistently en-GB, measured rather than assumed:

behaviour  37   |  behavior  3
modelled   41   |  modeled   0

So -ise/-lled is the house style and the suggestion would introduce the inconsistency it is trying to remove.

@github-actions

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This PR updates docs/residuals/R-18.md with results from a re-census comparing HLE and real-PIF boot paths across 66 titles, confirming that the silent-title count was not an artifact of HLE boot.

Blocking issues

None found.

Suggestions

  • docs/residuals/R-18.md:186: You state that the World Driver Championship regression is "tracked as its own work item", but do not cite an issue ID or task link. Add the specific tracker reference so this note remains actionable.
  • docs/residuals/R-18.md:204: If docs/STATUS.md contains any explicit warnings or caveats regarding the HLE boot path count, update or remove them in this PR to prevent outdated documentation from remaining in tree.

Nitpicks

  • docs/residuals/R-18.md:144: Avoid using ALL CAPS ("RUN") in section headings.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
docs/residuals/R-18.md (3)

146-154: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Add reproducible provenance for the census.

The 66-title/420-frame results are presented as measured, but the record does not identify the corpus revision, exact harness invocation, or retained output/trace used to produce them. Add that provenance so the counts can be independently checked.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/residuals/R-18.md` around lines 146 - 154, Add reproducible provenance
to the census section in R-18.md: identify the exact corpus revision, provide
the complete harness invocation for both boot paths, and reference the retained
output or trace artifact used to generate the 66-title/420-frame counts. Keep
the reported results unchanged.

Sources: Coding guidelines, Path instructions


156-163: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve superseded material as historical.

Residual records are append-mostly: retain the earlier caveat and tracked re-run action, mark them as historical/initial investigation state, and append the dated resolution. “Retained above as written” does not provide that historical label, and the new consequence replaces the previous OPEN ACTION.

Also applies to: 204-207

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/residuals/R-18.md` around lines 156 - 163, Update the residual record
around the retained caveat and tracked re-run action, including the
corresponding section at lines 204-207, to label both as “historical/initial
investigation state.” Append the dated resolution after the superseded material,
and replace the previous OPEN ACTION with the new consequence while preserving
the earlier text unchanged.

Sources: Path instructions, Learnings


185-192: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Call this an observed boot-path regression, not a real defect. Per the discrepancy rule in the coding guidelines, it needs the reference-emulator result and reference-independence note before it can be labelled a defect.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/residuals/R-18.md` around lines 185 - 192, Revise the World Driver
Championship entry to classify the result as an observed boot-path regression
rather than a real defect. Remove the current defect wording and standalone
work-item framing, and add the reference-emulator result plus a
reference-independence note required by the discrepancy rule.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/residuals/R-18.md`:
- Around line 193-202: Update the measured command-count comparison in the
residuals discussion to explicitly classify the delta as absolute or
differential, while retaining the existing statement that no differential trace
has been run. Keep the timing attribution clearly labeled as an inference.

---

Outside diff comments:
In `@docs/residuals/R-18.md`:
- Around line 146-154: Add reproducible provenance to the census section in
R-18.md: identify the exact corpus revision, provide the complete harness
invocation for both boot paths, and reference the retained output or trace
artifact used to generate the 66-title/420-frame counts. Keep the reported
results unchanged.
- Around line 156-163: Update the residual record around the retained caveat and
tracked re-run action, including the corresponding section at lines 204-207, to
label both as “historical/initial investigation state.” Append the dated
resolution after the superseded material, and replace the previous OPEN ACTION
with the new consequence while preserving the earlier text unchanged.
- Around line 185-192: Revise the World Driver Championship entry to classify
the result as an observed boot-path regression rather than a real defect. Remove
the current defect wording and standalone work-item framing, and add the
reference-emulator result plus a reference-independence note required by the
discrepancy rule.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0223f549-e50f-4449-8c7b-d320d7f60989

📥 Commits

Reviewing files that changed from the base of the PR and between 950c371 and f366a1a.

📒 Files selected for processing (1)
  • docs/residuals/R-18.md

Comment thread docs/residuals/R-18.md
@doublegate
doublegate merged commit a900da7 into main Jul 29, 2026
11 checks passed
@doublegate
doublegate deleted the docs/pif-census-result branch July 29, 2026 23:44
doublegate added a commit that referenced this pull request Jul 30, 2026
* chore: write the repo in en-US, and gate it so it stays that way

The project standard is en-US. The tree had drifted to en-GB -- and I made that
worse: on #202 I rejected a reviewer's generalising -> generalizing suggestion
on the measured grounds that the repo was consistently en-GB (37 "behaviour" to
3). The measurement was right and the conclusion was backwards: consistency with
a drift is not a reason to keep drifting.

1,505 replacements across 131 files, from 51 stem-level pairs applied
case-preservingly, so every inflection of a stem is covered by one entry.
Touches prose, doc comments, assertion messages, and private test-function
names.

NO public item is renamed, so there is no API change -- checked by enumerating
`pub fn|struct|enum|const|static|trait|type|mod` names against the stem list and
finding none. Also checked in advance:

- no CI job or script filters on an affected test name
- no excluded directory cites an affected test name, so swept and unswept text
  cannot desync
- no verbatim external quotation is altered (searched for quoted phrases beside
  a citation marker; the one hit was our own italic emphasis, not a quote)

Excluded trees, and why:
  ref-docs/       immutable research corpus -- corrections land as new dated
                  supplemental files, never in-place rewrites (module 40)
  n64brew_wiki/   CC BY-SA mirror, quoted verbatim by design
  ref-proj/       study clones of other emulators
  third_party/    vendored libdragon; not our prose to edit

Two literals are preserved because they are values, not words: `lightgrey` in a
shields.io badge URL (a color PARAMETER), and GitHub Actions' own `cancelled`
run status where the CHANGELOG quotes it. Stems whose form is correct in en-US
too were deliberately left out of the list -- analysis, synthesis, hypothesis,
peripheral, exercise, precise, premise, promise, otherwise, likewise, bitwise,
controlled, installed, stalled -- since a stem there would corrupt a correct
word.

Adds scripts/check_en_us.sh and an `en-US spelling` CI job, because a one-off
sweep decays: nothing fails when one en-GB form returns, so it survives review
and the next has precedent. This repo already demonstrates the pattern --
markdownlint runs in pre-commit only, and an MD040 violation sat unnoticed on
main until #203. The gate is mutation-checked in BOTH directions (fails on the
unswept tree at 1,355 lines, passes on the swept one at 443 files), strips
allowed literals rather than skipping their whole line so a permitted value
cannot shield a real hit beside it, and documents a per-line `spell-exempt`
escape hatch. Also wired into pre-commit for a faster local loop, with CI as the
unskippable gate.

Gates: fmt, clippy -D warnings, cargo test --workspace, rustdoc -D warnings,
no_std thumbv7em, markdownlint, check_no_roms, check_en_us -- all green.

* fix(en-us): repair 4 malformed words the sweep produced, and gate the class

Adjudicates the Antigravity review on #204, which caught one of four
corruptions. A before/after word-pair audit over the whole diff caught all
four, and it is the check I should have run before committing the sweep:

  characteris -> characteriz   corrupted  characteristics -> characteriztics
  optimis     -> optimiz       corrupted  optimistic      -> optimiztic
  realis      -> realiz        corrupted  realistically   -> realiztically
  centre      -> center        corrupted  centred         -> centerd

All four are stems matching INSIDE a word already correct in en-US. The gate
passed on the corrupted tree, and correctly so -- the outputs are not en-GB
either, so an en-GB pattern cannot see them.

The same defect was in the gate itself, in both directions: bare -is stems made
it report `characteristics`, `optimistic` and `realistically` as violations.
Split the stem list in two -- PLAIN_STEMS matched as substrings, ISE_STEMS
requiring an en-GB verb/noun ending -- so `optimis+tic` no longer matches while
`optimis+ation` still does.

Also:

- Added a MALFORMED backstop list so these specific non-words cannot return,
  with a comment saying plainly that it is a backstop for forms already seen and
  NOT a substitute for auditing a future sweep's word pairs.
- Added `cancelled`/`cancelling` and `centring` as stems. Their omission left
  real en-GB spellings unswept -- `the_sign_of_a_cancelled_zero_...` (renamed)
  and two prose sites -- while the value references keep the backticked
  `cancelled` form that names GitHub Actions' literal status.
- `centring` and `cancelling` are listed separately because neither CONTAINS its
  sibling stem, which is how a stem list silently under-matches.

The gate is now mutation-checked three ways: it catches an injected en-GB word,
catches an injected malformed word, and does NOT flag optimistic /
realistically / characteristics / synthesis / analysis.

Antigravity also reported scripts/check_en_us.sh as missing from the patch;
rejected as incorrect -- `git ls-files` lists it and the `en-US spelling` CI job
PASSED on this PR, which is impossible if the script were absent.

Gates: fmt, clippy -D warnings, cargo test --workspace, rustdoc -D warnings,
no_std thumbv7em, markdownlint, check_no_roms, check_en_us -- all green.
doublegate added a commit that referenced this pull request Jul 30, 2026
…etracts a claim

The 66-title census under `real_pif_boot` came back **29 render / 24 zero-command**,
against 29/25 before the fix. `hle_boot` is also 29/24, so the two paths now agree
EXACTLY and no title is boot-path sensitive in that partition any more. The sole
regression the #202 census found is closed.

RETRACTION, and it matters more than the census: #205 and R-16 said WDC gains "a
real 625x237 frame". That was wrong. The scratch probe printed
`scanout_scaled`'s DIMENSIONS and never counted content, and I read dimensions as
a picture -- R-18's lit-pixel lesson run in reverse, inferring a frame from
geometry instead of from what is in it.

Measured properly: WDC issues 175,815 RDP commands (from 0, near-parity with
176,085 under HLE) and lights ZERO pixels at every 60-frame sample. The VI is
genuinely programmed -- the single-shot 625x237 establishes that, so it is a real
black frame, not a blanked VI -- and R-18 already recorded the same shape under
HLE: "45 commands and exactly one distinct value (0x0001) -- it clears and draws
nothing."

So the correct claim is narrow: the AI livelock is removed and WDC's real-PIF
behavior now MATCHES its HLE behavior, submitting a full command stream and
rasterizing black on both. It does not render, and this fix never made it render.
Why the zero-command count moves while the render count does not: WDC clears the
1,000-command floor and fails the 1,000-lit-pixel floor, which is the two-term
rule working as designed.

The 24-title silent cohort is UNCHANGED and still needs per-title root-causing.
What closed here is the 25th.

Gates: fmt, clippy -D warnings, cargo test --workspace, rustdoc -D warnings,
markdownlint, check_no_roms, check_en_us -- all green.
doublegate added a commit that referenced this pull request Jul 30, 2026
* fix(ai): an unprogrammed AI_DACRATE must not stop the DAC (R-16)

`AI_STATUS.FULL` could latch and never clear, so a game polling it for a free
audio DMA slot spun forever.

Found by root-causing World Driver Championship, the one title the boot-path
census newly implicated: 176,085 RDP commands under `hle_boot` and ZERO under
`real_pif_boot`. Measured rather than guessed -- the dominant retiring
instruction under real-PIF is

    0x8007a8a0  LUI  t6, 0xA450      ; the AI register block
    0x8007a8a4  LW   a0, 0xC(t6)     ; AI_STATUS                    <- dominant
    0x8007a8b0  AND  t7, a0, at      ; test bit 31 (FULL)
    0x8007a8b4  BEQ  t7, zero, +3    ; exits only when FULL is clear

and counting FULL *transitions* rather than sampling the bit -- the sticky-
register lesson from this ledger row's own Cause.ExcCode mistake -- settles it:
over 300 frames the flag transitions 48 times under HLE and exactly ONCE under
real-PIF, latching at ~frame 30 and never clearing again.

Mechanism: `recompute_rate` mapped an unprogrammed `AI_DACRATE` to
`sample_rate = 0`, `period_ticks()` to 0, and `tick()` then returned BEFORE
`emit_sample` -- which is the only place a drained transfer is retired. A
stopped DAC therefore makes the two-deep FIFO unable to advance, and
`FULL` (`dma_count > 1`) becomes permanent. WDC queues two buffers and polls
FULL before programming the DAC, so it sat exactly in that window.

The fix invents nothing. Hardware has no stopped-DAC state to model -- the DAC
counter runs off the video clock from power-on whatever `AI_DACRATE` holds --
and ares (ISC, vendorable, so readable per ref-proj/README.md) makes the same
choice structurally: `AI::power()` sets `dac.frequency = 44100` and `AI::main()`
calls `sample()` unconditionally, so its equivalent retirement block runs from
power-on. `DEFAULT_DAC_HZ = 44_100` is taken from there and is labeled a
MODELING DEFAULT, not a measured value: `AI_DACRATE`'s reset value is not
documented in anything this project mirrors. Nothing observable should depend on
the exact number, because every title programs the register before it plays
anything.

The old zero-gate was avoiding a real failure -- `dac_rate == 0` computing
`video_clock / 1` ~= 48 MHz and flooding the sink -- and a default rate avoids
both that and the latch.

`Audio::new()` had to change too, and the reason is worth recording: it
initialized `sample_rate: 0` literally, and `recompute_rate` only runs on a
DACRATE or region write, so the first version of this fix left a machine that
never programmed the AI with a stopped DAC anyway. The defect survived its own
fix until the constructor matched.

Measured effect: WDC under `real_pif_boot` goes 0 -> 120,015 RDP commands and
from `scanout 0x0` to a real 625x237 frame; FULL transitions 1 -> 46. Full
workspace suite 810 pass, so the Phase-4 golden PCM stream is unaffected.

Pinned by `full_clears_even_when_dacrate_was_never_programmed`, mutation-checked
(restoring either half of the fix turns it red). One pre-existing test was
CORRECTED rather than deleted: `set_region_before_dacrate_keeps_rate_zero`
asserted the rate was exactly 0, which over-specified its own stated purpose --
its comment says it exists to prevent a ~48 MHz rate, and that protection is
kept as an order-of-magnitude bound in the renamed
`set_region_before_dacrate_does_not_fabricate_the_video_clock_rate`.

Still open and NOT claimed fixed: WDC now renders but ends its run in a `B -1`
self-loop at 0x8000_28C0, a different halt reached after 120k commands. This
closes the AI livelock, not that title.

n64-systemtest impact: none -- the suite has no AI coverage, so the count stays
at 90. Validated by ares' structure, the unit test, and WDC's unblocking.

Gates: fmt, clippy -D warnings, cargo test --workspace, rustdoc -D warnings,
no_std thumbv7em, markdownlint, check_no_roms, check_en_us -- all green.

* docs(r16): adjudicate review — ledger the DACRATE-zero conflation

Adopts the Antigravity review on #205. No blocking issues; three items.

1. `dac_rate == 0` conflates "never programmed" with "software wrote 0".
   CORRECT, and confirmed against the oracle: ares keeps the two apart —
   `AI::power()` sets 44100 while its AI_DACRATE write honors a literal zero
   (`dac.frequency = max(1, system.videoFrequency() / (io.dacRate + 1))`,
   ares/n64/ai/io.cpp:61), so a written 0 asks for a ~48 MHz DAC and ares gives
   it.

   NOT fixed here, deliberately. Telling the two apart needs a
   `dac_rate_programmed` flag, which adds a field to a serialized struct and so
   changes the save-state layout (ADR 0005) — an announced-in-advance change,
   not one to make in passing while fixing an unrelated livelock. It is also
   unobservable: no title requests a ~48 MHz DAC. Recorded in R-16 and in a code
   comment, with the exact ares line fidelity would require, rather than left as
   an undocumented conflation.

2. Raw register indices and bitmasks in the new test. REJECTED with reason: the
   crate has no symbolic register constants and every existing test in the module
   uses raw indices (`ai.write_reg(1, 0x40)`), so the new test matches the
   surrounding convention. Introducing named constants is worth doing, but as one
   change across the whole module rather than in two lines of a bug-fix PR, where
   it would read as unrelated churn.

3. Assert the exact constant rather than only a bound. ADOPTED, kept alongside
   the bound, because the two catch different things: the equality pins the
   WIRING (that `recompute_rate` reads the named constant instead of an inlined
   literal that could drift), while the bound pins the PROPERTY and is what
   catches the failure class the test exists for — a rate derived from the video
   clock rather than an audio clock.

Also records the corpus census result, which is negative and easy to omit: the
66-title census is UNCHANGED after the fix at 29 render / 24 silent, so no other
silent title shared this latch. That census boots through `hle_boot`, where WDC
already rendered, so it cannot show the improvement it was launched to measure —
foreseeable before the run, and stated rather than dropped.

Gates: fmt, clippy -D warnings, cargo test --workspace, rustdoc -D warnings,
markdownlint, check_no_roms, check_en_us — all green.

* docs(r16): the real-PIF census closes the boot-path divergence, and retracts a claim

The 66-title census under `real_pif_boot` came back **29 render / 24 zero-command**,
against 29/25 before the fix. `hle_boot` is also 29/24, so the two paths now agree
EXACTLY and no title is boot-path sensitive in that partition any more. The sole
regression the #202 census found is closed.

RETRACTION, and it matters more than the census: #205 and R-16 said WDC gains "a
real 625x237 frame". That was wrong. The scratch probe printed
`scanout_scaled`'s DIMENSIONS and never counted content, and I read dimensions as
a picture -- R-18's lit-pixel lesson run in reverse, inferring a frame from
geometry instead of from what is in it.

Measured properly: WDC issues 175,815 RDP commands (from 0, near-parity with
176,085 under HLE) and lights ZERO pixels at every 60-frame sample. The VI is
genuinely programmed -- the single-shot 625x237 establishes that, so it is a real
black frame, not a blanked VI -- and R-18 already recorded the same shape under
HLE: "45 commands and exactly one distinct value (0x0001) -- it clears and draws
nothing."

So the correct claim is narrow: the AI livelock is removed and WDC's real-PIF
behavior now MATCHES its HLE behavior, submitting a full command stream and
rasterizing black on both. It does not render, and this fix never made it render.
Why the zero-command count moves while the render count does not: WDC clears the
1,000-command floor and fails the 1,000-lit-pixel floor, which is the two-term
rule working as designed.

The 24-title silent cohort is UNCHANGED and still needs per-title root-causing.
What closed here is the 25th.

Gates: fmt, clippy -D warnings, cargo test --workspace, rustdoc -D warnings,
markdownlint, check_no_roms, check_en_us -- all green.

* fix(ai): adjudicate 7 review findings — two of my tests were vacuous

CodeRabbit posted seven inline findings on #205; five adopted, two rejected with
reasons. The two that mattered were real defects in my own tests.

VACUITY (the sharpest finding, adopted). My new
`set_region_before_dacrate_...` asserted `emitted < 5_000` after a SINGLE `tick`
-- but the first tick returns at the `next_sample_tick == 0` anchor branch, so
`emitted` was ALWAYS 0 and the bound passed with the emission path completely
broken. Fixed by priming the clock and making the bound two-sided: only the pair
is evidence, since an upper bound alone accepts a DAC that emits nothing, which
is precisely the state that caused this livelock.

Chasing that found the SAME defect in a PRE-EXISTING test, which the review did
not flag: `idle_tick_emits_nothing_before_dacrate` also called `tick` once, so it
too passed at the anchor branch regardless of the rate -- it passed identically
before and after a change to the exact behavior it claimed to pin. Renamed to
`the_first_tick_only_anchors_the_sample_clock`, re-aimed at the rule it actually
exercises, and given a second half asserting that the NEXT tick does emit,
without which it would pass against a DAC that never emits at all.

All three audio tests are now mutation-checked together: reverting either half of
the fix turns all three red.

STALE CONTRACT (adopted). `sample_rate`, `period_ticks` and `tick` still
documented an unset rate as zero/stopped, which the fix made false. Synchronized
-- and the `period == 0` early return is now labeled as a divide-by-zero guard
rather than a modeled DAC state, which is what it actually is.

docs/audio.md (adopted; a rule I broke). "A chip change touches the chip code AND
its docs/<chip>.md in the same commit." It did not. Added a section covering the
reset model, why it is a CORRECTNESS requirement and not an audio nicety, the
WDC evidence, and the known DACRATE=0 simplification.

PROVENANCE (adopted). The comment asserted "hardware has no stopped-DAC state" as
fact while admitting two paragraphs later that reset semantics are undocumented.
Now split explicitly into ESTABLISHED (ares runs its DAC from power-on),
INFERRED (a divider has no "off" encoding, so hardware likely matches), and NOT
ESTABLISHED (what AI_DACRATE holds at reset).

n64-systemtest wording (adopted): "impact: none" reads as a measurement that came
back clean. It is "not measured, and it cannot be" -- the suite has zero AI
coverage. Stated that way in both the ledger and the CHANGELOG.

R-18 historical marker (adopted): a header now states that everything under
`## Status` is append-only investigation history containing retracted claims, so
a paragraph read in isolation is not mistaken for current status.

REJECTED, with reasons:

- `DEFAULT_DAC_HZ` -> `pub(crate)` "in case a diagnostic tool needs it": nothing
  outside the crate reads it. Widening visibility on speculation is the inert-API
  hazard docs/engineering-lessons.md §3.2 describes.
- Repurposing spare bits in existing serialized state to smuggle in
  `dac_rate_programmed` without changing the layout: worse than the honest
  alternative. It makes the save-state format misdescribe itself, and the
  simplification is already ledgered with the ares line fidelity would require.

Gates: fmt, clippy -D warnings, cargo test --workspace, rustdoc -D warnings,
no_std thumbv7em, markdownlint, check_no_roms, check_en_us -- all green.

* refactor(ai): derive the power-on rate through recompute_rate, not a literal

Adopts all three items from the second Antigravity review on #205.

1. `new()` duplicated `recompute_rate`'s `dac_rate == 0` decision as a literal,
   kept correct only by a comment saying "must match `recompute_rate`" -- exactly
   the comment-enforced invariant this project distrusts, and the drift would be
   silent: a constructor left at 0 puts the machine back in the stopped-DAC state
   R-16's livelock needs, and no test of a *programmed* DAC would notice. `new()`
   now calls `recompute_rate()` (both are `const fn`, so this stays const).

   This measurably improves the tests: a SINGLE-POINT mutation of
   `recompute_rate` alone now fails four tests, where before the same defect
   needed both halves reverted before anything went red.

2. Added `TODO(T-AUDIO-01)` so the deferred AI_DACRATE=0 separation is
   discoverable from the code, not only from ledger R-16 -- it needs a
   save-state layout bump (ADR 0005), which is why it is deferred.

3. Added `debug_assert!(period > 0)` beside the `period == 0` guard. The comment
   claimed the state is unreachable on a constructed machine; the assert makes
   that claim CHECKABLE rather than merely stated, and a future change that
   reintroduces a zero rate is precisely the R-16 defect -- which hid the first
   time behind exactly this silent `return`.

Gates: fmt, clippy -D warnings, cargo test --workspace, rustdoc -D warnings,
markdownlint, check_en_us -- all green.
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