Skip to content

feat(rdp): wire register-sourced exotic combiner inputs (R-10) - #147

Merged
doublegate merged 6 commits into
mainfrom
feat/r10-combiner-register-inputs
Jul 25, 2026
Merged

feat(rdp): wire register-sourced exotic combiner inputs (R-10)#147
doublegate merged 6 commits into
mainfrom
feat/r10-combiner-register-inputs

Conversation

@doublegate

Copy link
Copy Markdown
Owner

Motivation

Ledger residual R-10 (gap-analysis Stage D toward v0.8.0 "Breadth"): the colour
combiner modelled the common inputs but read every exotic input as zero. This PR
closes the register-sourced subset — the inputs that need no new pipeline, only a
register decode and a mux wire.

Changes

  • Set Prim Color (0x3A) now extracts prim_lod_frac (and min_level) from its
    word-0 low byte.
  • New Set Convert (0x2C) dispatch extracts K4/K5 (raw 9-bit, lo[17:9]/lo[8:0]).
  • Combiner input mux wires prim_lod_frac → RGB mul-select 14 + alpha mul-select 6,
    K4 → RGB sub-B select 7, K5 → RGB mul-select 15.

Oracle validation (pin-the-oracle-first)

Two new non-vacuous Angrylion conformance vectors, byte-for-byte:

Vector Combine Golden Unwired
tex_tri_primlodfrac_16 One * prim_lod_frac (0x80) 0x8421 gray black
tex_tri_convert_k45_16 (One - K4) * K5 (0x40/0xC0) 0x94a5 gray black

Plus two mutation-checked unit tests (combine_cycle_routes_prim_lod_frac,
combine_cycle_routes_convert_k4_k5); the K5 select was reverted to confirm both the
unit test and the conformance vector go red, then restored.

Still open (documented in R-10, read as zero)

Noise (needs a per-pixel noise source), the derivative-computed lod_frac (needs the
LOD/mip pipeline, pairs with R-13), the chroma-key centre/scale (needs the key registers +
key_en), and the YUV convert K0K3 coefficients.

Gates run locally

cargo fmt --all --check, cargo clippy --workspace --all-targets -D warnings,
cargo test --workspace (29 conformance incl. 2 new, 104 rdp units incl. 2 new),
RUSTDOCFLAGS=-D warnings cargo doc, no_std thumbv7em build, and
pre-commit run markdownlint — all green.

🤖 Generated with Claude Code

Route PRIM_LOD_FRAC and the Set Convert K4/K5 constants through the colour
combiner instead of reading zero (ledger R-10, gap-analysis Stage D).

- Set Prim Color (0x3A) extracts prim_lod_frac + min_level from its word-0
  low byte; a new Set Convert (0x2C) dispatch extracts K4/K5 (raw 9-bit,
  lo[17:9]/lo[8:0]).
- The combiner input mux wires prim_lod_frac to RGB mul-select 14 and alpha
  mul-select 6, K4 to RGB sub-B select 7, and K5 to RGB mul-select 15.
- Two non-vacuous Angrylion conformance vectors pin the wiring byte-for-byte:
  tex_tri_primlodfrac_16 (One * prim_lod_frac, gray vs black if unwired) and
  tex_tri_convert_k45_16 ((One - K4) * K5, gray vs black if unwired).
- Two mutation-checked unit tests (combine_cycle_routes_prim_lod_frac,
  combine_cycle_routes_convert_k4_k5).

Still open (read as zero until their machinery exists): noise, the
derivative-computed lod_frac, the chroma-key centre/scale, and the YUV
convert K0-K3 coefficients.

Docs: ledger R-10 RESOLUTION, docs/rdp.md combiner scope, CHANGELOG.

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

coderabbitai Bot commented Jul 25, 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: 1 minute

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: 4054c361-c84c-4f40-8ba0-fd433fa431e4

📥 Commits

Reviewing files that changed from the base of the PR and between 29e70cf and a68610b.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • crates/rustyn64-rdp/src/lib.rs
  • crates/rustyn64-test-harness/tests/rdp_conformance.rs
  • docs/accuracy-ledger.md
  • docs/rdp.md
📝 Walkthrough

Walkthrough

The RDP now latches PRIM_LOD_FRAC, K4, and K5, exposes them through combiner inputs, routes their mux selects, and validates the paths with unit tests and three Angrylion-backed conformance vectors.

Changes

Register-sourced combiner inputs

Layer / File(s) Summary
Combiner input contracts and mux routing
crates/rustyn64-rdp/src/lib.rs
CombinerInputs exposes the new values and routes them through the RGB and alpha selector tables, with unit coverage.
RDP state dispatch and colour assembly
crates/rustyn64-rdp/src/lib.rs
Set Prim Color and Set Convert latch the register values, and combined_color passes them into the combiner.
Golden vectors and documented coverage
crates/rustyn64-test-harness/..., CHANGELOG.md, docs/...
Three 16-bit vectors cover primitive LOD fraction, positive K4/K5, and negative K4 sign extension; conformance and status documentation are updated.

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

Sequence Diagram(s)

sequenceDiagram
  participant RDPCommandDispatcher
  participant Rdp
  participant combined_color
  participant Combiner
  participant ConformanceHarness
  RDPCommandDispatcher->>Rdp: latch PRIM_LOD_FRAC, K4, and K5
  Rdp->>combined_color: provide latched render state
  combined_color->>Combiner: supply combiner inputs
  Combiner->>ConformanceHarness: produce framebuffer output
  ConformanceHarness->>ConformanceHarness: compare against RVEC golden vectors
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 10
✅ Passed checks (10 passed)
Check name Status Explanation
Title check ✅ Passed The title matches Conventional Commits, stays within 72 characters, and accurately summarises the register-sourced combiner wiring change.
Description check ✅ Passed The description is directly related to the RDP combiner changes and the new validation coverage.
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.
Oracle Number Is Stated ✅ Passed CHANGELOG.md’s combiner entry says “oracle stays 93 (no systemtest drives the render path)”, which explicitly states the n64-systemtest effect.
Docs-As-Spec Sync ✅ Passed PASS: the R-10 colour-combiner behaviour change in rustyn64-rdp has a same-PR docs/rdp.md update, and docs/accuracy-ledger.md records the same resolution.
Changelog Entry For User-Visible Changes ✅ Passed PASS: CHANGELOG.md has a new [Unreleased] entry for the user-visible R-10 combiner feature, so the changelog requirement is satisfied.
Measured, Never Tuned ✅ Passed PASS: accuracy-ledger R-10 records the new register-sourced constants and their Angrylion validation; no untethered hardware timing values were added.
Unsafe Stays Out Of The Chip Crates ✅ Passed PASS: crates/rustyn64-rdp/src/lib.rs still has #![forbid(unsafe_code)], and a repo-wide structural scan found no unsafe blocks/functions outside rustyn64-frontend.

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

@doublegate

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…ector

Adjudicating the Antigravity review on #147. The bot flagged K4/K5 as stored
raw-unsigned without sign extension. The sign extension is real but lives one
layer down in `combine_channel` (`special_expand` for the K4 sub-B input,
`sext9` for the K5 mul), matching Angrylion, which likewise stores K4/K5 raw
0..511 (`rdp_set_convert`) and sign-extends in the equation
(`special_9bit_exttable` / `SIGNF(c,9)`). Signing at decode would double-apply.

The positive `tex_tri_convert_k45_16` vector could not reach the bit-8-set path,
so this adds `tex_tri_convert_kneg_16` (negative `K4 = 0x1C0` = -64, golden
`0x5295` gray) that oracle-proves it byte-for-byte — a raw-positive read would
clamp black. Also sharpens the K4/K5 doc comments to name the exact mechanism
and cite Angrylion, so the raw-storage decision reads as deliberate.

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

Copy link
Copy Markdown
Owner Author

Adjudication of the Antigravity review

Thanks — the review surfaced a genuinely under-tested path, and the fix is a stronger oracle rather than a code change. Details per item:

Blocking issue (K4/K5 sign extension) — rejected as stated, but hardened

The premise ("sign extension never happens") is incorrect: it happens one layer below the input mux, in combine_channel, exactly as the reference does it.

  • Angrylion stores K4/K5 raw-unsigned too. rdp_set_convert (tex.c:1020): k4 = (args[1] >> 9) & 0x1ff; k5 = args[1] & 0x1ff; — the same raw 0..511 this PR stores. Sign-extending at decode would diverge from the oracle (and double-apply below).
  • The sign extension is in the equation. color_combiner_equation (combiner.c:124-126): the sub-B input (K4) goes through special_9bit_exttable[b], and the mul input (K5) through SIGNF(c, 9).
  • RustyN64 reproduces both bit-for-bit. special_9bit_exttable[i] = ((i & 0x180) == 0x180) ? (i | ~0x1ff) : (i & 0x1ff) (combiner.c:481) is exactly what special_expand(v) = sext9(v - 0x80) + 0x80 computes (checked across the range: 0x180 → -128, 0x1FF → -1, 0x100 → 256); sext9 is SIGNF(·, 9).

To make this oracle-proven rather than reasoned, I added tex_tri_convert_kneg_16: a negative K4 = 0x1C0 (raw 448, -64 expanded). The combine (One - K4) * K5 = (256 - (-64)) * 64 >> 8 = 80 renders gray 0x5295, matching Angrylion byte-for-byte — a raw-positive read would give (256 - 448) * 64 < 0 → clamped black. This exercises the exact bit-8-set path the positive vector could not.

Suggestion (sign-extend at decode) — rejected

Same reason: Angrylion holds the raw value and expands in the equation. Signing Rdp::k4/k5 at decode would double-apply the extension in combine_channel and break the byte-for-byte match (the new negative vector would fail).

Nitpick (doc comments) — adopted with modification

The old comments were accurate but terse. I rewrote the CombinerInputs::k4/k5 docs, the two input-mux comments, and the OP_SET_CONVERT comment to name the exact mechanism (special_expand for the K4 sub-B, sext9 for the K5 mul), state that the raw 0..511 storage is deliberate, and cite Angrylion — so the intent is unambiguous.

Fixed in 1582f47.

Adjudicating the second Antigravity review on #147.

- CombinerInputs gains #[non_exhaustive], matching Rdp — both grow every sprint
  as more combiner inputs are wired, so a new field stays a compatible change.
  (Rdp was already non_exhaustive; that half of the finding did not apply.)
- Remove the min_level field: it was extracted from Set Prim Color but never
  read. Unread state is drift; it lands with its LOD/mip consumer. The word
  layout stays documented in the dispatch comment.
- Note that Set Convert's ignored lo[31:18] carries the deferred YUV K0..K3.

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

Copy link
Copy Markdown
Owner Author

Adjudication of the second Antigravity review

Fixed in 67f4812.

Blocking (public-API breaking change without #[non_exhaustive]) — half adopted, half N/A

  • Rdp is already #[non_exhaustive] (see the attribute on its declaration — "this render state grows every sprint"), so adding fields to it was already compatible. That half does not apply.
  • CombinerInputs was not marked, though it grows the same way — good catch. Adopted: it now carries #[non_exhaustive], matching Rdp. (No external crate constructs it exhaustively — every site uses ..Default::default() — so this is safe, and it makes future exotic-input additions compatible by construction.)

Suggestion (min_level unread) — adopted (removed)

min_level was extracted from Set Prim Color but never read. Per this project's discipline against carrying unread/unvalidated state, I removed the field rather than reserving it — it lands with its actual consumer (the deferred LOD/mip path). The word layout (min_level[12:8], prim_lod_frac[7:0]) stays documented in the dispatch comment.

Suggestion (Set Convert ignored lo[31:18]) — adopted (comment)

Expanded the OP_SET_CONVERT comment: lo[31:18] carries the low bits of the YUV convert coefficients K0..K3, which are deferred (R-10), so ignoring them is deliberate.

Nitpick (prim_lod_frac u8 vs i16, document range) — already satisfied

CombinerInputs::prim_lod_frac's doc already states the valid range 0..=255. The u8i16 widening is intentional (the combiner works in signed i16) and lossless, as the nitpick notes.

@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

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)

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

Refresh the combiner contract rustdoc.

The public combine_cycle documentation still says exotic inputs read as zero, but these arms now wire PRIM_LOD_FRAC, K4, and K5. Restrict that claim to noise, derivative LOD, key inputs, and K0–K3 so the code contract matches R-10 and docs/rdp.md.

Also applies to: 694-695

🤖 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` around lines 629 - 652, Update the public
rustdoc for combine_cycle to remove the broad claim that exotic inputs read as
zero. Restrict that statement to noise, derivative LOD, key inputs, and K0–K3,
while documenting PRIM_LOD_FRAC, K4, and K5 as implemented inputs consistent
with the rgb_input_c and corresponding channel mappings.
🤖 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 `@CHANGELOG.md`:
- Around line 13-16: Update the shared min_level documentation: in CHANGELOG.md
lines 13-16, remove “and min_level” from the Set Prim Color extraction claim; in
docs/accuracy-ledger.md line 391, state that min_level remains deferred in bits
12:8 and is not stored, while preserving the prim_lod_frac low-byte extraction
description.

In `@crates/rustyn64-test-harness/tests/rdp_conformance.rs`:
- Around line 419-423: Correct the documentation for the Set Convert K4/K5 case
near tex_tri_convert_k45_16 to identify the bit-8-clear vector’s golden result
as 0x94A5 instead of 0x5295. Keep 0x5295 associated only with the negative-K4
vector and clarify the comments so the two R-10 conformance cases are not
conflated.

In `@docs/accuracy-ledger.md`:
- Line 391: Update the R-10 resolution entry to explicitly state the measured
impact on n64-systemtest’s failing-assertion count, or state that the impact was
not measured. Keep the existing Angrylion, conformance-vector, and oracle
results unchanged.
- Line 391: Update the R-10 resolution text to state only that PRIM_LOD_FRAC is
extracted from the low byte of the Set Prim Color word-0; remove any claim that
min_level is latched or stored, and keep min_level deferred with the
derivative-computed LOD machinery.

In `@docs/rdp.md`:
- Around line 398-405: Update the R-10 scope in the documentation to include
tex_tri_convert_kneg_16 alongside tex_tri_primlodfrac_16 and
tex_tri_convert_k45_16. Preserve the existing description while explicitly
listing all three conformance vectors, including the negative-K4 raw 9-bit
storage and sign-extension coverage.

---

Outside diff comments:
In `@crates/rustyn64-rdp/src/lib.rs`:
- Around line 629-652: Update the public rustdoc for combine_cycle to remove the
broad claim that exotic inputs read as zero. Restrict that statement to noise,
derivative LOD, key inputs, and K0–K3, while documenting PRIM_LOD_FRAC, K4, and
K5 as implemented inputs consistent with the rgb_input_c and corresponding
channel mappings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4cbc00c4-d9f3-48d6-96f5-28bd6a0568b5

📥 Commits

Reviewing files that changed from the base of the PR and between a5e8e58 and 67f4812.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • crates/rustyn64-rdp/src/lib.rs
  • crates/rustyn64-test-harness/tests/rdp_conformance.rs
  • crates/rustyn64-test-harness/tests/vectors/tex_tri_convert_k45_16.rvec
  • crates/rustyn64-test-harness/tests/vectors/tex_tri_convert_kneg_16.rvec
  • crates/rustyn64-test-harness/tests/vectors/tex_tri_primlodfrac_16.rvec
  • crates/rustyn64-test-harness/vectors-gen/driver.c
  • docs/accuracy-ledger.md
  • docs/rdp.md

Comment thread CHANGELOG.md
Comment thread crates/rustyn64-test-harness/tests/rdp_conformance.rs
Comment thread docs/accuracy-ledger.md Outdated
Comment thread docs/rdp.md
…puts

Adjudicating the third Antigravity review on #147. CombinerInputs is transient
per-pixel state (built in combined_color, never stored in System), so it is
never part of a save-state — yet it derived Serialize/Deserialize, which is what
led the reviewer to flag a (non-existent) save-state compat break. Dropping the
unused derives removes the confusion at its root and is the correct design for
transient state.

For the record on the reviewer's premise: save-states use bincode over the whole
System (a positional format), so #[serde(default)] would not help missing fields
anyway; the persistent RDP fields live on Rdp (which IS serialised, per ADR 0004's
requirement that new hidden state be reachable by the serialiser) and same-version
round-trip holds.

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

Copy link
Copy Markdown
Owner Author

Adjudication of the third Antigravity review

Fixed in 29e70cf.

Blocking (missing #[serde(default)] breaks state deserialization) — root-caused and fixed, not as suggested

The premise doesn't hold for this codebase, but it pointed at a real smell:

  • CombinerInputs is never serialized. It's transient per-pixel state, built fresh in combined_color and discarded — it is not a field of System/Rdp (grep: zero serde consumers, not a field anywhere). No save-state ever contains it, so nothing to break. It only looked persistent because it derived Serialize/Deserialize. I removed those vestigial derives — that eliminates the concern at its source and is the correct design for transient state.
  • #[serde(default)] wouldn't work anyway. Save-states are bincode over the whole System (frontend/src/emu.rs:153). bincode is positional/non-self-describing — it has no field names and does not skip or default missing trailing fields, so #[serde(default)] has no effect on old-blob loading. The suggested fix would be a no-op.
  • The persistent RDP fields (k4/k5/prim_lod_frac) live on Rdp, which is serialized — and ADR 0004 (§"every new piece of hidden state must be reachable by the save-state serialiser") requires exactly that. Same-version save/restore round-trip holds (workspace tests green). Cross-version blob compat is not an ADR-0004 guarantee, no committed snapshots exist, and a stale blob already fails closed (emu.rs:161).

Blocking (breaking public API without version bump) — rejected (see prior reply)

No external crate constructs CombinerInputs (all sites use ..Default::default()); #[non_exhaustive] exists precisely to keep future field additions compatible. Versions are cut at the phase-close ceremony (next: v0.8.0), not per-PR — this is [Unreleased] work.

Nitpick (magic mask (hi & 0xFF) → named constant) — rejected

execute_command's dispatch uses inline field masks throughout ((hi >> 21) & 0x7, hi & 0x3FF, lo & 0x00FF_FFFF, (hi >> 8) & 0x1F, …). Inline masks are the local convention here; a named constant for this one field would be inconsistent, not more so.

Adjudicating the CodeRabbit inline findings and one Antigravity nitpick on #147:

- CHANGELOG + ledger: min_level is no longer extracted (removed in 67f4812) —
  drop the stale "and min_level" claim; state it stays deferred.
- rdp_conformance.rs: the tex_tri_convert_k45_16 doc comment named the wrong
  golden (0x5295, which is the kneg vector's) — the positive k45 golden is
  0x94a5. Corrected.
- ledger R-10: state the n64-systemtest impact explicitly (none — the suite has
  no RDP-combiner coverage; count unchanged at 90), per the ledger convention.
- rdp.md: add the tex_tri_convert_kneg_16 vector to the R-10 scope (all three
  vectors now listed).
- Clarify the combine_cycle_passes_texel0_through rgb_b comment: select 7 is K4
  (0 in this default input), not the zero fall-through arm.

Docs/comments only; no behaviour change.

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

Copy link
Copy Markdown
Owner Author

Adjudication of the fourth Antigravity review

Fixed in ff50399 (the nitpick; the blocking/suggestions are addressed below).

Blocking (removing serde derives + #[non_exhaustive] = breaking API change without version bump) — rejected

Same disposition as the two prior replies, briefly: CombinerInputs has no external consumers (every construction site is intra-crate and uses ..Default::default(); nothing serializes it — grep confirms). Removing derives nothing depends on, and adding #[non_exhaustive] to a struct only external exhaustive constructors could notice (there are none), is not an observable break. Per project policy versions are cut at the phase-close ceremony (next: v0.8.0), not per-PR; this is [Unreleased] work and the CHANGELOG carries it.

Suggestion (k4/k5 as u16 instead of i16) — rejected

i16 faithfully holds the raw 0..=511 (all positive, 511 < i16::MAX) and matches the combiner's signed i16 mux pipeline (rgb_input_b/rgb_input_c return i16, combine_cycle lifts to i32). u16 would force a fallible/again-widening conversion at the CombinerInputs boundary for no gain — the "sign extension is deferred" intent is already stated in the field docs.

Suggestion (OP_SET_CONVERT ignores the hi word) — rejected (intentional, documented)

The dispatch reads the specific fields it models (K4/K5 from lo); the hi word and lo[31:18] carry the YUV coefficients K0..K3, deferred (now noted in the comment). It does not "swallow" state — unmodelled fields are simply not consumed, which is the same pattern as every other partial-command dispatch here.

Nitpick (combine_cycle_passes_texel0_through comment) — adopted

Correct — select 7 now evaluates to inp.k4, not the _ => 0 arm. Reworded to say so explicitly (K4 is 0 in that default input, so the effect is still Zero).

Adjudicating the fifth Antigravity review on #147. CombinerInputs is transient
per-pixel state with no external consumers (nothing outside rustyn64-rdp
references it, CombineCycle, combine, or combine_cycle). Making it `pub(crate)`
(and demoting combine/combine_cycle, its only public users, to `pub(crate)`)
enforces in the type system what the doc comment already claimed — it is not
part of this crate's public API — and permanently settles the recurring
"removing serde derives / adding fields is a breaking public change" finding:
it is no longer public. Dropped the now-redundant `#[non_exhaustive]` too (a new
field is always compatible within the crate).

Also hardens `combine_cycle_passes_texel0_through`: use zero-select 8 for rgb_b
instead of 7 (which now maps to K4), so the pass-through test no longer depends
on K4's default value.

No behaviour change; the combiner output is identical.

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

Copy link
Copy Markdown
Owner Author

Adjudication of the fifth Antigravity review

Fixed in a68610b.

Blocking (removing serde derives from a pub struct = breaking API) — adopted, via pub(crate)

Good call on the concrete resolution. I verified nothing outside rustyn64-rdp references CombinerInputs, CombineCycle, combine, or combine_cycle — they are internal-only despite being pub. So I restricted CombinerInputs to pub(crate) (and demoted its only public users, combine/combine_cycle, to pub(crate)), which makes the type system enforce what the doc comment already claimed (transient, never in a save-state) and permanently settles this finding: the struct is no longer public API, so removing derives / adding fields cannot be a public break. Dropped the now-redundant #[non_exhaustive] (a new field is always compatible within the crate).

Suggestion (combine_cycle_passes_texel0_through relies on K4 == 0) — adopted

Correct. Changed rgb_b from 7 (now K4) to 8 (an unambiguous zero-select), so the pass-through test no longer depends on K4's default.

Suggestion (rgb_input_b standalone callers expect sign-extended i16) — rejected

rgb_input_b is a private free function with exactly one caller (combine_cycle), which immediately lifts it to i32 and applies special_expand. There are no standalone callers, and it cannot be one (private). Sign-extending inside the fetcher would double-apply against combine_channel — the same reason the raw storage is deliberate.

Nitpick (prim_lod_frac u8 vs i16, document range) — already satisfied

CombinerInputs::prim_lod_frac's doc already states the valid range 0..=255; the u8i16 widening is intentional and lossless.

@github-actions

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This PR wires register-sourced exotic RDP combiner inputs (PRIM_LOD_FRAC from Set Prim Color and K4/K5 from Set Convert) through the color combiner mux, backed by three new Angrylion conformance test vectors and documentation updates.

Blocking issues

  • Breaking public API changes without a version bump: crates/rustyn64-rdp/src/lib.rs:L1188, L2562, and L2641 restrict CombinerInputs, Rdp::combine_cycle, and Rdp::combine visibility from pub to pub(crate). Furthermore, new public fields (prim_lod_frac, k4, k5) were added directly to pub struct Rdp (L1368-L1373), which lacks #[non_exhaustive]. These public API breaking changes require a crate version bump in Cargo.toml per project style guidelines.

Suggestions

  • crates/rustyn64-rdp/src/lib.rs:L1368-L1373: Mark pub struct Rdp with #[non_exhaustive] or make prim_lod_frac, k4, and k5 private with accessors to avoid breaking external struct literal instantiations when remaining K0..K3 registers or LOD fields land.
  • crates/rustyn64-rdp/src/lib.rs:L1581-L1588: OP_SET_CONVERT ignores hi and lo[31:18]. If a future command sequence expects K0..K3 or upper bits to reset, leaving them unhandled here could allow stale state if Set Convert is issued repeatedly across mode changes.

Nitpicks

  • crates/rustyn64-rdp/src/lib.rs:L1203: CombinerInputs.prim_lod_frac is stored as i16 while Rdp.prim_lod_frac is u8. Add a brief inline comment clarifying that CombinerInputs uses i16 to align directly with the return types of rgb_input_c and alpha_input_c.

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

@doublegate

Copy link
Copy Markdown
Owner Author

Adjudication of the sixth Antigravity review

No code change this round — the findings are self-contradictory, factually incorrect, already-satisfied, or a standing policy disagreement. Adjudicated below.

Blocking (visibility reduction + version bump) — rejected

  • The pub → pub(crate) reduction of CombinerInputs/combine/combine_cycle is the change the previous review explicitly requested ("restrict its visibility to pub(crate)"), which I adopted after verifying no external consumer exists. Flagging it now as a breaking change reverses that guidance; there is no external consumer to break (confirmed by grep across the workspace).
  • Rdp already has #[non_exhaustive] (line 1285, directly above pub struct Rdp). The claim that it "lacks #[non_exhaustive]" is incorrect, so adding prim_lod_frac/k4/k5 to it is already a compatible change.
  • Version bump: this project cuts versions at the phase-close release ceremony (module 10/70; next: v0.8.0), not per-PR — the workspace shares one version and feature work lands under [Unreleased] (the CHANGELOG carries this entry). Bumping Cargo.toml per-PR would contradict the actual guidelines, not follow them. The crate is workspace-internal (unpublished), so there is no external semver contract in play.

Suggestion (mark Rdp #[non_exhaustive]) — already satisfied

See above — it is marked (line 1285).

Suggestion (Set Convert stale state) — rejected (no stale state)

Set Convert writes self.k4/self.k5 unconditionally on every issue, so repeated commands cannot leave stale modelled state. K0..K3 are not modelled at all (read as zero everywhere), so there is no field for them to go stale. When they land they will be latched the same way.

Nitpick (CombinerInputs.prim_lod_frac i16 vs Rdp.prim_lod_frac u8) — already documented

The field doc states the valid range 0..=255; the i16 width matches the combiner mux return types (rgb_input_c/alpha_input_c), which is why the u8 source is widened losslessly at the CombinerInputs boundary.


All six review rounds and the four CodeRabbit findings are adjudicated. The substantive improvements (the negative-K sign-extension oracle vector, pub(crate) transient-state encapsulation, min_level/serde removal, and the doc corrections) are in. Merging.

@doublegate
doublegate merged commit 002a559 into main Jul 25, 2026
11 checks passed
@doublegate
doublegate deleted the feat/r10-combiner-register-inputs branch July 25, 2026 10:46
doublegate added a commit that referenced this pull request Jul 26, 2026
…Antigravity)

Adopt all 3 Antigravity suggestions (no blocking):
- Add the end-to-end Angrylion conformance vector `tex_tri_chromakey_16` (the main
  suggestion — matching the #147 K4/K5 / PRIM_LOD_FRAC precedent). Set Key GB/R
  program centre [0x20,0x40,0x60] / scale [0x40,0x80,0xC0]; the combine
  (One-centre)*scale>>8 = [56,96,120] -> RGBA5551 0x3b1f, which RustyN64 matches
  byte-for-byte (black if unwired -> non-vacuous). The Set Combine word differs from
  V27 only in rgb_b (7->6 KeyCentre) and rgb_c (15->6 KeyScale); alpha/cyc0 identical.
  This validates the decode + mux end-to-end through the real pipeline, beyond the
  unit tests.
- Document the hi word (width_g[23:12], width_b[11:0]) in OP_SET_KEY_GB, matching
  OP_SET_KEY_R's width_r note.
- Clarify the rgb_input_b fallback comment (no R-10-deferred select remains for B).

Gates: workspace test (incl. 32 rdp_conformance), fmt, clippy, rustdoc, markdownlint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
doublegate added a commit that referenced this pull request Jul 26, 2026
* feat(rdp): chroma-key combiner inputs (Set Key R/GB, R-10)

Wire the last register-sourced R-10 combiner inputs. `Set Key GB` (0x2A) and
`Set Key R` (0x2B) now decode the per-channel chroma-key centre and scale
(bit-layout ported from Angrylion rdp_set_key_gb/_r), and they route through the
combiner as KeyCentre (RGB sub-B select 6) and KeyScale (RGB mul-select 6) instead
of reading zero — matching Angrylion combiner.c cases 6.

- The key *width* is not stored: it drives only the deferred chroma-key alpha
  compare (key_en), not the combiner mux, so it lands with that consumer (the
  min_level precedent — no unread state).
- Two mutation-checked unit tests: set_key_decodes_centre_and_scale_per_channel
  (distinct per-channel values pin the decode field positions) and
  combine_cycle_routes_chroma_key ((One-centre)*scale>>8 → [56,96,120]; unwiring
  either input changes the result).

Same register-sourced-combiner-input pattern as #147 (PRIM_LOD_FRAC / K4 / K5).
n64-systemtest impact: none (no RDP-combiner coverage). Still deferred under R-10:
noise, derivative lod_frac, the chroma-key alpha compare, and YUV convert K0-K3.

Gates: workspace test, fmt, clippy, rustdoc, no_std, markdownlint clean.

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

* test(rdp): add chroma-key .rvec conformance vector + decode docs (#159 Antigravity)

Adopt all 3 Antigravity suggestions (no blocking):
- Add the end-to-end Angrylion conformance vector `tex_tri_chromakey_16` (the main
  suggestion — matching the #147 K4/K5 / PRIM_LOD_FRAC precedent). Set Key GB/R
  program centre [0x20,0x40,0x60] / scale [0x40,0x80,0xC0]; the combine
  (One-centre)*scale>>8 = [56,96,120] -> RGBA5551 0x3b1f, which RustyN64 matches
  byte-for-byte (black if unwired -> non-vacuous). The Set Combine word differs from
  V27 only in rgb_b (7->6 KeyCentre) and rgb_c (15->6 KeyScale); alpha/cyc0 identical.
  This validates the decode + mux end-to-end through the real pipeline, beyond the
  unit tests.
- Document the hi word (width_g[23:12], width_b[11:0]) in OP_SET_KEY_GB, matching
  OP_SET_KEY_R's width_r note.
- Clarify the rgb_input_b fallback comment (no R-10-deferred select remains for B).

Gates: workspace test (incl. 32 rdp_conformance), fmt, clippy, rustdoc, markdownlint.

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

* docs(rdp): refresh stale R-10 comments after wiring chroma-key (#159 CodeRabbit)

CodeRabbit's review caught nearby comments still describing the chroma-key inputs
as deferred/read-as-zero after this PR wired them:
- The CombinerInputs population comment, the `combine_cycle` rustdoc, and the
  CombinerInputs struct doc now list prim-LOD-frac / K4 / K5 / chroma-key
  centre-scale as the wired register-sourced inputs, and noise / derivative
  lod_frac / YUV K0-K3 as the still-deferred (read-zero) ones.

Docs only. Gates: fmt, clippy, rustdoc clean.

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 26, 2026
…ge fixes

CodeRabbit (5 comments — 3 adopted, 2 addressed, 1 rejected w/ citation):
- MAJOR (driver.c): the vector didn't observe the key alpha (the 16-bit alpha bit
  came from coverage, not chroma_key_min). Reworked tex_tri_chromakey_alpha_16 to
  enable alpha-compare (bit 0) with a Set Blend Color threshold of 0x80: the Shade
  triangle is drawn only where chroma_key_min >= 0x80 (combine yields exactly 0x80).
  Now a chroma_key_min off by -1 makes the triangle vanish (mutation-verified), and
  the golden genuinely observes the key alpha. Narrowed the test/ledger/rdp.md claims.
- MAJOR (serde/save-state): REJECTED the serde(default) fix — bincode is positional
  so serde(default) is a no-op (engineering-lessons; the #147 precedent), and adding
  RDP register state matches #159's key_center/key_scale addition this same session;
  the 0.x save-state format evolves with emulated state (ADR 0005 reserves format
  stability for the stable release). No versioning system exists or is in scope.
- Assert distinct per-channel key_width in the decode test (renamed).
- Add a bit-16-set (negative) col17 case to the chroma_key_min unit test.
- Fix the contradictory #159 CHANGELOG "still deferred" line.

Antigravity (no blocking — 1 adopted, 1 fixed, 2 rejected):
- Fix the width_r comment (lo[27:16], 12-bit, not [31:16]).
- Mask col17 to 17 bits inside chroma_key_min (defensive).
- Reject: the 2-cycle "unpopulated combined" concern — the key_en branch runs AFTER
  the two_cycle cyc0 eval, so inp.combined is populated.
- Reject: rgb_input_a is already bound to a local `a` (called once).

Gates: workspace test, fmt, clippy, rustdoc, no_std, markdownlint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
doublegate added a commit that referenced this pull request Jul 26, 2026
* feat(rdp): chroma-key alpha compare (key_en, R-10)

The follow-on to #159's chroma-key combiner inputs: the key_en alpha-compare
keying path (Angrylion combiner_1cycle). Set Other Modes bit 40 (hi>>8&1) decodes
to OtherModes.key_en, and Set Key GB/R now also store the per-channel key_width
(GB hi[23:12]/[11:0], R lo[31:16]) — now that key_en consumes it.

When key_en, Rdp::combine takes the key path: the RGB output is the sub-A
"chromabypass" colour (clamped) and the pixel alpha is chroma_key_min over the
pre->>8 17-bit combined colour (combine_channel_17bit = ((A-B)*C + (D<<8) + 0x80)
& 0x1ffff, matching color_combiner_equation) and the key widths — per channel
SIGN(col,17) folded (-k, or -k+0x10 when the low nibble is 8), +(width<<4),
min-of-3, clamp [0,0xff].

The new behaviour is GATED on key_en, so the common combiner path is byte-
identical — all 31 prior RDP conformance vectors pass unchanged.

Validated byte-for-byte against Angrylion by tex_tri_chromakey_alpha_16 (key_en +
Shade sub-A -> golden 0x4321 = shade RGB + alpha bit; clearing key_en outputs the
combined colour + alpha-combiner result, mutation-verified non-vacuous) plus a
hand-computed chroma_key_min unit test.

Still deferred under R-10: noise (un-oracled), the derivative lod_frac, YUV K0-K3.

Gates: workspace test (incl. 33 rdp_conformance), fmt, clippy, rustdoc, no_std,
markdownlint clean.

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

* test(rdp): adjudicate #160 bot review — observable key alpha + coverage fixes

CodeRabbit (5 comments — 3 adopted, 2 addressed, 1 rejected w/ citation):
- MAJOR (driver.c): the vector didn't observe the key alpha (the 16-bit alpha bit
  came from coverage, not chroma_key_min). Reworked tex_tri_chromakey_alpha_16 to
  enable alpha-compare (bit 0) with a Set Blend Color threshold of 0x80: the Shade
  triangle is drawn only where chroma_key_min >= 0x80 (combine yields exactly 0x80).
  Now a chroma_key_min off by -1 makes the triangle vanish (mutation-verified), and
  the golden genuinely observes the key alpha. Narrowed the test/ledger/rdp.md claims.
- MAJOR (serde/save-state): REJECTED the serde(default) fix — bincode is positional
  so serde(default) is a no-op (engineering-lessons; the #147 precedent), and adding
  RDP register state matches #159's key_center/key_scale addition this same session;
  the 0.x save-state format evolves with emulated state (ADR 0005 reserves format
  stability for the stable release). No versioning system exists or is in scope.
- Assert distinct per-channel key_width in the decode test (renamed).
- Add a bit-16-set (negative) col17 case to the chroma_key_min unit test.
- Fix the contradictory #159 CHANGELOG "still deferred" line.

Antigravity (no blocking — 1 adopted, 1 fixed, 2 rejected):
- Fix the width_r comment (lo[27:16], 12-bit, not [31:16]).
- Mask col17 to 17 bits inside chroma_key_min (defensive).
- Reject: the 2-cycle "unpopulated combined" concern — the key_en branch runs AFTER
  the two_cycle cyc0 eval, so inp.combined is populated.
- Reject: rgb_input_a is already bound to a local `a` (called once).

Gates: workspace test, fmt, clippy, rustdoc, no_std, markdownlint clean.

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

---------

Co-authored-by: Claude Opus 4.8 <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