Skip to content

feat(rdp): chroma-key alpha compare (key_en, R-10) - #160

Merged
doublegate merged 2 commits into
mainfrom
feat/rdp-chroma-key-alpha
Jul 26, 2026
Merged

feat(rdp): chroma-key alpha compare (key_en, R-10)#160
doublegate merged 2 commits into
mainfrom
feat/rdp-chroma-key-alpha

Conversation

@doublegate

Copy link
Copy Markdown
Owner

Motivation

The follow-on to #159 (chroma-key combiner inputs): this wires the key_en alpha-compare keying path — the other half of N64 chroma-keying. It also stores the per-channel key_width that #159 deliberately left out ("lands with its consumer") — the consumer is here.

Changes

  • Decode: Set Other Modes bit 40 (hi >> 8 & 1) → OtherModes.key_en; Set Key GB/R now also decode key_width (GB hi[23:12]/[11:0], R lo[31:16]).
  • Key path (Angrylion combiner_1cycle): when key_en, Rdp::combine outputs the sub-A chromabypass colour (clamped) as RGB, and the pixel alpha is chroma_key_min over the pre->>8 17-bit combined colour (combine_channel_17bit, 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].
  • Gated on key_en so the common combiner path is byte-identical — all 31 prior RDP conformance vectors pass unchanged.

Verification

  • tex_tri_chromakey_alpha_16 .rveckey_en + Shade sub-A; Angrylion's golden is 0x4321 (shade RGB + the keyalpha's alpha bit), which RustyN64 matches byte-for-byte. Mutation-verified non-vacuous: forcing the common path (clearing key_en) outputs the combined colour + alpha-combiner result and fails.
  • chroma_key_min_folds_and_takes_the_minimum — hand-computed unit test covering the fold, the nibble==8 special case, and the clamp.
  • All 31 prior RDP vectors unchanged (proves the common path is untouched).
  • Gates: cargo test --workspace, fmt, clippy -D warnings, rustdoc -D warnings, no_std, markdownlint.

Still deferred under R-10: noise (un-oracled — Angrylion fakes it with a validation PRNG), the derivative lod_frac, and the YUV K0K3 convert. n64-systemtest impact: none (no RDP-combiner coverage).

🤖 Generated with Claude Code

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>
@coderabbitai

coderabbitai Bot commented Jul 26, 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: 10 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 391035ff-7dbc-4c4e-9448-64d3aa9b0aea

📥 Commits

Reviewing files that changed from the base of the PR and between a88982a and 226cb6e.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • crates/rustyn64-rdp/src/lib.rs
  • crates/rustyn64-test-harness/tests/rdp_conformance.rs
  • crates/rustyn64-test-harness/tests/vectors/tex_tri_chromakey_alpha_16.rvec
  • crates/rustyn64-test-harness/vectors-gen/driver.c
  • docs/accuracy-ledger.md
📝 Walkthrough

Walkthrough

The RDP now decodes chroma-key enablement and per-channel widths, computes key-derived alpha in Rdp::combine, and validates the path with a generated Angrylion conformance vector and unit tests. Documentation records chroma-key alpha comparison as resolved.

Changes

RDP chroma-key alpha compare

Layer / File(s) Summary
Decode chroma-key state
crates/rustyn64-rdp/src/lib.rs
OtherModes decodes key_en, while Set Key GB and Set Key R store per-channel key_width values in Rdp.
Compute key-derived alpha
crates/rustyn64-rdp/src/lib.rs
The key-enabled combiner computes pre-shift channel values, derives clamped alpha with chroma_key_min, and returns sub-A RGB; the disabled path retains cycle-1 combination.
Validate and document the vector path
crates/rustyn64-test-harness/vectors-gen/driver.c, crates/rustyn64-test-harness/tests/rdp_conformance.rs, crates/rustyn64-test-harness/tests/vectors/*, CHANGELOG.md, docs/accuracy-ledger.md, docs/rdp.md
A key-enabled textured triangle vector and golden framebuffer are added, with conformance and unit-test coverage plus updated accuracy documentation.

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

Sequence Diagram(s)

sequenceDiagram
  participant VectorGenerator
  participant Rdp
  participant ChromaKeyMin
  participant ConformanceTest
  VectorGenerator->>Rdp: emit key-enabled triangle commands
  Rdp->>Rdp: decode key_en and key_width
  Rdp->>ChromaKeyMin: combine 17-bit channel values with key widths
  ChromaKeyMin-->>Rdp: return clamped alpha
  Rdp-->>ConformanceTest: render framebuffer
  ConformanceTest->>ConformanceTest: compare with Angrylion golden vector
Loading

Possibly related PRs

  • doublegate/RustyN64#159: Updates the same RDP Set Key GB/Set Key R handling and combiner wiring for key centre and scale.
🚥 Pre-merge checks | ✅ 9 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Oracle Number Is Stated ⚠️ Warning Violates the oracle-number rule: the PR only says “n64-systemtest impact: none” and never states a failing-assertion delta or “not measured”. Add the measured n64-systemtest failing-assertion count change, or explicitly say it was not measured, in the changelog/ledger/docs entry.
✅ Passed checks (9 passed)
Check name Status Explanation
Title check ✅ Passed Matches the main change and fits the Conventional Commits format, scope and length rules.
Description check ✅ Passed It is directly about the key_en chroma-key alpha path and key_width handling, so it is on-topic.
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.
Docs-As-Spec Sync ✅ Passed PASS: the rustyn64-rdp key_en/key_width behaviour change is documented in docs/rdp.md, with the matching R-10 ledger entry updated in the same PR.
Changelog Entry For User-Visible Changes ✅ Passed PASS: CHANGELOG.md [Unreleased] includes an Added entry for the R-10 user-visible chroma-key alpha compare change, satisfying the changelog rule.
Measured, Never Tuned ✅ Passed R-10 in docs/accuracy-ledger.md records the key_en/key_width path as Angrylion-validated, and the code comments bind the new literals to that entry; no unproven constants/timing found.
Unsafe Stays Out Of The Chip Crates ✅ Passed PASS: no unsafe was added in the diff, and #![forbid(unsafe_code)] remains in core/RDP/RSP/audio/cart/cpu/snapshot.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 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 `@CHANGELOG.md`:
- Around line 11-23: Update the older [Unreleased] entry below the RDP
chroma-key entry to remove claims that key_width is not stored or chroma-key
alpha comparison is deferred. Revise or merge it so [Unreleased] consistently
documents the implemented final key_en behavior described in the new entry.

In `@crates/rustyn64-rdp/src/lib.rs`:
- Around line 5213-5227: Add a test case to
chroma_key_min_folds_and_takes_the_minimum using at least one col17 value at or
above 0x10000, exercising the negative result from SIGN(col17, 17). Assert the
expected folded minimum so incorrect signed 17-bit sign extension is detected
while preserving the existing cases.
- Around line 1644-1657: The decode test for the key-setting dispatcher must
independently verify all three key_width channels. Update the test covering
OP_SET_KEY_GB and OP_SET_KEY_R to use distinct R/G/B width values, assert
key_width[0], key_width[1], and key_width[2] after dispatch, and remove any
stale wording claiming width is unused; preserve the existing centre/scale
assertions.
- Around line 900-902: Add backwards-compatible deserialization for the newly
added RDP fields key_en and key_width in the Rdp serialization path, using serde
defaults or an equivalent versioned migration. Ensure EmuCore::restore can
decode existing save-state blobs missing these fields while preserving the
current values for newly serialized states.

In `@crates/rustyn64-test-harness/vectors-gen/driver.c`:
- Around line 956-975: The conformance fixture does not currently observe
key-derived alpha because alpha comparison is disabled and rasterization
replaces combiner alpha with coverage. In
crates/rustyn64-test-harness/vectors-gen/driver.c:956-975, enable a non-zero
alpha-compare threshold or add a direct pre-coverage assertion using values that
distinguish chroma_key_min; regenerate
crates/rustyn64-test-harness/tests/vectors/tex_tri_chromakey_alpha_16.rvec:1. In
crates/rustyn64-test-harness/tests/rdp_conformance.rs:462-476, narrow the
end-to-end claim or assert failure when key alpha is bypassed. Update
docs/accuracy-ledger.md:391 and docs/rdp.md:407-412 to distinguish helper/unit
evidence from end-to-end evidence, ensuring the accuracy oracle does not claim
coverage for unobserved behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 78061b69-33e7-4174-8795-649447bac6e0

📥 Commits

Reviewing files that changed from the base of the PR and between fc076a5 and a88982a.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • crates/rustyn64-rdp/src/lib.rs
  • crates/rustyn64-test-harness/tests/rdp_conformance.rs
  • crates/rustyn64-test-harness/tests/vectors/tex_tri_chromakey_alpha_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-rdp/src/lib.rs
Comment thread crates/rustyn64-rdp/src/lib.rs
Comment thread crates/rustyn64-rdp/src/lib.rs
Comment thread crates/rustyn64-test-harness/vectors-gen/driver.c
…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

Copy link
Copy Markdown
Owner Author

Antigravity adjudication (commit 226cb6e)

No blocking issues; 1 adopted, 1 fixed, 2 rejected.

  • Adopt: corrected the OP_SET_KEY_R comment — width_r is lo[27:16] (12-bit, & 0xFFF), not [31:16].
  • Adopt: mask col17 to 17 bits inside chroma_key_min (col17[ch] & 0x1_FFFF) so an unmasked caller is safe.
  • Reject: the 2-cycle "inp.combined unpopulated" concern — the key_en branch runs after the if two_cycle { inp.combined = combine_cycle(cyc0, ..) } block, so combined is populated before the key path reads cyc1.
  • Reject: rgb_input_a is not called twice — its result is bound to the local a once and reused by both combine_channel_17bit(a, ..) and clamp_9bit(a).

@github-actions

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This PR implements RDP chroma-key alpha comparison (key_en, Set Other Modes bit 40) by decoding and storing per-channel 12-bit key widths, evaluating the 17-bit pre-shift combiner equation, and deriving pixel alpha via chroma_key_min.

Blocking issues

None found.

Suggestions

  • crates/rustyn64-rdp/src/lib.rs:L604-L609: combine_channel_17bit duplicates the equation arithmetic of combine_channel up to the final shift/clamp. Consider extracting the un-shifted 17-bit combiner evaluation into a shared helper function so that combine_channel simply performs (combine_channel_17bit(a, b, c, d) >> 8).clamp(0, 0xFF) as u8 (or similar), avoiding drift between the two combiner paths.

Nitpicks

  • crates/rustyn64-rdp/src/lib.rs:L2733: In combine, rgb_input_a, rgb_input_b, rgb_input_c, and rgb_input_d are called per-channel inside the loop for key_en. While lightweight, caching or matching the pattern used in combine_cycle maintains consistent code layout across combiner execution paths.

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

@doublegate
doublegate merged commit b54f9a7 into main Jul 26, 2026
11 checks passed
@doublegate
doublegate deleted the feat/rdp-chroma-key-alpha branch July 26, 2026 04:47
doublegate added a commit that referenced this pull request Aug 1, 2026
…/43) (#242)

`conformance_gpu::census` replays all 43 registered `.rvec` vectors through
parallel-rdp and grades each against Angrylion's golden framebuffer — the same
independent oracle the software rasterizer is graded by, not one implementation
against the other.

42 of 43 match on both paths: triangles (flat, shaded, textured, Z-buffered,
fractional, negative-slope, right-major), texture loads (Load Tile, Load Block,
TLUT, CI4/CI8, 4-bit), the combiner, the blender, dither, tile
clamp/mask/mirror/shift, and the 3-point filter.

The one divergence runs the opposite way from the expected one: parallel-rdp
does not implement the `key_en` chroma-key alpha compare, which RustyN64's
software rasterizer does (#160). Verified in its source — `op_set_other_modes`
never decodes bit 8 of `words[0]`, `set_color_key` routes `key_center`/
`key_scale` only to combiner mux inputs, `key_width` is written and never read,
and no shader mentions a key. The gate asserts the exact census, so a vector
leaving the known-gap set fails as loudly as one joining it.

`GpuRdp` now owns its RDRAM instead of borrowing it: the buffer must be aligned
to the device's `minImportedHostPointerAlignment` or the direct host-import path
is silently replaced by a staging copy, and a contract a caller can satisfy by
accident is worse than no contract. The alignment is queried from the device
rather than hardcoded. RDRAM is reached through `with_rdram`/`with_rdram_mut`,
which use upstream's coherency handshake and report a failed publish rather than
dropping it.

Byte order was the silent hazard: parallel-rdp stores RDRAM in native
little-endian word order while RustyN64 stores big-endian bytes. Pinned by an
involution test, an exact-permutation test, and a cross-mechanism test that
catches two consistently-wrong-but-mutually-inverse helpers.

Not integrated: no Bus wiring, no shared RDRAM with a running machine, no
frontend feature, no dirty-region sync and therefore no ADR 0004 determinism
claim.

Adjudicated 15 bot findings across two reviewers: 14 adopted, 1 rejected with
reasons on the PR.
doublegate added a commit that referenced this pull request Aug 2, 2026
Three ADRs for the three items in the declined backlog that measure positive.
Each is written on the re-derived numbers rather than the ones they were
declined on, since those were shares of a frame that has since halved.

0018 (async GPU RDP) accepts shape (b) — present one frame late — as an opt-in
that is off by default, because its cost is a frame of presentation latency and
~3.5% is not enough to spend a user's latency budget for them. Records that
shape (a) is unavailable: `present` stages RDRAM before enqueueing, so
submitting mid-frame would change which memory each command reads. Also names a
GPU-to-GPU ordering hazard the plan missed, which is NOT the CPU-side tracker
ADR 0014 §6 describes.

0019 (GPU as the machine's rasterizer) accepts A4 as an opt-in but BLOCKS
building it until the GPU/Angrylion census reaches 43/43. This is the finding
that changed the shape of the decision: the GPU is currently LESS complete than
the software rasterizer it would replace (`key_en`, #160), so shipping it for
~2.5% would trade correctness for frame time. The ADR inverts the
justification — A4 is worth building as an ACCURACY change, with the frame time
as a side effect, and that reframing is what makes the gate load-bearing.

0020 amends 0016 to accept the SIMD exception for `multiply_lane` only, and
says plainly that the technique got worse while the context got better: 0016
declined 1.056x, this accepts 1.045x. The four gates carry forward unrelaxed,
and the ADR states in advance what would make it a mistake — that a
hard-to-write equivalence test is a reason to stop, not to sample.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant