Skip to content

feat(rdp): decode the DP FIFO command stream (T-31-001) - #56

Merged
doublegate merged 5 commits into
mainfrom
feat/rdp-command-decoder
Jul 22, 2026
Merged

feat(rdp): decode the DP FIFO command stream (T-31-001)#56
doublegate merged 5 commits into
mainfrom
feat/rdp-command-decoder

Conversation

@doublegate

Copy link
Copy Markdown
Owner

Motivation

First ticket of Phase 3 (v0.4.0 "Rasteriser"), Sprint 1 — T-31-001, the DP FIFO
and command decoder
. Before any pixel can be drawn, the RDP has to walk its
command stream without desyncing: multi-word primitives (triangles, texture
rectangles) must be consumed whole, and unimplemented opcodes must be skipped by
exactly their length. The prior tick stub advanced one 64-bit word per call,
which would shred any multi-word command.

Changes

  • Rdp::tick drains the FIFO. Reads the command word at DPC_CURRENT,
    decodes the opcode (bits 61:56), advances DPC_CURRENT by the command's full
    length. One command retired per scheduler tick — gradual drain, not a burst.
    No opcode is dispatched yet (T-31-002+ adds the sync commands + DP interrupt,
    then the fill pipeline); each command is recognised, consumed, and counted.
  • rustyn64_rdp::command — the length table, sourced from the N64brew
    Reality Display Processor/Commands map. One 64-bit word for every command
    except the variable Fill Triangle forms (0x080x0F: 4-word base + shade/
    texture/z blocks selected by the opcode's low three bits) and Texture
    Rectangle (0x24/0x25, 2 words).
  • commands_processed is a retired-work tally, not a cycle position —
    nothing schedules against it, so it does not touch the derive-don't-increment
    rule (ADR 0006).
  • Docs: docs/rdp.md gains a command-decoder section in the same change;
    CHANGELOG.md under [Unreleased].

Verification

  • Opcode→length map unit-tested exhaustively across 0x000x3F, including all
    eight triangle forms and the texture-rectangle pair.
  • A mixed-list walk (set-state, a 22-word STZ triangle, a no-op, a texture
    rectangle, Sync Full) asserts DPC_CURRENT lands exactly on DPC_END, one
    command per tick. Mutation-checked: forcing the length to 8 fails the walk
    (27 ticks ≠ 5).
  • Gates run locally, all green: cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace, RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps, the thumbv7em-none-eabihf no_std
    build, and pre-commit run markdownlint.

🤖 Generated with Claude Code

Phase 3, Sprint 1's first ticket: the RDP now drains its command FIFO
instead of blindly advancing one 64-bit word per tick. `Rdp::tick` reads
the command at `DPC_CURRENT`, decodes the opcode (bits 61:56), and
advances the pointer by the command's *full* length, so a multi-word
primitive is consumed whole and the stream never desyncs mid-command. It
retires one command per scheduler tick — the FIFO drains gradually, not
in a burst. No opcode is dispatched to a handler yet (that is T-31-002+):
each command is recognised, its length consumed, and a retired-work
counter incremented.

The length table (`rustyn64_rdp::command`) is sourced from the N64brew
`Reality Display Processor/Commands` map: one 64-bit word for every
command except the variable-length Fill Triangle forms (0x08-0x0F, a
4-word base plus shade/texture/z coefficient blocks selected by the
opcode's low three bits) and the two-word Texture Rectangle pair
(0x24/0x25). `commands_processed` is a retired-work tally, not a cycle
position — nothing schedules against it, so it does not touch the
derive-don't-increment rule (ADR 0006).

Tests: the opcode->length map is unit-tested exhaustively across
0x00-0x3F, and a mixed-list walk (set-state, a 22-word STZ triangle, a
no-op, a texture rectangle, Sync Full) asserts the decoder lands
`DPC_CURRENT` exactly on `DPC_END`, one command per tick. Mutation-checked:
forcing the length to 8 fails the walk (27 ticks != 5).

Docs: `docs/rdp.md` gains a command-decoder section in the same change;
`CHANGELOG.md` under [Unreleased].

Gates run locally: cargo fmt --check, clippy --workspace --all-targets
-D warnings, cargo test --workspace, RUSTDOCFLAGS=-D warnings cargo doc,
the thumbv7em no_std build, and markdownlint — all green.

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

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 92681e13-4ed2-4d17-afb7-53473125348a

📥 Commits

Reviewing files that changed from the base of the PR and between 4e1c99d and c93d0cf.

📒 Files selected for processing (1)
  • crates/rustyn64-rdp/src/lib.rs

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added DP command-stream decoding and consumption in the RDP tick loop (no rasterisation yet), processing at most one full command per tick.
    • Introduced a commands_processed counter to track retired commands.
  • Bug Fixes

    • Improved FIFO progression by advancing only when the complete command is available.
    • Ensured correct stalling when the end marker appears mid-command and when in XBUS mode.
  • Documentation

    • Documented opcode and command-length/encoding rules and the new tick/stall behaviour.
  • Tests

    • Added unit tests covering opcode bit extraction and command-length decoding across the opcode map.

Walkthrough

The RDP now decodes DP FIFO opcodes, consumes complete commands in one scheduler tick, tracks retired commands, and stalls for incomplete commands or XBUS. Documentation and tests cover command-length rules and FIFO behaviour.

Changes

DP FIFO decoder

Layer / File(s) Summary
Command length contract
crates/rustyn64-rdp/src/command.rs
Adds opcode extraction and command-length helpers for all RDP opcodes, including variable-length Fill Triangle and two-word Texture Rectangle commands, with exhaustive unit tests.
FIFO tick integration and validation
crates/rustyn64-rdp/src/lib.rs, docs/rdp.md, CHANGELOG.md
Updates Rdp::tick to decode and consume complete commands, track commands_processed, stall on incomplete data or XBUS, and documents and tests the resulting FIFO alignment behaviour.

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

Sequence Diagram(s)

sequenceDiagram
  participant RdpTick
  participant RdramBus
  participant CommandDecoder
  participant DpFifoState
  RdpTick->>RdramBus: Read command word at DPC_CURRENT
  RdramBus-->>RdpTick: Return command word
  RdpTick->>CommandDecoder: Decode opcode and command length
  CommandDecoder-->>RdpTick: Return full word length
  RdpTick->>DpFifoState: Advance DPC_CURRENT and increment commands_processed
Loading

Possibly related PRs

  • doublegate/RustyN64#44: Adds the Rdp::status and DPC register semantics used by the decoder’s freeze and XBUS gating.
🚥 Pre-merge checks | ✅ 9 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Oracle Number Is Stated ⚠️ Warning The new DP FIFO entry omits any n64-systemtest delta or explicit 'not measured' note; docs/STATUS.md still carries the current 93 suite-wide failures. Add the measured n64-systemtest failing-assertion delta to the new behaviour note, or state explicitly that it was not measured.
✅ Passed checks (9 passed)
Check name Status Explanation
Title check ✅ Passed Matches Conventional Commits and accurately summarises the DP FIFO decoder change.
Description check ✅ Passed The description is directly about the DP FIFO decoder work and related docs/tests.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
Docs-As-Spec Sync ✅ Passed PASS: rustyn64-rdp’s behaviour change is documented in docs/rdp.md under T-31-001, including tick stalls, XBUS, length rules, and commands_processed.
Changelog Entry For User-Visible Changes ✅ Passed PASS: CHANGELOG.md has an [Unreleased] Added entry for the DP FIFO command decoder and commands_processed, covering the user-visible change.
Measured, Never Tuned ✅ Passed PASS: the only new numeric command-length values are sourced to the N64brew wiki in command.rs/docs; no new measured hardware constant appears, and accuracy-ledger.md needs no entry.
Unsafe Stays Out Of The Chip Crates ✅ Passed Touched chip/core crates still carry #![forbid(unsafe_code)], and a repo-wide Rust syntax search found no unsafe blocks, fns, traits, or externs.

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

@github-actions

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This PR implements RDP DP FIFO command length decoding for opcodes 0x000x3F and updates Rdp::tick to advance DPC_CURRENT by each command's length.

Blocking issues

  1. Incorrect length for Texture Rectangle commands (0x24 / 0x25) causes FIFO stream desync

    • command.rs:47: command_len_words returns 2 words for 0x24 (TEX_RECT) and 0x25 (TEX_RECT_FLIP). On N64 RDP hardware, texture rectangle commands consist of 3 64-bit words (24 bytes: header word, $S/T/dSdx/dTdy$ word, and $dSdy/dTdx$ word). Decoding them as 2 words leaves DPC_CURRENT pointing at the 3rd payload word on the next tick, causing the decoder to treat parameter data as an opcode and desync the stream.
  2. Partial command consumption when cmd_current + len_bytes > cmd_end

    • lib.rs:197-206: Rdp::tick checks self.cmd_current >= self.cmd_end to guard against an empty FIFO, but it does not verify whether self.cmd_current + len_bytes <= self.cmd_end before advancing cmd_current. If DPC_END is updated incrementally while a multi-word command (such as a 22-word triangle) is being written into RDRAM, tick will execute against unwritten memory, advance cmd_current past cmd_end, and ignore the command payload when the rest of the words arrive.

Suggestions

  1. Avoid multiplying word length at call site

    • lib.rs:201: command::command_len_words(opcode) * 8 performs unchecked multiplication. Consider having command_len_bytes return byte offsets directly or using checked arithmetic to prevent potential overflow if cmd_current is near u32::MAX.
  2. Test big-endian payload assembly explicitly in test bus

    • lib.rs:334-346: SliceBus relies on default RdramBus::rdram_read_u32 behavior. Adding explicit test cases verifying big-endian doubleword extraction across byte boundaries will prevent host architecture endianness assumptions from hiding extraction bugs.

Nitpicks

  • command.rs:42: Redundant outer parentheses in let zbuffer = (opcode & 1) as u32;.
  • CHANGELOG.md:23: Update changelog documentation to reflect 3-word length for texture rectangle commands.

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: 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-rdp/src/lib.rs`:
- Around line 352-357: Update push_cmd in the decoder alignment test to accept
an explicit fixed word count for each fixture command instead of calling
command::command_len_words(opcode). Use independently specified counts at every
call site, while preserving the existing opcode encoding and zero-padding so the
test endpoint is independent of the production decoder table.
- Around line 199-206: The command retirement logic in tick must wait for the
full encoded command before advancing cmd_current: after decoding opcode and
len_bytes, return when cmd_end - cmd_current is less than len_bytes, preserving
the header-only 0x0F until its remaining 21 words arrive; add coverage for this
incremental FIFO case. Update docs/rdp.md lines 89-95 to state that tick retires
commands only when their complete encoded length is available.
- Around line 192-205: Update tick to return immediately when DP_STATUS_XBUS is
set, before reading RDRAM, decoding an opcode, or advancing cmd_current. Add a
test covering XBUS mode that verifies both CURRENT/cmd_current and
commands_processed remain unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3fdfb3f9-d073-4644-94d1-355c85c26242

📥 Commits

Reviewing files that changed from the base of the PR and between 1ee000f and ffb46b2.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • crates/rustyn64-rdp/src/command.rs
  • crates/rustyn64-rdp/src/lib.rs
  • docs/rdp.md

Comment thread crates/rustyn64-rdp/src/lib.rs
Comment thread crates/rustyn64-rdp/src/lib.rs Outdated
Comment thread crates/rustyn64-rdp/src/lib.rs Outdated
Adjudicating the PR #56 bot review:

- **Wait for the whole command** (CodeRabbit #2, Antigravity blocking #2):
  `Rdp::tick` now returns without advancing when `DPC_END - DPC_CURRENT`
  is less than the decoded length. The rdpq microcode advances `DPC_END`
  incrementally as it fills the buffer, so it can land mid-command;
  consuming a partial multi-word primitive would decode unwritten RDRAM.
  New test drives a 22-word triangle with `DPC_END` first at word 10
  (stalls) then at word 22 (consumed whole). Mutation-checked.

- **XBUS stalls the decoder** (CodeRabbit #3): with `DPC_STATUS.XBUS` set
  the command source is DMEM, which is not yet wired, so the decoder must
  not fall back to reading RDRAM (that would decode parameter data as
  opcodes and desync). New test asserts no advance under XBUS.
  Mutation-checked.

- **Independent decoder test** (CodeRabbit #1): the FIFO-walk fixture now
  states each command's word count explicitly rather than calling
  `command_len_words`, so the walk is a genuine check of the decoder
  against the N64brew map, not a tautology built from it.

Rejected: Antigravity blocking #1 (texture rectangle is 3 words). The
N64brew command map shows 0x24/0x25 as two 64-bit words (Word 0 coords +
Word 1 s/t/dsdx/dtdy); there is no third dsdy/dtdx word — texrect is
axis-aligned and carries only dsdx and dtdy. The decoder's 2 stands.

Docs: docs/rdp.md gains the two stall conditions; CHANGELOG updated.
Gates: fmt, clippy --workspace -D warnings, cargo test --workspace,
rustdoc -D warnings, thumbv7em no_std, markdownlint — all green.

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

Copy link
Copy Markdown
Owner Author

Adjudication of the Antigravity review

Thanks — adjudicated each item; fixes in 3f92685.

Blocking

  1. Texture Rectangle length (2 vs 3 words) — REJECTED (bot premise incorrect).
    The N64brew command map (Reality Display Processor/Commands, §0x24/0x25) shows
    Texture Rectangle / Flip as two 64-bit words: Word 0 = lrx/lry/tile/ulx/uly
    (screen rectangle), Word 1 = s/t/dsdx/dtdy (texture coords + derivatives). There
    is no third dsdy/dtdx word — texrect is axis-aligned, so only dsdx and dtdy
    exist (the command is "equivalent to a left-major triangle where dxhdy=dxmdy=dxldy=0").
    This matches angrylion's rdp_command_length (16 bytes = 2 words). The decoder's
    0x24 | 0x25 => 2 stands. No CHANGELOG change (see nitpick 2).

  2. Partial command consumption — ADOPTED. tick now stalls when
    DPC_END - DPC_CURRENT < len_bytes, so an incrementally-advanced DPC_END that
    lands mid-command waits for the rest instead of decoding unwritten RDRAM. New test
    a_partial_command_is_not_consumed_until_complete, mutation-checked.

Suggestions

  1. Checked arithmetic on len * 8 — REJECTED (no reachable overflow). DPC_START/
    DPC_END are masked to DPC_ADDR_MASK (0x00FF_FFF8, 24-bit), so cmd_current is
    bounded well under 16 MiB and len_bytes ≤ 22*8 = 176; the wrapping_add cannot wrap
    in practice, and the new complete-command guard bounds the walk to [cmd_current, cmd_end). Adding checked arithmetic here would be dead defensiveness.

  2. Explicit big-endian extraction test — REJECTED (already covered). The FIFO-walk
    test writes headers with (opcode << 24).to_be_bytes() and the decoder recovers the
    opcode via rdram_read_u32 + opcode_of; a wrong-endian read misdecodes the opcode
    and desyncs the walk, so BE doubleword extraction is exercised end-to-end already.

Nitpicks

  1. "Redundant parens" in (opcode & 1) as u32 — REJECTED (parens are load-bearing).
    as binds tighter than &, so opcode & 1 as u32 parses as opcode & (1u32) — a
    u8 & u32 type error. The parentheses are required; clippy (pedantic+nursery) passes
    with them and would reject truly redundant ones.

  2. CHANGELOG texrect wording — REJECTED, consequent of blocking docs: require every change to ship via PR with a bot-comment ceremony #1: texrect is 2 words.

@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-rdp/src/lib.rs (1)

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

Mirror commands_processed in the documented public interface.

docs/rdp.md's Rdp declaration omits this new public field. Keep the API reference complete. As per path instructions, “Docs are the SPEC here”.

🤖 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-rdp/src/lib.rs` at line 96, Update the documented Rdp
declaration in docs/rdp.md to include the public commands_processed: u64 field,
matching the Rust Rdp interface. Keep the documentation’s existing declaration
style and ordering consistent with the implementation.

Source: 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 `@crates/rustyn64-rdp/src/lib.rs`:
- Line 418: Correct the comment on the push_cmd call using opcode 0x0E to
identify it as a shade+texture triangle, while preserving the existing opcode,
arguments, and word-count annotation.

---

Outside diff comments:
In `@crates/rustyn64-rdp/src/lib.rs`:
- Line 96: Update the documented Rdp declaration in docs/rdp.md to include the
public commands_processed: u64 field, matching the Rust Rdp interface. Keep the
documentation’s existing declaration style and ordering consistent with the
implementation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 657daaaf-4240-4ca5-a1f3-6a71448552d2

📥 Commits

Reviewing files that changed from the base of the PR and between ffb46b2 and 3f92685.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • crates/rustyn64-rdp/src/lib.rs
  • docs/rdp.md

Comment thread crates/rustyn64-rdp/src/lib.rs Outdated
…01 review)

Adjudicating CodeRabbit's re-review: the documented `Rdp` struct in
docs/rdp.md now lists the `commands_processed` field added in ffb46b2,
keeping the docs-as-spec interface in sync with the code.

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

Copy link
Copy Markdown
Owner Author

@coderabbitai Adopted the outside-diff finding (lib.rs:96 / interface listing) in the latest commit: docs/rdp.md's Rdp struct now lists commands_processed, keeping the docs-as-spec interface in sync with the added field. All other findings from both review passes are adjudicated above.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@doublegate Confirmed. The Rdp interface listing is now consistent with the public commands_processed field; the outside-diff documentation finding is addressed. The Texture Rectangle two-word mapping is also correct.


✏️ Learnings added
Learnt from: doublegate
URL: https://github.com/doublegate/RustyN64/pull/56

Timestamp: 2026-07-22T07:39:38.190Z
Learning: In `crates/rustyn64-rdp`, Texture Rectangle and Texture Rectangle Flip RDP opcodes `0x24` and `0x25` are two 64-bit command words (16 bytes), not three.

Learnt from: doublegate
URL: https://github.com/doublegate/RustyN64/pull/56

Timestamp: 2026-07-22T07:39:38.190Z
Learning: For the docs-as-spec interface in `docs/rdp.md`, public fields added to `crates/rustyn64-rdp/src/lib.rs`'s `Rdp` struct, such as `commands_processed`, must be listed in the documented `Rdp` interface.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Adjudicating CodeRabbit: the 0x0E fixture comment now reads
"Fill Triangle (shade + texture)" instead of the abbreviated "(ST)",
matching the spelled-out 0x0F comment in the walk fixture and removing any
read as the base (non-shaded) Fill Triangle. Keeps the N64brew wiki name
"Fill Triangle" (the project's mirrored, canonical command map) rather
than an alternate source's "Non-Shaded Triangle" convention.

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

@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-rdp/src/lib.rs`:
- Line 418: Update the opcode 0x0E fixture in the push_cmd test to use the
canonical N64brew label “Fill Triangle (ST)”, optionally retaining the “shade +
texture” explanatory gloss. Keep the existing 20-word length and command
behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: af55d3b5-669f-464a-839a-7254e0bf013b

📥 Commits

Reviewing files that changed from the base of the PR and between f0862cc and 4e1c99d.

📒 Files selected for processing (1)
  • crates/rustyn64-rdp/src/lib.rs

Comment thread crates/rustyn64-rdp/src/lib.rs Outdated
…1 review)

Adjudicating CodeRabbit's follow-up: the 0x0E and 0x0F fixture comments
now carry the canonical N64brew command labels — Fill Triangle (ST) and
(STZ) — alongside the shade/texture/z gloss, matching the mirrored command
map's terminology. Lengths (20, 22) and behaviour unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@doublegate
doublegate merged commit f246ad1 into main Jul 22, 2026
10 checks passed
@doublegate
doublegate deleted the feat/rdp-command-decoder branch July 22, 2026 07:54
doublegate added a commit that referenced this pull request Jul 22, 2026
…32-001) (#64)

* docs(phase-3): close out Sprint 1 and plan Sprint 2

Sprint 1 (command decode, fill, VI scan-out) is complete: T-31-001..005 all
merged (#56-#63). Mark the acceptance criteria met and record the deliberate
deviations honestly rather than silently checking boxes:

- T-31-005's golden is an inlined FNV-1a hash constant (harness is hash-based),
  not a file under tests/golden/; it runs unconditionally (more coverage than
  gating behind test-roms); and the frame is a synthetic FILL command stream,
  not a booted ROM (real cart boot is Phase 5). The DP-FIFO-onward path is
  identical, so the picture path is fully exercised now.
- VI X/Y_SCALE resampling and AA/divot/de-dither are not implemented (1:1 scan,
  ledger R-5); per-register VI write masks are ledger R-4.
- The documented RDP hazards (T-31-002) are deferred to Sprint 3, where the
  render pipeline they govern exists.

Refine the Sprint 2 stub into real tickets (T-32-001..004), grounded in the
n64brew Commands.md texture-command encodings: TMEM + tile descriptors +
texture-state commands, Load Block/Load Tile, Load TLUT + texel-format
decoders, and the sampler + copy-mode Texture Rectangle. Point the overview at
the new plan and mark Sprint 1 COMPLETE / Sprint 2 in progress.

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

* feat(rdp): TMEM, tile descriptors, and the texture-state commands (T-32-001)

Give the RDP its texture state and the three commands that describe it, the
foundation the texel loads and the sampler build on. Pure state: no texel is
moved and no framebuffer pixel changes.

- `Rdp` owns a 4 KiB TMEM and eight `TileDescriptor`s, reset at power-on.
- Dispatch `Set Texture Image` (0x3D), `Set Tile` (0x35), and `Set Tile Size`
  (0x32), decoding every field per the N64brew command tables (format/size/line/
  tmem_addr/palette + per-axis clamp/mirror/mask/shift for Set Tile; SL/TL/SH/TH
  for Set Tile Size; format/size/width/addr for Set Texture Image). Set Tile
  preserves the disjoint tile-size coords via a struct-update spread.

TMEM is a lazily-allocated `Option<Box<[u8; 4096]>>`: it starts None (read as
all-zero) and allocates on first write. This keeps `Rdp`'s Default cheap, which
matters because `Bus::rdp_tick` does a `core::mem::take` every RCP step -- a None
placeholder swaps in with no 4 KiB allocation or copy, while the real TMEM box
moves by pointer. An inline array would memcpy ~12 KiB per tick; a plain boxed
default would allocate per tick. Neither touches the Bus seam.

The four clamp/mirror bools are the hardware's four independent bit-flags
(clamp+mirror per S/T axis), decoded straight from command bits, so
struct_excessive_bools is allowed with that justification rather than forcing an
enum that would misrepresent the register.

Field-by-field unit tests pin each decode with distinct per-field values so a
swapped bit range surfaces as a wrong field. docs/rdp.md gains the T-32-001
section and its State list is updated. n64-systemtest oracle unchanged at 93
(nothing rendered changed).

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

* docs,refactor(rdp): adjudicate bot review on the texture state

Follow-up to the CodeRabbit and Antigravity reviews of PR #64.

- TileDescriptor: order the T-axis fields (bits 19:10) before the S-axis fields
  (bits 9:0), matching the command word's MSB->LSB layout and the set_tile
  decoder (Antigravity). Field-init order in the literals was already T-first;
  this only aligns the struct declaration.
- tmem_byte: document that `offset` is a BYTE address, not the 64-bit-word
  address that Set Tile's tmem_addr/line use (a word address must be *8 first) --
  guards against an address-scaling bug in the upcoming load/sampler tickets
  (Antigravity).
- sprint-1 plan: reword the VI register criterion as read/write-through
  (store-and-read-back), not full "hardware semantics", since the per-register
  write masks are deferred (ledger R-4) -- the doc is the spec (CodeRabbit).
- sprint-2 plan: the TMEM acceptance criterion described an inline [u8; 4096],
  contradicting the lazily-allocated Option<Box<..>> the PR ships. Reword to the
  observable contract (reads as zero until written, resets at power-on; storage
  may be lazy) so the plan matches the implementation (CodeRabbit).

Deferred (Antigravity): a tmem_mut/ensure_tmem allocation helper -- it lands with
its first caller in T-32-002 (Load Block/Load Tile); adding it now would be dead
code with no load command to use it.

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

* test,docs(rdp): adjudicate CodeRabbit outside-diff review (T-32-001)

Three outside-the-diff comments from CodeRabbit's incremental review, all adopted.

- set_tile_size_decodes_coords: seed descriptor 2's addressing fields (format/
  size/line/tmem_addr) to non-zero via Set Tile before Set Tile Size, then assert
  they are preserved. The old test asserted `== 0` on a fresh descriptor, which
  would have passed even if Set Tile Size wrongly cleared them; this makes the
  preservation guard real.
- sprint-2 plan, Load TLUT criterion: require an INCLUSIVE entry count SH-SL+1
  (the typical (0,0,count-1,0) gives `count` entries) and require SL/TL/SH/TH to
  be latched into the tile descriptor.
- sprint-2 plan, Load Block over-limit: attribute the "writes nothing over 2048
  texels" behavior to its source (N64brew Commands.md Load Block) rather than
  stating it unsourced, and note the fuzz oracle supersedes it in Sprint 3 if the
  hardware does a partial write instead.

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

* test,docs(rdp): adjudicate CodeRabbit round-3 review (T-32-001)

Three more CodeRabbit comments (2 inline planning-doc, 1 outside-diff test), all
adopted.

- Add `dispatch_routes_set_tile_and_set_tile_size`: the field-level tests called
  set_tile/set_tile_size directly, bypassing the dispatch routing. This drives
  the actual `dispatch` entry for 0x35 and 0x32 with inputs that distinguish the
  handlers (Set Tile sets format=3 which Set Tile Size must preserve), so a
  mis-wired opcode arm is caught, not only a decode bug (outside-diff comment).
- sprint-2 Load Block criterion: pin both sides of the 2048-texel boundary --
  exactly 2048 loads fully, 2049 writes nothing -- to guard an off-by-one.
- sprint-2 Load TLUT criterion: require the base to be in the upper TMEM half AND
  aligned to 16 TMEM words (128 bytes), with the test covering both a below-0x100
  base and a misaligned high-half base, not only the lower bound.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
doublegate added a commit that referenced this pull request Jul 30, 2026
…ot lock-free

The cpal callback ran `Mutex::lock()`. That callback executes on the device's
real-time thread, so a blocking acquire there is a priority inversion: the audio
thread waits on the emulator thread, misses its deadline, and the device
underruns anyway. Blocking buys nothing and costs a glitch of unbounded length.

`pull` now uses `try_lock` and emits one buffer of silence when contended. The
trade is explicit and documented -- a contended callback produces silence where a
blocking one MIGHT have got real samples; bounded and bursty beats unbounded.
Both sides also transfer in bulk (a single drain/extend instead of per-sample
pop_front/push_back with a capacity test each iteration), which shortens the
critical section so the non-blocking acquire succeeds in practice rather than
merely in principle.

The module header called this "the lock-free audio ring" and the type "a simple
lock-free-ish SPSC ring". It is neither -- it is a Mutex<VecDeque<f32>>. Both now
say so, and the asymmetry that matters (producer may block, consumer must not) is
stated at the type. A genuinely wait-free ring needs unsafe or a new dependency
and stays a roadmap item.

WHAT THIS DOES NOT FIX, recorded so it is not re-diagnosed. The ~1s on / ~1s off
chopping is not a ring defect and not a resampler defect. `produce_audio` stages
one EMULATED frame of audio per emulated frame while the device consumes in
wall-clock time, so the supply ratio is exactly fps/60. At the ~10 FPS this core
sustains, the ring receives under a fifth of what it must deliver. No buffering
strategy manufactures the missing 80%; it closes when the core gets faster and
not before. Since docs/performance.md records that 60 FPS is out of reach for
this execution model, an explicit slow-running audio policy will eventually be
needed instead -- noted, not designed. #56 stays open for that reason; this PR is
scoped to the part that is a defect at ANY frame rate.

Tests: a contended pull must return silence rather than wait. The pull runs on
its own thread behind a recv_timeout so a regression FAILS in seconds with "pull
must return while the lock is held, not block on it" rather than wedging the test
binary until CI's job timeout -- the first version of this test did hang, which is
loud but a bad citizen, and was fixed before merge rather than rationalised.
Sequenced with channels, not sleeps, so there is no window to flake on; the
holder is released on both the pass and fail paths so a failure cannot strand the
puller. Mutation-checked both ways. Plus a test that a push larger than capacity
keeps the newest tail, the behavior the bulk rewrite had to preserve.

Review rejected two suggestions to recover from a poisoned mutex, on a checkable
fact now recorded in the docs: the release profile sets panic = "abort", so there
is no unwinding and a Mutex cannot be poisoned in a shipped build. Under
test/debug a poisoned lock means a panic already happened and was already
reported, so recovering the guard would continue on state that panic may have
interrupted. For the callback specifically, WouldBlock and Poisoned mean the same
actionable thing: no samples available to this call.

Refs #56.
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