Skip to content

feat(pif): report "no device" on empty ports — SM64 renders its title screen (R-18) - #190

Merged
doublegate merged 3 commits into
mainfrom
fix/pif-no-device-bit
Jul 29, 2026
Merged

feat(pif): report "no device" on empty ports — SM64 renders its title screen (R-18)#190
doublegate merged 3 commits into
mainfrom
fix/pif-no-device-bit

Conversation

@doublegate

Copy link
Copy Markdown
Owner

Super Mario 64 renders its title screen. Mario's head, textured cap with the M logo, over the tiled SUPER MARIO 64 background — screenshots/super-mario-64-title.png. 125,278 RDP commands, 138,474/148,125 pixels lit at 625×237, through the full LLE path. Viewed and confirmed, not counted.

The unblocking defect was in the PIF, not the RDP

SM64 was halting in its own assert path — a B -1 self-loop at 0x80246DD8, reached by falling through BNE t1, zero, +4 where t1 is a byte at 0x8032D590 that is never written — immediately after an osCreateThread(..., pri=100) call.

Cause: our PIF answered the 0x00/0xFF info command as a connected controller on all four joybus channels, so osContInit reported four pads on a one-pad console. n64brew_wiki/markdown/PIF-NUS.md §RX byte: special flags is explicit:

Bit 7 | 0x80 | No device — set if the handshake failed because no device appears to be connected to the joybus channel.

We never set it.

A second, pre-existing defect sat next to it

mark_no_device wrote the flag to resp - 1. For a block laid out TX RX tt[tx_len] rr[rx_len] with resp = i + 2 + tx_len, that is the last TX data byte, not the RX byte at i + 1. Since run_channel returns early on tx_len == 0, the two offsets could never coincide — so the flag had never once landed where software reads it, while a doc comment asserted it did.

Mutation-checked — and the second test exists because the first mutation PASSED

mutation caught?
revert empty-port guard ✅ red
revert mark_no_device offset passed

The empty-port guard short-circuits before run_channel, so nothing exercised mark_no_device at all. mark_no_device_flags_the_rx_byte_not_the_tx_payload reaches it via a connected channel given an unsupported command, and uses a two-byte TX so resp - 1 and the RX byte are two apart — with a one-byte TX an off-by-one is indistinguishable from a correct write. Both mutations are now caught.

Measured effect

Title RDP commands result
Super Mario 64 0 → 125,278 title screen
Banjo-Kazooie 0 → 133,625 real 3D geometry

Banjo-Kazooie is committed as a known-imperfect frame: geometry, textures and depth ordering are right; colours carry a heavy blue/yellow cast (open combiner/texel-format issue). It is evidence of what currently happens, not a correctness target.

Oracle unchanged: n64-systemtest Phase 1 Failed: 0, suite-wide 90.

A measurement trap, recorded

A 240-frame sample reported SM64 at scanout = 0x0 and looked like a geometry failure. It was a transient — one frame with H_VIDEO = 0 mid-reprogram of the VI. Sampling at 60/120/180/300 shows a stable 625×237 throughout. A single-point sample of a value the guest rewrites can land on the rewrite.

Still open

Jet Force Gemini and Banjo-Tooie remain at 0 RDP commands; 1080 Snowboarding still faults in IPL3 on an odd DMEM address. Separate causes, recorded in R-18.

Gates

fmt · clippy -D warnings · cargo test --workspace · rustdoc -D warnings · markdownlint · n64-systemtest — all green. No ROMs committed; only rendered output, which the commercial-roms policy permits.

…e screen

Super Mario 64 renders its title screen (screenshots/super-mario-64-title.png):
Mario's head, textured cap with the M logo, over the tiled SUPER MARIO 64
background. 125,278 RDP commands, 138,474/148,125 pixels lit at 625x237,
through the full LLE path. Viewed and confirmed, not counted.

The unblocking defect was in the PIF, not the RDP. SM64 was halting in its own
assert path -- a B -1 self-loop at 0x80246DD8, reached by falling through
BNE t1, zero, +4 where t1 is a byte at 0x8032D590 that is never written --
immediately after an osCreateThread call. Our PIF answered the 0x00/0xFF info
command as a connected controller on ALL FOUR joybus channels, so osContInit
reported four pads on a one-pad console. PIF-NUS.md, RX byte: special flags, is
explicit: bit 7 (0x80) is "set if the handshake failed because no device
appears to be connected to the joybus channel". We never set it.

A second, pre-existing defect sat next to it. mark_no_device wrote the flag to
resp - 1, which for a block laid out TX RX tt[tx_len] rr[rx_len] with
resp = i + 2 + tx_len is the LAST TX DATA BYTE, not the RX byte at i + 1. Since
run_channel returns early on tx_len == 0 the two offsets could never coincide,
so the flag had never once landed where software reads it -- while a doc
comment asserted it did.

Both fixed and separately mutation-checked. The second test exists only because
the first mutation PASSED: the empty-port guard short-circuits before
run_channel, so nothing exercised mark_no_device at all until a test was
written for a connected channel given an unsupported command.

Measured effect: Super Mario 64 0 -> 125,278 RDP commands and a title screen;
Banjo-Kazooie 0 -> 133,625 commands and real 3D geometry, committed as a
known-imperfect frame (geometry, textures and depth right; colours carry a
blue/yellow cast, an open combiner/texel-format issue).

A measurement trap is recorded with it: a 240-frame sample reported SM64 at
scanout 0x0 and looked like a geometry failure. It was a transient -- one frame
with H_VIDEO = 0 mid-reprogram of the VI. Sampling at 60/120/180/300 shows a
stable 625x237 throughout.

Still open: Jet Force Gemini and Banjo-Tooie remain at 0 RDP commands, and 1080
Snowboarding still faults in IPL3 on an odd DMEM address.

Gates: fmt, clippy -D warnings, cargo test --workspace, rustdoc -D warnings,
markdownlint, n64-systemtest (Phase 1 Failed: 0, suite-wide 90) -- all 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

  • New Features

    • Added controller-port presence detection to accurately reflect which controller ports are populated.
    • Added new rendered-image evidence for Super Mario 64’s title screen and an early Banjo-Kazooie scene.
  • Bug Fixes

    • Corrected “no device” reporting for disconnected controller ports, ensuring the flag is placed in the correct reply byte.
    • Improved commercial game rendering outcomes, including corrected boot sequencing, interrupt handling, and CI4 texture palette mapping.
    • Recorded first verified commercial title-screen render with command and pixel accuracy results.
  • Documentation

    • Updated accuracy ledger entries and screenshot documentation with revised methodology, new evidence, and outstanding limitations.

Walkthrough

PIF now tracks connected controller ports and reports absent devices through the correct RX-byte flag. Tests cover the corrected response layout and dispatch behaviour. R-18 records and screenshot documentation now include commercial-title rendering evidence and related accuracy findings.

Changes

PIF controller handling and R-18 evidence

Layer / File(s) Summary
PIF controller state and dispatch
crates/rustyn64-cart/src/pif.rs
Adds controller connectivity state and accessors, routes disconnected channels to no-device responses, and writes RX_NO_DEVICE using the explicit RX-byte offset.
Controller response tests
crates/rustyn64-cart/src/pif.rs
Verifies absent ports flag the RX byte without altering the final TX byte, connectivity affects dispatch, and populated ports respond without the flag.
R-18 evidence records
docs/accuracy-ledger.md, screenshots/README.md, CHANGELOG.md
Records commercial-title rendering results, corrected accuracy findings, screenshot evidence, and the PIF fix in the unreleased changelog.

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

Sequence Diagram(s)

sequenceDiagram
  participant PifExecute
  participant ControllerPort
  participant ResponseBuffer
  PifExecute->>ControllerPort: inspect connected state
  ControllerPort->>ResponseBuffer: set RX_NO_DEVICE on RX byte
  ResponseBuffer-->>PifExecute: return channel response
Loading
🚥 Pre-merge checks | ✅ 7 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning It matches the main change, but it exceeds the 72-character Conventional Commits limit, so it fails the title rule. Shorten it to type(scope): subject within 72 characters, e.g. feat(pif): report no device on empty ports.
Docs-As-Spec Sync ⚠️ Warning FAIL: rustyn64-cart/src/pif.rs changed observable Joybus behaviour, but docs/cart.md did not change; HEAD only touches pif.rs and docs/accuracy-ledger.md, with no no-docs-needed rationale. Add or update docs/cart.md for the new controller-port mask and RX 'no device' semantics, or explain in the PR body why cart docs are intentionally unchanged.
Measured, Never Tuned ⚠️ Warning FAIL: Pif::new() hard-codes [true, false, false, false] with no manual/wiki citation or ledger provenance; R-18 records the SM64 outcome, not that default mask. Either cite the one-pad default to a manual/wiki page or record its measurement in docs/accuracy-ledger.md; otherwise remove the hard-coded hardware assumption from Pif::new().
✅ Passed checks (7 passed)
Check name Status Explanation
Description check ✅ Passed The description is clearly about the PIF no-device fix and SM64 render result, so it is related to the changeset.
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: the PR states the n64-systemtest oracle effect explicitly ('Phase 1 Failed: 0, suite-wide 90'), matching the STATUS ledger.
Changelog Entry For User-Visible Changes ✅ Passed PASS: CHANGELOG.md has an [Unreleased] section with Added/Fixed entries for the SM64 render and PIF joybus fix, matching the rule.
Unsafe Stays Out Of The Chip Crates ✅ Passed No unsafe constructs were found; every chip/core lib.rs still has #![forbid(unsafe_code)], and the frontend has no unsafe blocks.

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

Adopts three Antigravity suggestions.

set_controllers_connected / controllers_connected replace the hardcoded
private field, so a frontend can attach pads to ports 2-4 instead of the PIF
permanently reporting single-player.

The empty-port path now delegates to mark_no_device(i + 1) rather than poking
ram directly, centralising the bounds check. This also strengthened the
mutation check: reverting the offset now turns BOTH no-device tests red rather
than one, because the two paths share the write.

The tests derive the RX-byte offset from the documented frame layout
(SKIPS + 1) instead of counting backwards from resp with magic subtraction,
and assert the layout itself so the derivation cannot drift silently.

Gates: fmt, clippy -D warnings, cargo test --workspace, rustdoc -D warnings --
all green. Both mutations re-checked after the refactor.

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

Copy link
Copy Markdown
Owner Author

Adjudication — Antigravity review

No blocking issues. All three suggestions adopted.

1. No API to configure connected controllers — adopted

Correct, and it would have been a real limitation: connected was private with no way in, so the PIF permanently reported single-player and a 2-4 player frontend had no route to attach pads. Added set_controllers_connected([bool; 4]) and controllers_connected(), documented that channel 4 is the cartridge/EEPROM bus and not a controller port.

2. Bypassing the mark_no_device helper — adopted, and it improved the test

Right on the DRY/bounds-checking grounds, and it turned out to matter more than that. Delegating means the two no-device paths now share the write, so reverting the offset turns BOTH tests red instead of one:

before refactor:  offset mutation -> 1 failed
after  refactor:  offset mutation -> 2 failed

Worth noting because the reason the second test exists is that the first mutation originally passed — the empty-port guard short-circuited before run_channel, so nothing exercised mark_no_device at all. Delegating removes that split entirely.

3. Magic subtraction in the test offsets — adopted

resp - 1 - 1 was opaque and, worse, fragile in the direction that matters: it encodes the answer the test is checking. The offsets now derive from the documented layout —

const SKIPS: usize = 0;
const TX: [u8; 2] = [0x99, 0x5A];
let rx_byte = SKIPS + 1;                    // TX RX tt[..] rr[..]
let last_tx = SKIPS + 2 + TX.len() - 1;
assert_eq!(resp, SKIPS + 2 + TX.len(), "frame layout as documented");

— with the layout itself asserted, so the derivation cannot drift silently against frame().

(Clippy items_after_statements required hoisting those consts to the top of each test; fixed in the same commit.)

Gates re-run: fmt, clippy -D warnings, cargo test --workspace, rustdoc -D warnings — green, and both mutations re-checked after the refactor.

@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 `@crates/rustyn64-cart/src/pif.rs`:
- Around line 521-528: Update the documentation around Pif::mark_no_device to
remove the claim that the connected unsupported-command path is its only route.
Describe it specifically as the connected-controller unsupported-command route,
while acknowledging that the missing-EEPROM path also reaches mark_no_device.
- Around line 116-133: Add a controller dispatch test covering
set_controllers_connected: enable port 1, send its info handshake through the
PIF dispatch path, and assert the expected controller ID plus an RX byte without
error flags. Ensure the test fails when the configured presence mask is ignored,
while preserving the default controller behavior for existing tests.

In `@docs/accuracy-ledger.md`:
- Line 413: Update the later historical passage about `tlut_en` so it no longer
says the field remains undecoded; preserve the surrounding historical wording
and clarify that `tlut_en` is decoded while retaining the documented limitation
that the committed vector does not independently validate its gating behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ad312a15-8e34-4520-a890-ed73e378b068

📥 Commits

Reviewing files that changed from the base of the PR and between bc91aae and 465494c.

⛔ Files ignored due to path filters (2)
  • screenshots/banjo-kazooie-first-3d-scene.png is excluded by !**/*.png
  • screenshots/super-mario-64-title.png is excluded by !**/*.png
📒 Files selected for processing (4)
  • CHANGELOG.md
  • crates/rustyn64-cart/src/pif.rs
  • docs/accuracy-ledger.md
  • screenshots/README.md

Comment thread crates/rustyn64-cart/src/pif.rs
Comment thread crates/rustyn64-cart/src/pif.rs Outdated
Comment thread docs/accuracy-ledger.md Outdated
Adopts three CodeRabbit findings.

set_controllers_connected had no test -- every controller test ran on the
default mask, so a no-op setter would have passed. the_configured_port_mask_
reaches_the_joybus_dispatch inverts the default (port 0 empty, port 1
populated) and asserts BOTH directions from one setter call, so a setter
writing a constant cannot satisfy it. Mutation-checked: stubbing the setter to
a no-op turns it red.

mark_no_device's doc claimed the connected/unsupported-command path was its
ONLY route; a missing EEPROM on channel 4 reaches it too. Corrected to say what
is actually true -- it is the route this test drives, and it was the only route
any test could take before the empty-port guard was refactored to delegate.

Ledger R-18 said "tlut_en remains undecoded" in one clause while recording it
as FIXED in #185 in another. Labelled as the historical statement it was,
rather than rewritten, per the append-only rule the row follows.

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

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

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This PR configures joybus channels 2–4 as disconnected by default in the PIF and fixes mark_no_device to target the RX byte (i + 1) rather than the last TX data byte (resp - 1), preventing empty controller ports from falsely reporting connected devices to osContInit.

Blocking issues

None found.

Suggestions

  • pif.rs: Save-state binary compatibility (ADR 0005): Adding connected: [bool; 4] alters the struct memory footprint and serialization layout of Pif. If Pif is serialized directly into save states, verify whether adding this field breaks backward compatibility with existing save states or requires migration logic under ADR 0005.

Nitpicks

  • pif.rs: Field connected: [bool; 4] is ambiguous next to pak_present: [bool; 4]; consider renaming to ports_connected or controllers_connected for consistency with its public accessor methods.

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 (1)
crates/rustyn64-cart/src/pif.rs (1)

292-310: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the cart/PIF spec and oracle note
crates/rustyn64-cart/src/pif.rs changes behaviour, but this commit only edits docs/accuracy-ledger.md for R-18. docs/cart.md is still untouched, and there is no PIF-specific n64-systemtest impact note here (or an explicit “not measured” entry) for this change.

🤖 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 `@crates/rustyn64-cart/src/pif.rs` around lines 292 - 310, Update the
documentation alongside the PIF behavior change in `pif.rs`: document the
empty-controller-channel handling and channel 4 exception in `docs/cart.md`, and
add a PIF-specific `n64-systemtest` impact note in the relevant accuracy
documentation, explicitly marking it as not measured if no result is available.

Sources: Coding guidelines, Path instructions

🤖 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/accuracy-ledger.md`:
- Line 413: Remove the unreleased “v0.6.0” version reference from the R-18 entry
in the accuracy ledger. Replace it with an unversioned description of shipping
based on the committable gates and honest “boots and executes” capstone,
preserving the surrounding historical context.

---

Outside diff comments:
In `@crates/rustyn64-cart/src/pif.rs`:
- Around line 292-310: Update the documentation alongside the PIF behavior
change in `pif.rs`: document the empty-controller-channel handling and channel 4
exception in `docs/cart.md`, and add a PIF-specific `n64-systemtest` impact note
in the relevant accuracy documentation, explicitly marking it as not measured if
no result is available.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6173be44-3899-4d1a-9b66-44abfc524652

📥 Commits

Reviewing files that changed from the base of the PR and between 465494c and 925c22e.

📒 Files selected for processing (2)
  • crates/rustyn64-cart/src/pif.rs
  • docs/accuracy-ledger.md

Comment thread docs/accuracy-ledger.md
@doublegate
doublegate merged commit 112f92e into main Jul 29, 2026
11 checks passed
@doublegate
doublegate deleted the fix/pif-no-device-bit branch July 29, 2026 13:37
doublegate added a commit that referenced this pull request Jul 29, 2026
* chore(release): v0.8.0 "Breadth" -- Phase 7

Cuts the Phase 7 release. Both cut criteria from to-dos/VERSION-PLAN.md are
met and were verified rather than assumed:

- The battery reports a real pass rate: 54 probes, 100%, across two Angrylion
  oracle suites (41 RDP rasteriser vectors + 13 VI scan-out vectors).
- Every residual R-1 through R-23 carries a ledger disposition, and no residual
  is referenced anywhere in docs/ or to-dos/ without a ledger entry.

The headline is that commercial cartridges render. Super Mario 64 draws its
title screen and 29 of 66 staged titles render through their own graphics
microcode. Both defects that unblocked that were in the CPU and the PIF, not
the RDP where R-18 had been looking:

- an interrupt taken across an ERET was charged to the ERET, overwriting EPC
  with the ERET's own address and livelocking the CPU (#189);
- the PIF answered as a connected controller on all four joybus channels, so
  osContInit reported four pads on a one-pad console and Super Mario 64 halted
  in its own assert (#190).

T-71-003 -- the one cut criterion CI can never run, because F3DEX ships inside
ROMs ADR 0008 forbids committing -- closes on a committed local census plus 14
viewed screenshots, recorded in VERSION-PLAN as the single documented
exception rather than an unstated weakening of the gate.

Version bumped 0.7.0 -> 0.8.0 across the workspace and all path dependencies;
CHANGELOG [Unreleased] cut to [0.8.0]; STATUS records Phase 7 complete and the
release history through v0.8.0.

Pre-release gate: clean tree, version consistent (Cargo == CHANGELOG), no
stale 0.7.0 in manifests, v0.8.0 tag free, no ROMs staged, no conflict
markers, CHANGELOG entry present, no_std build green. Plus fmt, clippy
-D warnings, cargo test --workspace, rustdoc -D warnings, markdownlint.

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

* docs(changelog): 'unblocked it', not 'unblocked that'

Adopts a CodeRabbit wording fix in the v0.8.0 release note.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <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