Skip to content

feat(vi): framebuffer scan-out to RGBA8 (T-31-004 part 2) - #60

Merged
doublegate merged 5 commits into
mainfrom
feat/vi-scanout-frame
Jul 22, 2026
Merged

feat(vi): framebuffer scan-out to RGBA8 (T-31-004 part 2)#60
doublegate merged 5 commits into
mainfrom
feat/vi-scanout-frame

Conversation

@doublegate

Copy link
Copy Markdown
Owner

Motivation

Phase 3 (v0.4.0 "Rasteriser"), Sprint 1 — T-31-004 part 2, the framebuffer
scan-out
. Part 1 (#59) landed the VI register file; this converts the
framebuffer the FILL pipeline writes into a presentable RGBA8 frame — the
conversion that makes the pixels a picture.

Changes

  • Bus::scanout(&self, out) -> (w, h) reads VI_ORIGIN/VI_WIDTH/VI_CTRL
    and the active height from VI_V_VIDEO, converting the framebuffer to RGBA8:
    16-bit RGBA5551 (5→8-bit channel expansion, 1-bit alpha to 0/255) and
    32-bit RGBA8888 (direct copy). TYPE 0/1 (blank) returns (0, 0).

Scope (honest)

A 1:1 scanVI_X_SCALE/VI_Y_SCALE resampling and the AA/divot/de-dither
post-filters are deferred (open residual R-5); the pixel conversion itself is
exact and cited. No per-frame driver yet — the scheduler tick that calls
scanout lands with the V_CURRENT advance (part 3). The harness golden-frame
comparison (T-31-005) is the first real consumer.

Verification

  • Byte-for-byte 16-/32-bit conversion (white/black RGBA5551, direct RGBA8888) and
    the blank-VI case. The 5→8-bit expansion is mutation-checked (dropping the
    low-bit replication makes 0xFFFF map to 0xF8F8F8 instead of white).
  • Gates green: cargo fmt --check, clippy --workspace --all-targets -- -D warnings, cargo test --workspace, RUSTDOCFLAGS="-D warnings" cargo doc, the
    thumbv7em-none-eabihf no_std build, pre-commit run markdownlint.

🤖 Generated with Claude Code

Phase 3, Sprint 1's fourth ticket, part 2 -- the scan-out conversion that
turns the FILL pipeline's framebuffer into a presentable picture.

- Bus::scanout(&self, out) reads VI_ORIGIN/VI_WIDTH/VI_CTRL and the active
  height from VI_V_VIDEO ((V_END - V_START) half-lines -> lines), and
  converts the framebuffer to RGBA8: 16-bit RGBA5551 (each 5-bit channel
  widened to 8 by replicating the high bits, the 1-bit alpha to 0/255) and
  32-bit RGBA8888 (a direct copy). TYPE 0/1 (blank) returns (0, 0) and
  writes nothing.

Scope: a 1:1 scan -- VI_X_SCALE/VI_Y_SCALE resampling and the analog
post-filters (AA, divot, de-dither) are deferred, recorded as open
residual R-5 in the accuracy ledger; the pixel conversion itself is exact
and cited. There is no per-frame driver yet: the scheduler tick that calls
scanout lands with the V_CURRENT advance (T-31-004 part 3). The harness
golden-frame comparison (T-31-005) is the first real consumer.

Tests: byte-for-byte 16- and 32-bit conversion (white/black 5551 cases,
direct 8888 copy) and the blank-VI case. The 5->8-bit expansion is
mutation-checked (dropping the low-bit replication makes 0xFFFF map to
0xF8F8F8 instead of white).

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>
@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: bfe576f6-de89-4d53-bfcd-5e7ee4aa9862

📥 Commits

Reviewing files that changed from the base of the PR and between b1eccc0 and be0153f.

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

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added VI framebuffer scan-out that exports video memory to RGBA8 using active VI register state.
    • Supports direct RGBA8888 copying and RGBA5551 5→8 channel expansion with 0/255 alpha mapping; blank modes produce no output (returns zero dimensions).
    • If the destination is too small (or the VI format is not supported), scan-out is skipped and the output buffer remains unchanged.
  • Tests
    • Added unit tests covering RGBA8888/5551 paths, alpha mapping, blank behaviour, and undersized destinations.
  • Documentation
    • Updated accuracy notes/ledger and VI scan-out documentation, including current 1:1, unfiltered scope and remaining residuals.
  • Chores
    • Documented the work in the changelog.

Walkthrough

Bus::scanout converts VI-selected RDRAM framebuffer data to RGBA8 for 16-bit and 32-bit formats, handles blank or undersized output, and derives dimensions from VI state. Tests cover conversion and validation. Documentation records the implementation and deferred scaling and filtering work.

Changes

VI framebuffer scan-out

Layer / File(s) Summary
Scan-out conversion and validation
crates/rustyn64-core/src/bus.rs
Adds Bus::scanout, RGBA5551 channel expansion, RGBA8888 copying, blank and undersized-output handling, and tests for these paths.
Scan-out status and residuals
docs/rdp.md, docs/accuracy-ledger.md, CHANGELOG.md
Documents the implemented 1:1 conversion, scheduler status, deferred scaling and filters, and residual R-5.

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

Sequence Diagram(s)

sequenceDiagram
  participant VI
  participant Scanout
  participant RDRAM
  participant Output
  VI->>Scanout: provide framebuffer configuration
  Scanout->>RDRAM: read framebuffer pixels
  RDRAM-->>Scanout: return pixel data
  Scanout->>Output: copy or convert to RGBA8
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 9 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title matches Conventional Commits format, but the subject is not in imperative mood as required. Rewrite the subject in imperative form, e.g. "feat(vi): add framebuffer scan-out to RGBA8".
✅ Passed checks (9 passed)
Check name Status Explanation
Description check ✅ Passed The description is clearly about VI framebuffer scan-out and matches the change set.
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 docs/accuracy-ledger.md R-5 and docs/rdp.md both state the n64-systemtest effect was not measured; docs/STATUS.md gives the current oracle count as 93.
Docs-As-Spec Sync ✅ Passed PASS: rustyn64-core’s scan-out change is documented in docs/rdp.md, and R-5 in docs/accuracy-ledger.md records the deferred parts; the PR body explains the scope.
Changelog Entry For User-Visible Changes ✅ Passed PASS — CHANGELOG.md has a new [Unreleased] entry for the user-visible VI framebuffer scan-out feature.
Measured, Never Tuned ✅ Passed PASS: the scan-out values are covered by docs/accuracy-ledger.md R-5 and docs/rdp.md; no uncited hardware constant or timing value was added.
Unsafe Stays Out Of The Chip Crates ✅ Passed No unsafe syntax appears in non-frontend Rust code, and the six non-frontend crate roots still #![forbid(unsafe_code)]; frontend has no unsafe blocks.

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

@github-actions

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This PR implements 1:1 VI framebuffer scan-out (Bus::scanout) to convert 16-bit RGBA5551 and 32-bit RGBA8888 framebuffers into RGBA8 pixel buffers.

Blocking issues

  • Silent failure on undersized output buffer (bus.rs): When out.len() is smaller than the required width * height * 4 bytes, scanout aborts mid-frame and returns (width, height). Returning full dimensions when scan-out was incomplete creates a silent failure path where the caller cannot detect that out contains truncated, partial frame data. Validate out.len() upfront and return (0, 0) if the provided buffer is insufficient.

Suggestions

  • Validate required buffer length upfront (bus.rs): Evaluating dst + 4 > out.len() inside the per-pixel inner loop adds unnecessary bounds checks to every pixel iteration. Validate out.len() < (width as usize) * (height as usize) * 4 once before starting the loop.
  • Mask input in expand5 to guard against overflow panics (bus.rs): expand5 performs v5 << 3 on u8. If called with any value greater than 31, debug builds will panic due to shift overflow. Apply (v5 & 0x1F) inside expand5 to ensure domain safety.

Nitpicks

  • Redundant integer casting (bus.rs): Iterate over 0..4usize to avoid casting i as usize inside the byte copy loop.

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

…4 review)

Adjudicating the Antigravity review of #60:
- Validate the destination up front: scanout returns (0, 0) and writes
  nothing when out is smaller than width*height*4, rather than aborting
  mid-frame and claiming full dimensions with truncated data. This also
  removes the per-pixel bounds check. Mutation-checked (removing the guard
  panics on the out-of-bounds write).
- Mask expand5's input to 5 bits so an out-of-range argument cannot
  overflow the shift in debug (all call sites already pass <= 31; belt and
  suspenders).
- Replace the 32-bit byte-copy loop with rdram_read_u32().to_be_bytes()
  into a slice copy -- clearer than the nitpick's cast shuffle, and no per-
  byte trait calls.

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

Copy link
Copy Markdown
Owner Author

Adjudication of the Antigravity review

Fixes in the latest commit.

Blocking

  • Silent failure on undersized buffer — ADOPTED. scanout now validates
    out.len() >= width * height * 4 up front and returns (0, 0) writing
    nothing when it can't hold the frame, so a non-zero return means the whole
    frame was written. New test scanout_refuses_an_undersized_buffer;
    mutation-checked (removing the guard panics on the out-of-bounds write).

Suggestions

  • Validate length up front (perf) — ADOPTED, same change: the per-pixel
    bounds check is gone.
  • Mask expand5 input — ADOPTED. expand5 masks to 5 bits first, so an
    out-of-range argument can't overflow << 3 in debug. (All call sites already
    pass & 0x1F values; this is belt-and-suspenders.)

Nitpicks

  • Redundant cast in the byte loop — ADOPTED with modification. Rather than
    swap i as usize for i as u32, the 32-bit path now does
    out[dst..dst+4].copy_from_slice(&self.rdram_read_u32(src).to_be_bytes())
    no loop, no cast, and one word read instead of four byte reads.

@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: 5

🤖 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-core/src/bus.rs`:
- Around line 447-449: Update the frame-writing logic around the dst + 4 bounds
check to preflight the complete required RGBA output length before modifying
out. When the buffer is insufficient, return the established explicit
insufficient-buffer result rather than (width, height), and ensure no partial
frame data is written.
- Around line 1209-1215: Strengthen scanout_is_blank_when_the_vi_is_off to
pre-fill the destination with a nonzero sentinel, then verify it remains
unchanged after blank scan-out. Extend the test to cover both blank VI types,
TYPE == 0 and TYPE == 1, while preserving the expected (0, 0) result and
confirming neither path writes to the output.
- Around line 1198-1206: Update the scanout fixture in the RGBA5551 test around
bus.scanout to include a non-uniform pixel such as 0x0887, replacing or
supplementing the uniform white/black cases. Assert its observable RGBA output
is [0x08, 0x10, 0x18, 0xFF] so component ordering and field shifts are covered.

In `@docs/rdp.md`:
- Around line 280-282: Correct the scheduler description near the “VI_V_CURRENT
firing / the scheduler tick” heading: describe the scheduler advancing
VI_V_CURRENT and raising the VI interrupt at VI_V_INTR, rather than saying
VI_V_CURRENT fires. Preserve the surrounding scan-out scheduling and conversion
context.
- Around line 296-314: The scan-out verification record needs explicit
validation status. In docs/rdp.md lines 296-314, state the measured
n64-systemtest failing-assertion count, or explicitly document that it was not
measured and why; in docs/accuracy-ledger.md line 188, replace “are validated”
with wording that describes validation as planned while keeping R-5 open.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a6c3c18f-90de-4c56-ac09-0cf346028202

📥 Commits

Reviewing files that changed from the base of the PR and between 10efb38 and a3673a9.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • crates/rustyn64-core/src/bus.rs
  • docs/accuracy-ledger.md
  • docs/rdp.md

Comment thread crates/rustyn64-core/src/bus.rs Outdated
Comment thread crates/rustyn64-core/src/bus.rs Outdated
Comment thread crates/rustyn64-core/src/bus.rs Outdated
Comment thread docs/rdp.md Outdated
Comment thread docs/rdp.md
…es (T-31-004 review)

Adjudicating the CodeRabbit review of #60:
- RGBA5551 test now uses non-uniform channels (0x0887 -> [08,10,18,FF] and
  0x0886 -> [08,10,18,00]) so component order, the field shifts, and both
  alpha states are covered -- white/black could not distinguish channels.
- Blank-scanout test pre-fills a 0xA5 sentinel and asserts it is untouched,
  and covers both blank types (TYPE 0 and 1); a zero-filled buffer would
  have passed a blank path that wrongly wrote zeroes.
- docs/rdp.md: describe the scheduler advancing VI_V_CURRENT and raising the
  VI interrupt at VI_V_INTR, not VI_V_CURRENT 'firing'; and record the
  oracle effect explicitly (not measured -- scanout has no runtime driver,
  so it is unreachable by n64-systemtest; count stands at 93).
- accuracy-ledger R-5: 'are validated' -> 'will be validated' (the entry is
  open until the fuzz suite runs).

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

Caution

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

⚠️ Outside diff range comments (3)
crates/rustyn64-core/src/bus.rs (3)

1191-1204: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the complete RGBA8888 frame.

The 2×2 fixture checks only the first and last pixels, so an implementation that skips or corrupts either middle pixel still passes. Assert all four output pixels to cover the row/column traversal.

As per coding guidelines, tests must verify observable effects and choose cases that fail when the implementation is broken.

🤖 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-core/src/bus.rs` around lines 1191 - 1204, Update the scanout
test around the 2×2 framebuffer fixture and its existing output assertions to
validate all four RGBA8888 pixels in row-major order. Replace the partial
first/last-pixel checks with complete coverage of the output buffer, preserving
the expected px32 values for both middle pixels.

Source: Coding guidelines


29-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the expand5 guard mutation-testable or remove it.

Every current caller already masks its extracted channel with 0x1F, so deleting v5 & 0x1F does not change any scanout result and the tests will still pass. Either remove the redundant guard or add a focused test for its out-of-range contract.

As per coding guidelines, tests must mutation-check guards by proving that they fail without the fix.

🤖 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-core/src/bus.rs` around lines 29 - 34, The `expand5` mask
guard is currently redundant and untested. In `expand5`, either remove the `v5 &
0x1F` masking or add a focused test that passes an out-of-range value and
verifies the documented masked result, ensuring the test fails if the guard is
removed.

Source: Coding guidelines


444-445: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle reversed VI_V_VIDEO ranges explicitly (crates/rustyn64-core/src/bus.rs:444). docs/rdp.md and accuracy ledger R-5 only define height as (V_END − V_START) half-lines; saturating_sub adds an unlisted zero-height fallback for V_START > V_END. Either reject that state or record the measured policy in docs/accuracy-ledger.md.

🤖 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-core/src/bus.rs` around lines 444 - 445, Update the height
calculation near the VI timing validation in bus.rs to handle V_START > V_END
explicitly instead of silently using saturating_sub; reject the reversed
VI_V_VIDEO range through the existing invalid-timing path, or document the
chosen measured behavior in docs/accuracy-ledger.md while preserving the defined
(V_END − V_START) half-line calculation.

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 `@docs/accuracy-ledger.md`:
- Line 188: The R-5 accuracy-ledger entry lacks precise, falsifiable provenance
for its conversion and deferred VI behavior claims. Update the R-5 row to cite
exact N64brew manual/wiki sections or named measured test matrices covering
RGBA5551 5→8-bit widening, RGBA8888 channel ordering, VI_X_SCALE/VI_Y_SCALE
sampling, and AA/divot/de-dither filtering, while preserving the existing
open-status and scope.

---

Outside diff comments:
In `@crates/rustyn64-core/src/bus.rs`:
- Around line 1191-1204: Update the scanout test around the 2×2 framebuffer
fixture and its existing output assertions to validate all four RGBA8888 pixels
in row-major order. Replace the partial first/last-pixel checks with complete
coverage of the output buffer, preserving the expected px32 values for both
middle pixels.
- Around line 29-34: The `expand5` mask guard is currently redundant and
untested. In `expand5`, either remove the `v5 & 0x1F` masking or add a focused
test that passes an out-of-range value and verifies the documented masked
result, ensuring the test fails if the guard is removed.
- Around line 444-445: Update the height calculation near the VI timing
validation in bus.rs to handle V_START > V_END explicitly instead of silently
using saturating_sub; reject the reversed VI_V_VIDEO range through the existing
invalid-timing path, or document the chosen measured behavior in
docs/accuracy-ledger.md while preserving the defined (V_END − V_START) half-line
calculation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c85bd60c-3303-4536-89f3-df3b573be2f9

📥 Commits

Reviewing files that changed from the base of the PR and between 8770c75 and c339f4e.

📒 Files selected for processing (3)
  • crates/rustyn64-core/src/bus.rs
  • docs/accuracy-ledger.md
  • docs/rdp.md

Comment thread docs/accuracy-ledger.md Outdated
Adjudicating CodeRabbit: R-5 now cites exact N64brew sections rather than
article titles -- VI_CTRL.TYPE[1:0] for the RGBA5551/8888 format select,
Set Color Image / Video DAC for the 5551 bit layout and the standard 5->8
high-bit-replication widening, and VI_X_SCALE/VI_Y_SCALE + VI_CTRL
(AA_MODE/DIVOT_ENABLE/dedither) for the deferred sampling and post-filters.

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 `@docs/accuracy-ledger.md`:
- Line 188: Update the R-5 entry in the accuracy ledger to include an explicit
n64-systemtest impact statement, indicating whether the failing-assertion count
was measured; if not, state that it was not measured because the timing set has
not been run.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6f3150bf-b9cc-4680-8c9f-f55d722d01e7

📥 Commits

Reviewing files that changed from the base of the PR and between c339f4e and b1eccc0.

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

Comment thread docs/accuracy-ledger.md Outdated
Adjudicating CodeRabbit: R-5 now carries the explicit oracle-impact
statement in the entry itself -- not measured, because Bus::scanout has no
runtime driver and is unreachable by the suite (count stands at 93).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@doublegate
doublegate merged commit 0f5b06e into main Jul 22, 2026
10 checks passed
@doublegate
doublegate deleted the feat/vi-scanout-frame branch July 22, 2026 16:12
doublegate added a commit that referenced this pull request Jul 22, 2026
…lamp (T-31-004 review)

Adjudicating the Antigravity review of #62:
- Clear the entire frame buffer on the black fallback (was only the default
  region), so no stale pixels from a previous larger frame survive.
- Extract the geometry decision into a pure presentable_geometry() so the
  FB_MAX clamp is unit-testable without configuring the core VI; new test
  covers zero, exact-FB_MAX, too-wide, and too-tall.
- Correct the doc comment: Bus::scanout self-guards against a buffer
  overrun (it returns (0,0) when its output cannot hold w*h*4, the guard
  added in the #60 review), so a ROM cannot overrun frame.rgba; the FB_MAX
  clamp is a blit-geometry bound, not overrun prevention.

Rejected the overrun framing: scanout's internal capacity guard (core-
tested by scanout_refuses_an_undersized_buffer) makes an overrun
impossible, so the post-hoc FB_MAX check is defensive, not a safety hole.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
doublegate added a commit that referenced this pull request Jul 30, 2026
Audio::tick opened by computing period_ticks() -- MASTER_HZ / sample_rate, a
64-bit divide -- and only then asked whether a sample was due. The scheduler calls
it on every RCP step, about 1.04M times a frame, while at ~32 kHz the period is
~5,859 master ticks. So roughly 1,950 of every 1,951 calls divided and threw the
quotient away. That one line was 3.67% of a rendering frame, the largest source
line outside the CPU pipeline.

Ordering, not caching: return before the divide when a schedule already exists and
its next sample is still ahead. Precisely that -- the divide is still performed on
the two paths that need it, when a sample is due and when next_sample_tick is zero
and the first one must be anchored. "Only when due" would overstate it.

Behavior-identical rather than approximately so: on the skipped path the old code
either returned at the period == 0 guard or fell into a while whose condition is
exactly the negation of the new test, and neither route touches a field. A memo
field was rejected -- the quotient is derived from sample_rate, and caching derived
state in a serialized struct would change the save-state layout (ADR 0005) to buy
what the reordering buys for nothing.

A-B-A in one sitting, Super Mario 64, --release, frame_cost_probe:

  A  before        107.413 / 107.587 ms
  B  after         103.447 / 103.175 ms
  A  before again  107.652 ms

Three A legs within 0.22%, so the session did not drift. 107.55 -> 103.31 ms,
1.041x; the conservative pairing gives 1.038x. The profile predicted 3.67%.

The R-16 debug_assert is kept reachable on the new fast path, where it compiles out
of release entirely, rather than firing only on the ~0.05% of calls that emit. It
asserts the same invariant in the same situations the old guard did: period == 0 is
reachable only when sample_rate == 0, and AI_DACRATE is 14 bits so the divide
cannot floor to zero.

Mutation-checked, and the existing suite failed the check. Breaking the early-out
grossly is caught by seven tests, but the off-by-one that defers every sample by one
RCP step left the whole workspace green -- the other AI tests advance `now` in
strides of a full period and never land on the boundary. That case now has a test
which goes red under the mutation and green without it.

Two review rounds corrected claims that did not match the code: the new test called
tick() first and asserted it anchored the schedule, when write_reg had already set
next_sample_tick so that call took the early return; and docs/audio.md said the
period is computed "only when a sample is actually due".

Also in this change, documentation only:

- The render-phase attribution map, re-measured. The previous one was taken at
  138.7 ms/frame, before the scan-out memo (#216) and the RDP split-borrow skip
  (#219), so it was stale for exactly those buckets. Both columns now carry an
  explicit remainder and total, with a note that they are not like-for-like.

- The 60 FPS target, bounded by measurement. CPU buckets plus scheduler dispatch
  are 53.5% of a frame; setting both to zero caps the win at 2.15x (48.0 ms,
  20.8 FPS). ADR 0011 remains the largest single win left but is not a path to
  60 FPS, and the task list's claim that it was is retired. A dynarec is presented
  as a proposed route with its costs -- ADR 0011 leaves the mechanism open, and
  unsafe is already permitted in the frontend and FFI, so it is a placement and
  own-ADR question rather than an impossibility.

- A null result that retires the "split Latch" task without implementing it. Six
  pipeline.rs lines are all the same inter-stage latch copy and sum to 16.1% of a
  frame. Adding [u64; 9] of padding to Latch -- 120 to 192 bytes, per-cycle latch
  traffic 840 to 1344, +60% -- produced no sign of the roughly +10% that a
  transfer-bound cost predicts. perf is charging each stage's retired work to the
  store that ends it. Recorded with full provenance, and explicitly NOT quoting its
  ~1% delta: that probe was two-leg, not A-B-A, and 1% is inside session drift.
  Refuting a 10% prediction does not require resolving 1%.

Refs #55. Retires #60.
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