Skip to content

test(harness): assert the RSP criterion, which nothing did - #238

Merged
doublegate merged 2 commits into
mainfrom
perf/rsp-vu-hoist
Jul 31, 2026
Merged

test(harness): assert the RSP criterion, which nothing did#238
doublegate merged 2 commits into
mainfrom
perf/rsp-vu-hoist

Conversation

@doublegate

Copy link
Copy Markdown
Owner

Why this exists

I was asked for the single-family VU hoist experiment. #237 established — via review — that there is no gate for it to run against, so this is the prerequisite: an RSP-scoped n64-systemtest assertion.

Phase 2's cut criterion is Failed: 0 in the RSP categories. It was verified by hand at v0.3.0 and never asserted. phase_1_categories_report_no_failures excludes "RSP" and "SP " through its LATER_PHASES list, so it stays green through an arbitrarily broken vector unit.

docs/STATUS.md's own evidence column said so out loud — "dump per-test to confirm none are RSP-prefixed" — and nobody noticed that a manual dump is not a gate. I then cited that suite as a refactor's gate, and a reviewer checked.

Baseline

RSP categories: 0 failing across 224 RSP tests started
(suite ran to xioctl(EXIT); 90 failing suite-wide across 950 started).

The mutation check is the argument for the gate

Making VAND compute VOR (0x28 => s | t):

assertion result
rsp_categories_report_no_failures FAILED
phase_1_categories_report_no_failures still reports 0 failing

The old gate cannot see a broken VU; the new one can. That is not a hypothetical about coverage — it is the same suite, the same run, one defect, and two different verdicts. Restored afterwards, tree verified against the snapshot.

Shape

Same as the Phase 1 assertion and for the same reasons: output exists, the suite reached xioctl(EXIT), and — the part that matters — the RSP category itself started. That witness is not redundant with the suite-wide count: a change that stopped RSP tests starting would leave zero RSP failures, which is the vacuous-pass shape this project keeps re-encountering. The threshold is > 20 against an observed 224, loose on purpose — it exists to catch a category that did not run, not to pin the suite's size.

RSP_CATEGORIES is deliberately a narrower list than LATER_PHASES: "RSP" and "SP " only. The RDP, MI, VI, AI, PI, SI and cart entries belong to other subsystems and are not asserted here.

The runner's five-tuple return became a Report struct on the way — it was gaining a sixth field, at which point every call site is a positional puzzle and a swapped pair compiles.

Docs

docs/STATUS.md's Phase 2 row and docs/testing-strategy.md's oracle table now name the automated assertion instead of the manual procedure, and say why there are two tests rather than one.

Verification

One guarded conditional: cargo fmt --all --check; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace; RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps; scripts/check_en_us.sh; pre-commit run markdownlint --all-files; and both systemtest assertions green.

Next

With the gate in place, the single-family hoist experiment can run against it. That is the following PR, not this one.

🤖 Generated with Claude Code

Phase 2's cut criterion is `Failed: 0` in n64-systemtest's RSP categories. It was
verified by hand at v0.3.0 and NEVER ASSERTED: `phase_1_categories_report_no_
failures` excludes "RSP" and "SP " through its LATER_PHASES list, so it stays
green through an arbitrarily broken vector unit. docs/STATUS.md's own evidence
column said so out loud — "dump per-test to confirm none are RSP-prefixed" — and
nobody noticed that a manual dump is not a gate.

Found while sizing a vector-unit refactor: I cited that suite as the refactor's
gate, and a reviewer checked. It is not one.

`rsp_categories_report_no_failures` is the same shape as the Phase 1 assertion
and for the same reasons: output exists, the suite reached xioctl(EXIT), and —
the part that matters — the RSP category ITSELF started. That last witness is not
redundant with the suite-wide count: a change that stopped RSP tests starting
would leave zero RSP failures, which is the vacuous pass this project keeps
re-encountering.

BASELINE: 0 failing across 224 RSP tests started, suite to EXIT, 90 suite-wide
across 950.

MUTATION-CHECKED, and the result is the argument for the gate existing: making
VAND compute VOR (`0x28 => s | t`) fails `rsp_categories_report_no_failures`
while `phase_1_categories_report_no_failures` STILL REPORTS 0 FAILING. The old
gate cannot see a broken VU; the new one can. Restored afterwards and the tree
verified against the snapshot.

The runner's five-tuple return became a `Report` struct on the way — it was
gaining a sixth field, at which point every call site is a positional puzzle and
a swapped pair compiles.

docs/STATUS.md's Phase 2 row and docs/testing-strategy.md's oracle table now name
the automated assertion instead of the manual procedure, and say why there are
two tests rather than one.

Gates: fmt, clippy, test --workspace, rustdoc, en-US, markdownlint, and both
systemtest assertions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 2 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: a199500f-2070-4120-aaea-23edb962a3e3

📥 Commits

Reviewing files that changed from the base of the PR and between 26a0a30 and 2872768.

📒 Files selected for processing (1)
  • crates/rustyn64-test-harness/tests/systemtest.rs
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added dedicated reporting for RSP and SP system-test results.
    • Added coverage for tracking started, completed, and failed RSP tests.
  • Bug Fixes

    • Improved failure reporting by separating RSP/SP results from other test phases.
  • Documentation

    • Updated system-test status and testing guidance to reflect separate test gates and zero-failure requirements.

Walkthrough

The system-test harness now separates RSP/SP results from Phase 1 results. It returns a structured Report, counts started RSP tests, validates guest completion, and documents separate no-failure gates.

Changes

RSP test reporting

Layer / File(s) Summary
Structured RSP report
crates/rustyn64-test-harness/tests/systemtest.rs
The harness classifies RSP/SP categories, counts started RSP tests, collects RSP failures, and returns results through Report.
RSP gate and test criteria
crates/rustyn64-test-harness/tests/systemtest.rs, docs/STATUS.md, docs/testing-strategy.md
The ignored RSP test checks console output, guest completion, more than 20 started tests, and zero failures. Documentation records separate Phase 1 and RSP assertions.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 10
✅ Passed checks (10 passed)
Check name Status Explanation
Title check ✅ Passed The title uses an allowed type and scope, states the RSP assertion change, uses imperative wording, has no trailing period, and is 58 characters long.
Description check ✅ Passed The description directly explains the RSP assertion, the Report refactor, documentation updates, mutation check, and verification results.
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 The commit changes only the system-test harness and two documentation files; it does not change emulation behaviour, so the tooling/docs exception applies.
Docs-As-Spec Sync ✅ Passed The PR changes only the test harness and existing status/testing docs; it does not change rustyn64-cpu, -rsp, -rdp, -audio, -cart or -core behaviour.
Changelog Entry For User-Visible Changes ✅ Passed The commit changes only the test harness and testing/status documentation; it adds no runtime or public user-facing behaviour, so the CI/tooling-only exception applies.
Measured, Never Tuned ✅ Passed The diff changes only system-test parsing and documentation; its new RSP prefixes and >20 witness are test metadata, with no hardware constant, timing value, or emulator behaviour added.
Unsafe Stays Out Of The Chip Crates ✅ Passed The PR adds no unsafe Rust or forbid changes. Structural scans find no unsafe constructs, and all chip crate roots plus rustyn64-core retain #![forbid(unsafe_code)].

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

🤖 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 `@crates/rustyn64-test-harness/tests/systemtest.rs`:
- Around line 65-67: Update the rustdoc immediately above the run function to
document that it returns a Report, removing the stale tuple description and
mentioning the Report return type directly. Keep the run signature and
implementation unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 012b3993-19bd-49e2-a959-96f17bca01c2

📥 Commits

Reviewing files that changed from the base of the PR and between ab631c8 and 26a0a30.

📒 Files selected for processing (3)
  • crates/rustyn64-test-harness/tests/systemtest.rs
  • docs/STATUS.md
  • docs/testing-strategy.md

Comment thread crates/rustyn64-test-harness/tests/systemtest.rs Outdated
…eturns

Changing the return type to `Report` left its doc comment describing five
positional values, omitting the RSP failures and the rsp_started witness
entirely. A comment that asserts what the code does and disagrees with it is the
exact failure mode this repo tracks, and I introduced one in the same commit
that added the struct.

Rewritten to point at the struct's own field docs, with the reason for the
struct (a sixth field was arriving, and a swapped pair of positionals compiles)
and the reason one run serves both assertions (~25 s in --release; two runs
would double it for no added coverage).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This pull request introduces the rsp_categories_report_no_failures test to assert zero failures in RSP/SP categories and refactors run() to return a Report struct.

Blocking issues

  • Incorrect suite-wide failure reporting: In crates/rustyn64-test-harness/tests/systemtest.rs, failures is initialized to 0 but is never incremented inside for line in text.lines() when matching Test '<name>' failed:. Consequently, Report.failures will always evaluate to 0, producing inaccurate test failure counts in diagnostic log outputs and failure messages.

Suggestions

  • Combine redundant log iterations: In crates/rustyn64-test-harness/tests/systemtest.rs, text.lines() is iterated over three separate times to count started, count rsp_started, and collect failure lists. Consolidate these into a single loop over text.lines().

Nitpicks

  • crates/rustyn64-test-harness/tests/systemtest.rs:L129: rsp_started += ... uses compound addition immediately after initializing rsp_started to 0; use direct assignment (=).

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

@doublegate
doublegate merged commit 9b61916 into main Jul 31, 2026
12 checks passed
@doublegate
doublegate deleted the perf/rsp-vu-hoist branch July 31, 2026 20:10
doublegate added a commit that referenced this pull request Jul 31, 2026
…al (#239)

* docs(perf): the VU family hoist — built, accurate, and measured NEUTRAL

The experiment the SIMD section asked for, run against the RSP gate added in
#238. It reverts under this document's own rule, and what it settles is worth
more than the change would have been.

WHICH FAMILY, MEASURED RATHER THAN GUESSED. An opcode histogram in vu_compute
over 900 frames of Super Mario 64: 125 M VU compute ops, ~139 k per frame. The
MULTIPLY FAMILY (0x00..=0x0F) is ~61% of them — 0x0e alone is 17.8%, 0x0f 15.2%.
It is also the only family that dispatched TWICE per lane (outer match, then
multiply_lane's own), so the opcode was decoded sixteen times per instruction.
If dispatch-in-the-loop were the constraint, that is where it would show.

THE CHANGE WAS CORRECT. A dedicated eight-lane loop for op <= 0x0F, returning
directly since the multiplies write no VCC, no VCO, and fall outside the
0x28..=0x2D accumulator range. Accuracy held on the new gate: RSP 0 failing
across 224 tests started, suite-wide unchanged at 90.

AND IT IS WORTH NOTHING. Six readings a side, one sitting:

  A  64.114 64.303 64.183 64.110 63.744 63.899   (mean 64.059)
  B  63.840 64.182 64.040 63.473 63.775 63.517   (mean 63.804)

The ranges overlap heavily. Means differ by 0.40% in the hoist's favour while
the conservative pairing says it is 0.68% SLOWER. When the sign depends on which
pairing you quote, the result is neutral — the same standard applied to the
rsp_tick early-out, and the same conclusion. Reverted per the standing rule.

WHAT IT SETTLES. This was the strongest available test of "the per-lane dispatch
is what stops the VU going fast", on the family that is 61% of the work and
carries double the dispatch. It moved nothing.

With the decode sizing (0.29% for a perfect cache), the VU's 8.54% of a frame is
THE ARITHMETIC ITSELF, not the dispatch around it. No amount of restructuring the
interpreter reaches it. What remains is genuine SIMD — eight u16 lanes per
operation, which is what the hardware is — or nothing.

That makes the unsafe/nightly decision the real gate on further VU work rather
than something to defer behind a restructure. This measurement does not make that
call; it removes the cheaper alternative that was standing in front of it.

Recorded under Ruled out: do not hoist VU opcode families for performance. The
remaining families are smaller and singly-dispatched, so they can only measure
smaller.

All instrumentation was scratch: vu.rs snapshotted, restored, tree verified clean
against the snapshot between every leg.

Gates: check_en_us.sh, markdownlint. Docs-only — the hoist itself is not landed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(perf): bound the hoist conclusion to what one family measured

Two review findings.

I OVER-GENERALISED FROM ONE FAMILY, AGAIN. The section claimed that no
interpreter restructuring reaches the VU, that SIMD or nothing is what remains,
and that the other families can only measure smaller. None of that was measured.
One family was hoisted on one workload.

Split into what IS established — hoisting the multiply family's dispatch is
neutral on SM64's render phase, and with the 0.29% decode sizing that is two of
the three things a pre-decoded threaded interpreter would do — and what is
HYPOTHESIS, each labelled and each needing its own measurement: that the 8.54% is
predominantly arithmetic (neither experiment decomposed the per-lane body); that
no restructuring helps (a full per-opcode specialisation, which also hoists
multiply_lane's inner match, is a DIFFERENT change); that smaller families
measure smaller (a reason to expect it, not a measurement); and that SIMD is what
remains (obvious candidate, untried).

The Ruled-out entry now rules out REPEATING THIS EXPERIMENT rather than the
different change it was being read to cover.

What survives is narrower and still useful: the cheapest candidate is gone from
the front of the queue, so the unsafe/nightly decision is no longer deferrable
behind "restructure first, it is free" — the free restructure was tried on its
best case and did nothing.

PROVENANCE ADDED for the benchmark: the actual A-B-A order (A x3 -> B x3 -> B x3
-> A x3, the B blocks contiguous because the rebuild is what costs time), the
revision, and the exact command; host, OS, toolchain, profile and ROM hash are
the ones recorded once in section Method rather than duplicated.

Gates: check_en_us.sh, markdownlint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

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