Skip to content

feat(rdp-gpu): grade the GPU backend against the Angrylion corpus (42/43) - #242

Merged
doublegate merged 5 commits into
mainfrom
feat/gpu-rdp-parity-gate
Aug 1, 2026
Merged

feat(rdp-gpu): grade the GPU backend against the Angrylion corpus (42/43)#242
doublegate merged 5 commits into
mainfrom
feat/gpu-rdp-parity-gate

Conversation

@doublegate

Copy link
Copy Markdown
Owner

Motivation

"Does the GPU RDP have all the features and accuracy of the software one?" is a
question that needs a gate, not an opinion. This builds it — and, per this
project's own rule, builds it before the thing it grades (the Bus wiring, which
is the next PR).

The corpus already existed: 43 registered .rvec vectors whose goldens are
Angrylion's output, license-clean, and already the oracle the software
rasterizer is graded against.

The result

42 of 43 vectors match Angrylion on both paths.

Reproduced byte-for-byte through parallel-rdp: 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

tex_tri_chromakey_alpha_16. parallel-rdp does not implement the key_en
chroma-key alpha compare
— the feature RustyN64 added in #160 and which matches
Angrylion. This is checkable in its source, not inferred from pixels:

  • op_set_other_modes (rdp_device.cpp) decodes bits 9–19 of words[0] and
    never bit 8, which is key_en. There is no 1 << 8 anywhere in the
    function.
  • op_set_key_r / op_set_key_gb do store the key — but Renderer::set_color_key
    routes key_center/key_scale to the combiner's mux inputs (the
    KEY_CENTER / KEY_SCALE sources, a different feature). key_width, which the
    alpha compare needs, is written and never read.
  • No shader under parallel-rdp/shaders/ mentions a key at all.

The diagnostic that made this obvious: the same 27 pixels are non-zero on both
paths — so rasterization and coverage agree exactly — and only the colour differs
(0x4321 vs 0x0001, i.e. the shade colour vs black with alpha). A coverage
match with a colour miss points at the combiner, not the rasterizer.

So the premise that a GPU backend needs catching up to the software one is not
right in either direction: parallel-rdp is more complete almost everywhere and
less complete here.

Design

Graded by the oracle, not by each other. Software-only ⇒ a GPU defect;
GPU-only ⇒ a software gap; the census names which vectors fall where, so a
disagreement is diagnosable rather than merely alarming.

Asserts the exact census, not a threshold. A vector leaving the known-gap
set fails as loudly as one joining it — an unexplained improvement is as much a
signal as a regression.

Cannot pass vacuously. No Vulkan device ⇒ it says, by name, that it verified
nothing. The CI step fails if the test neither grades nor reports a skip.

One GpuRdp per vector. A shared context would carry TMEM, tile and combiner
state across vectors, and a vector that passes only because its predecessor left
the right state behind is not evidence of anything. Costs a device init per
vector; buys independence.

Byte order — the silent hazard

parallel-rdp stores RDRAM in native little-endian word order
(vram8.data[index ^ 3] throughout its shaders); RustyN64 stores big-endian
bytes, and the .rvec goldens are in RustyN64's layout. Every crossing reverses
each aligned 4-byte group. Getting it wrong renders right-shaped/wrong-coloured
output rather than failing.

Pinned two ways: the swap is an involution (so one function serves both
directions), and the exact permutation is fixed — because an 8-byte reversal
would also round-trip and pass an involution test alone.

GpuRdp now owns its RDRAM

The buffer must be page-aligned or the direct host-import path is silently
replaced by a staging copy (#241 shipped exactly that bug). A contract a caller
can satisfy by accident is worse than no contract, so the caller no longer gets
the chance to get it wrong — the shim allocates and aligns it.

RDRAM is reached through with_rdram/with_rdram_mut, which use upstream's
begin_read_rdram/end_write_rdram. That is not ceremony: when host import is
unavailable the device renders into its own buffer and the shim's allocation
is stale, so reading it directly would work on a machine where the import
succeeded and silently return pre-render bytes on one where it did not.

What this is NOT

Still no Bus integration, no shared RDRAM with a running machine, no frontend
feature, no dirty-region sync and therefore no ADR 0004 determinism claim. That
is the next PR; docs/rdp.md keeps the list current.

Gates run locally

cargo fmt --all --check; cargo clippy --workspace --all-targets -- -D warnings; clippy with gpu-rdp on both rustyn64-rdp-gpu and
rustyn64-test-harness; cargo test --workspace; both gpu-rdp test suites;
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps;
scripts/check_en_us.sh; pre-commit run markdownlint --all-files; the no_std
thumbv7em build. All green, one conditional, no pipes.

🤖 Generated with Claude Code

42 of 43 .rvec vectors match on both paths, and the one divergence runs the
opposite way from the expected one.

`conformance_gpu::census` replays the whole registered conformance corpus
through parallel-rdp and grades each vector against **Angrylion's** golden
framebuffer -- the same independent oracle the software rasterizer is graded by,
not against the software rasterizer's own output. Two implementations agreeing
proves nothing about either; two independently matching a third does. That also
makes a disagreement diagnosable: software-only means a GPU defect, GPU-only
means a software gap, and the census reports which.

Reproduced byte-for-byte through the GPU: 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.

**parallel-rdp does not implement the `key_en` chroma-key alpha compare**, which
RustyN64's software rasterizer does (#160). Verified in its source rather than
inferred from pixels: `op_set_other_modes` decodes bits 9-19 of `words[0]` and
never bit 8 -- there is no `1 << 8` anywhere in the function; `set_color_key`
routes `key_center`/`key_scale` only to the combiner's mux inputs, a different
feature; `key_width`, which the alpha compare needs, 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 -- an unexplained
improvement is as much a signal as a regression.

Byte order was the hazard to get right, and it fails silently: parallel-rdp
stores RDRAM in native little-endian word order (`vram8.data[index ^ 3]`) while
RustyN64 stores big-endian bytes, so every crossing reverses each aligned 4-byte
group. Pinned by an involution test plus one that fixes the exact permutation,
since an 8-byte reversal would also round-trip.

`GpuRdp` now OWNS its RDRAM rather than borrowing it. The buffer must be
page-aligned or the direct host-import path is silently replaced by a staging
copy; a contract a caller can satisfy by accident is worse than no contract, so
the caller no longer gets the chance. RDRAM is reached through
`with_rdram`/`with_rdram_mut`, which use upstream's
`begin_read_rdram`/`end_write_rdram` -- when host import is unavailable the
device renders into its own buffer and the shim's allocation is stale, so
reading it directly would work on one machine and silently return pre-render
bytes on another.

One `GpuRdp` per vector, deliberately: a shared context would carry TMEM, tile
and combiner state between vectors, and a vector that passes only because its
predecessor left the right state behind is not evidence.

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

coderabbitai Bot commented Jul 31, 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: 2e35b332-001f-4d6c-a0e6-ddbcb4ec2879

📥 Commits

Reviewing files that changed from the base of the PR and between aa4cec4 and 94f883a.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • crates/rustyn64-rdp-gpu/shim/prdp_shim.cpp
  • crates/rustyn64-rdp-gpu/shim/prdp_shim.h
  • crates/rustyn64-rdp-gpu/src/lib.rs
  • crates/rustyn64-test-harness/src/conformance_gpu.rs
  • crates/rustyn64-test-harness/tests/rdp_conformance_gpu.rs
  • docs/rdp.md
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added GPU-backed RDP conformance testing against reference renderings.
    • Added GPU-managed RDRAM with safe read and write access.
    • Added reporting for unavailable Vulkan hardware and unmappable memory.
  • Bug Fixes

    • Improved GPU memory alignment and synchronisation handling.
  • Documentation

    • Documented GPU parity results: 42 of 43 vectors match the reference output, with one known chroma-key difference.
    • Documented RDRAM ownership, access, and byte-order handling.

Walkthrough

The GPU RDP wrapper now owns aligned RDRAM. The test harness replays the .rvec corpus, compares GPU and software output with Angrylion goldens, and enforces the known 42-of-43 parity census in CI.

Changes

GPU RDP parity

Layer / File(s) Summary
Backend-owned RDRAM
crates/rustyn64-rdp-gpu/shim/*, crates/rustyn64-rdp-gpu/src/lib.rs, crates/rustyn64-rdp-gpu/tests/smoke.rs
The shim allocates aligned RDRAM and exposes its size, read mapping, and write publication. GpuRdp uses size-based construction and scoped RDRAM access.
GPU replay and census
crates/rustyn64-test-harness/Cargo.toml, crates/rustyn64-test-harness/src/*
The feature-gated harness converts RDRAM word order, replays vectors, grades GPU and software framebuffers, and classifies corpus results.
Conformance gate and documentation
crates/rustyn64-test-harness/tests/rdp_conformance_gpu.rs, .github/workflows/ci.yml, CHANGELOG.md, docs/rdp.md
The test enforces the known software-only vector and rejects GPU-only or neither results. CI runs the gate and GPU-enabled Clippy. Documentation records the 42-of-43 census and key_en divergence.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CI
  participant GPUConformance
  participant GpuRdp
  participant Angrylion
  CI->>GPUConformance: run corpus census
  GPUConformance->>GpuRdp: replay each vector
  GpuRdp-->>GPUConformance: return framebuffer
  GPUConformance->>Angrylion: compare with golden framebuffer
  GPUConformance-->>CI: report exact census
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 7 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title describes the main change and uses the required Conventional Commit format, but it is 73 characters and exceeds the 72-character limit. Shorten the subject by at least one character while preserving the Conventional Commit format and imperative mood.
Measured, Never Tuned ⚠️ Warning PRDP_RDRAM_ALIGN_MIN 4096u and the Power-on RDRAM is zero behaviour have no manual/wiki citation or accuracy-ledger entry. Cite the Vulkan external-memory-host specification and an N64 RDRAM source, or record measured provenance in docs/accuracy-ledger.md; remove unsupported defaults.
Unsafe Stays Out Of The Chip Crates ⚠️ Warning The PR adds unsafe blocks in crates/rustyn64-rdp-gpu/src/lib.rs for Publish and RDRAM access, outside rustyn64-frontend; this violates the custom check despite ADR 0014. Remove the added unsafe from rustyn64-rdp-gpu, or explicitly change the check to permit the ADR 0014 GPU FFI exception.
✅ Passed checks (7 passed)
Check name Status Explanation
Description check ✅ Passed The description is directly related to the GPU RDP conformance gate, its 42/43 result, known divergence, design, and scope.
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 The PR adds GPU conformance and CI tooling only; docs state the GPU backend has no Bus integration, so emulator behaviour and n64-systemtest are unchanged.
Docs-As-Spec Sync ✅ Passed The PR leaves all six listed chip crates unchanged; it changes rdp-gpu/test-harness, and docs/rdp.md also records the GPU behaviour.
Changelog Entry For User-Visible Changes ✅ Passed CHANGELOG.md has an [Unreleased] Added section documenting the GPU-RDP parity gate and the user-visible GpuRdp RDRAM ownership/API changes.

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: 8

🤖 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 98-107: Update the earlier [Unreleased] changelog entry around the
command processor lifetime description to remove the claim that it borrows an
RDRAM slice or enforces a lifetime parameter. Replace it with the current
GpuRdp-owned-RDRAM design and its with_rdram/with_rdram_mut access model,
avoiding a second corrective entry.

In `@crates/rustyn64-rdp-gpu/shim/prdp_shim.cpp`:
- Around line 88-94: Update the allocation and cleanup paths around
ctx->rdram.ptr and AlignedBuffer::~AlignedBuffer to support MSVC C++17 by using
the platform-appropriate aligned allocation and matching deallocation functions,
including _aligned_malloc and _aligned_free on Windows while preserving the
existing POSIX behavior. If this backend is intentionally POSIX-only instead,
document that constraint in docs/rdp.md.

In `@crates/rustyn64-rdp-gpu/shim/prdp_shim.h`:
- Around line 41-43: Update the return-value documentation for prdp_create in
prdp_shim.h to include rejection when rdram_size is zero or not a multiple of
the required alignment among the conditions that return NULL. Keep the existing
failure conditions and no-throw boundary contract unchanged.

In `@crates/rustyn64-rdp-gpu/src/lib.rs`:
- Around line 224-240: Update with_rdram_mut to inspect the status returned by
prdp_end_write_rdram after the callback completes, returning Some(out) only when
publication succeeds and None when it fails. Preserve the existing null-pointer
handling and callback result behavior.

In `@crates/rustyn64-test-harness/src/conformance_gpu.rs`:
- Around line 10-17: Update the module documentation near the outcome table to
describe four outcomes, adding a row for the `(false, false)`/`neither`
classification recorded by `Census` and `census`. Explain that when neither
software nor GPU matches the golden result, the vector or oracle is suspect,
while preserving the existing interpretations for the other three cases.

In `@crates/rustyn64-test-harness/tests/rdp_conformance_gpu.rs`:
- Around line 69-73: The census test currently enforces only a weak minimum and
does not verify the vectors present in both sets. In
crates/rustyn64-test-harness/tests/rdp_conformance_gpu.rs:69-73, replace the
c.total() threshold with exact assertions for c.total() and c.both.len(), using
the committed values from docs/STATUS.md. In docs/rdp.md:1008-1010, retain the
“exact census, not a threshold” wording after the test enforces those values; no
direct documentation change is otherwise required.

In `@docs/rdp.md`:
- Around line 968-969: Update the “No shared RDRAM” item in docs/rdp.md to
remove the stale claim that the binding borrows a buffer or relies on a lifetime
parameter. Describe the current safety mechanism instead: GpuRdp owns RDRAM
through the backend, and access is coordinated via the with_rdram and
with_rdram_mut handshakes while no machine is running.
- Around line 977-988: Update the stale accuracy-battery count in STATUS.md from
54 to 56 probes, reflecting 43 RDP and 13 VI vectors. Preserve the GPU gate
status as not started and not passed, and run the pinned markdownlint hook
before merging.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 42e23943-f2f7-4eaf-b794-1b17a992e8e7

📥 Commits

Reviewing files that changed from the base of the PR and between bd985d6 and 9f59192.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock
📒 Files selected for processing (11)
  • .github/workflows/ci.yml
  • CHANGELOG.md
  • crates/rustyn64-rdp-gpu/shim/prdp_shim.cpp
  • crates/rustyn64-rdp-gpu/shim/prdp_shim.h
  • crates/rustyn64-rdp-gpu/src/lib.rs
  • crates/rustyn64-rdp-gpu/tests/smoke.rs
  • crates/rustyn64-test-harness/Cargo.toml
  • crates/rustyn64-test-harness/src/conformance_gpu.rs
  • crates/rustyn64-test-harness/src/lib.rs
  • crates/rustyn64-test-harness/tests/rdp_conformance_gpu.rs
  • docs/rdp.md

Comment thread CHANGELOG.md
Comment thread crates/rustyn64-rdp-gpu/shim/prdp_shim.cpp Outdated
Comment thread crates/rustyn64-rdp-gpu/shim/prdp_shim.h Outdated
Comment thread crates/rustyn64-rdp-gpu/src/lib.rs
Comment thread crates/rustyn64-test-harness/src/conformance_gpu.rs
Comment thread crates/rustyn64-test-harness/tests/rdp_conformance_gpu.rs Outdated
Comment thread docs/rdp.md
Comment thread docs/rdp.md
…nwind

Adopts five of six findings from the Antigravity review of #242.

**The hardcoded 4096-byte alignment is now queried from the device.** It was the
only thing standing between this and a silent fallback on hardware whose
`minImportedHostPointerAlignment` exceeds a page: the import would fail,
parallel-rdp would stage every access through a copy, and the frame would come
out byte-identical with nothing in any return value to say so. Granite does not
surface the value, so the shim asks Vulkan directly via
`VkPhysicalDeviceExternalMemoryHostPropertiesEXT`; the device now comes up
BEFORE the allocation, because the allocation's alignment is a property of it.
4096 remains the floor and the fallback for a device that reports nothing.

**`with_rdram_mut` publishes host writes on the unwind path.** `f` can panic —
the conformance harness's word swap asserts on a length mismatch, and did — and
a panic caught higher up would have skipped `end_write_rdram`, leaving the device
rendering from stale memory with no error reported. Now an RAII guard.

**The conformance replay writes straight into mapped RDRAM.** It was staging an
8 MiB copy and swapping that across: two full passes and a 16 MiB allocation per
vector, 43 times over. The equivalent fusion in the frontend measures 3.3x, so
this was not hypothetical.

Doing that surfaced a real bug in my own comment: it claimed every vector region
is 4-byte aligned, and a committed vector carries a **2-byte** preload. The
word-wise swap asserted on it. The regions now go through per-byte `^ 3`
helpers, which hold for any address and any length — the assert is what proved
the claim wrong, which is the argument for having written it as an assert.

Plus the two nitpicks: the backslash continuations inside `echo "::notice::"`
were injecting the next line's indentation into the annotation text, and the
`* 2` doubleword-to-word conversion is now a named constant.

**Rejected: bumping this crate's version for the `GpuRdp::new` signature
change.** All twelve crates in this workspace carry a literal `version =
"0.8.0"` that is bumped uniformly at release; `rustyn64-rdp-gpu` has never been
published, was added in #241 in this same unreleased cycle, and has one in-tree
consumer. Bumping it alone would desync the workspace and imply a release that
has not happened. The change is recorded in CHANGELOG's `[Unreleased]`, which is
this project's stated mechanism for exactly this.

Census unchanged at 42/43, and the smoke test still takes the direct-import path.

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

Copy link
Copy Markdown
Owner Author

Adjudication — 5 of 6 adopted, in aa4cec4

Blocking: breaking API change without a version bump — rejected

The observation is right (the GpuRdp::new signature did change incompatibly);
the remedy does not fit this repo.

All twelve crates here carry a literal version = "0.8.0" — none use
version.workspace = true — and it is bumped uniformly at release, which is why
they are all on the same number. rustyn64-rdp-gpu has never been published to
crates.io, was added in #241 in this same unreleased cycle, and has exactly one
in-tree consumer, which this PR updates in the same commit.

Bumping this one crate alone would desync the workspace and imply a release that
has not happened. The change is recorded under CHANGELOG [Unreleased], which is
this project's stated mechanism for precisely this (master-core module 10:
"Update the CHANGELOG in the same change as any user-visible behavior"). The
version moves at the v0.9.0 cut, for every crate at once.

Unwind safety in the host write flush — adopted

Correct, and less hypothetical than it reads: the conformance harness's word swap
assert!s on a length mismatch, and it did fire during this PR's own
development. A panic caught higher up would have skipped end_write_rdram and
left the device rendering from stale memory with nothing reporting an error.
with_rdram_mut now publishes through an RAII guard, so it runs on the unwind
path.

Hardcoded page alignment — adopted

The best finding here. Granite does not surface
minImportedHostPointerAlignment, so the shim queries Vulkan directly via
VkPhysicalDeviceExternalMemoryHostPropertiesEXT, and the device now comes up
before the allocation — the allocation's alignment is a property of the
device, so the old ordering could not have been fixed in place. 4096 remains the
floor and the fallback when a device reports nothing.

This mattered because the failure was invisible: on hardware wanting more than a
page, the import fails, parallel-rdp stages every access through a copy, and the
frame comes out byte-identical. #241 shipped exactly that bug once already.

Verified: VK_EXT_external_memory_host is still enabled and the fallback
messages are still absent on this device.

Heap allocation per vector — adopted, and it found a bug

Right on the substance — and the equivalent fusion in the frontend measures
3.3× faster by A-B-A, so this was not tidiness.

Writing straight into mapped RDRAM immediately failed, because a comment I had
written claimed every vector region is 4-byte aligned and a committed vector
carries a 2-byte preload. The word-wise swap asserted on it. The regions now
go through per-byte ^ 3 helpers that hold for any address and any length. The
assert being there is why the wrong claim surfaced in seconds rather than as a
wrong picture.

Backslash continuation in echo "::notice::"adopted

Correct: the continuation line's indentation was being injected into the
annotation text. All three are single-line now.

* 2 multiplier — adopted

Now U32_PER_DOUBLEWORD, with the reason named (command_len_words counts
64-bit RDP doublewords; the shim takes 32-bit words).


Census unchanged at 42/43, and the smoke test still takes the direct-import
path.

All eight CodeRabbit findings on #242. Each was right; three were substantive.

**`with_rdram_mut` was discarding `end_write_rdram`'s status** — the one failure
it exists to detect. A failed publish means host writes never reached the device,
and the conformance replay uses exactly this `Some`/`None` to decide a vector was
set up, so it would have graded a vector against an unwritten framebuffer instead
of reporting `RdramUnmappable`. It also contradicted the contract docs/rdp.md
records for this wrapper. The RAII guard now records the outcome in a `Cell` —
`drop` cannot return one — and the function returns `None` when it failed, while
still publishing on the unwind path.

**`std::aligned_alloc` does not exist on MSVC** at any language level: its `free`
cannot release an over-aligned block, so the CRT has a separate
`_aligned_malloc`/`_aligned_free` pair and mixing them with `free` is undefined.
Now behind `#ifdef _MSC_VER`. Untested here, since the `gpu-rdp` CI job is
Linux-only, and the comment says so.

**The census was documented as exact in two places and asserted as `>= 35` in
one.** The threshold was the weaker half: a vector leaving the corpus entirely
would have passed while the prose still claimed 42 of 43. Now `EXPECTED_TOTAL`
and `EXPECTED_BOTH` are asserted exactly, and both move by hand when a vector is
added.

Three documentation defects, all of the kind this repo keeps writing lessons
about — a claim that was true when written and is not now:

- The CHANGELOG's `[Unreleased]` carried the #241 entry saying the wrapper "ties
  the command processor's lifetime to a borrowed RDRAM slice" two paragraphs
  from the entry reversing exactly that. Neither has shipped, so the release
  notes would have carried both. Amended in place rather than corrected twice.
- The parity module's table listed three outcomes while `Census` has four
  buckets. The missing row is the one a reader most needs: neither path
  reproduces, which indicts the vector or the oracle rather than either
  rasterizer.
- docs/rdp.md's "No shared RDRAM" item still described the borrow-based
  mechanism. The substance held; only the stated mechanism was stale.

Plus the header's NULL list, which omitted the size-validation rejection — and
that header is explicitly held correct by review rather than by a generator, so
an incomplete contract there is the whole risk.

Census unchanged at 42/43.

Co-Authored-By: Claude Opus 5 <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-test-harness/src/conformance_gpu.rs`:
- Around line 77-96: Add a dedicated round-trip test for the write_region and
read_region helpers, covering both ordinary data and a 2-byte unaligned/preload
case; write source bytes into a test RDRAM buffer, read them back at the same
logical address, and assert the result matches the source.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 13c3d1b8-bea0-4bd3-a323-f048a8d4774e

📥 Commits

Reviewing files that changed from the base of the PR and between 9f59192 and aa4cec4.

📒 Files selected for processing (4)
  • .github/workflows/ci.yml
  • crates/rustyn64-rdp-gpu/shim/prdp_shim.cpp
  • crates/rustyn64-rdp-gpu/src/lib.rs
  • crates/rustyn64-test-harness/src/conformance_gpu.rs

Comment thread crates/rustyn64-test-harness/src/conformance_gpu.rs
doublegate and others added 2 commits July 31, 2026 19:56
… pair

The `^ 3` region helpers carry the general case — any address, any length,
including the 2-byte preload that broke the word-wise path — and had no direct
test while `swap_words_into` had two. CodeRabbit's point.

Two tests, because a round trip alone is not enough: `write_region` and
`read_region` are mutual inverses, so **two consistently wrong functions round
trip perfectly**. The second test asserts they agree with `swap_words_into`
where both apply, which is what actually pins the permutation.

Mutation-checked, and the pair is what makes it worth stating: changing `^ 3` to
`^ 1` in `write_region` alone fails two tests; changing it in BOTH — still a
perfect involution — fails exactly one, the cross-mechanism one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`(i + 1) as u8` tripped `clippy::cast_possible_truncation`. It could not
actually truncate — `len` maxes at 9 — but the gate does not reason about that
and should not have to.

Landed red because the previous commit's gate ran in a block separated from the
commit by `;` rather than `&&`, so a failing lint did not stop the commit. That
is the same shape as piping a gate into `tail`: the check ran, its answer was
discarded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This PR adds a GPU RDP conformance gate to grade the parallel-rdp backend against the 43-vector Angrylion .rvec corpus, while refactoring GpuRdp to allocate and own page-aligned RDRAM rather than borrowing caller memory.

Blocking issues

  • crates/rustyn64-rdp-gpu/shim/prdp_shim.cpp:72: query_import_alignment calls vkGetPhysicalDeviceProperties2 without verifying that the function pointer was resolved by volk or that Vulkan 1.1 / VK_KHR_get_physical_device_properties2 is supported on the device. On Vulkan 1.0 drivers or environments where the extension function pointer is NULL, calling it will result in a null-pointer dereference crash when GpuRdp::new is invoked.
  • crates/rustyn64-rdp-gpu/shim/prdp_shim.cpp:138: PRDP_ALIGNED_ALLOC(align, rdram_size) passes align directly from query_import_alignment to std::aligned_alloc. C11/C++17 standard requires align to be a valid power-of-two supported by the implementation. If a driver returns a non-power-of-two value or invalid alignment, std::aligned_alloc triggers undefined behavior. Validate that (align & (align - 1)) == 0 and is a multiple of sizeof(void*).

Suggestions

  • crates/rustyn64-test-harness/src/conformance_gpu.rs:213: In split_commands, encountering an unknown opcode or zero command length (len == 0) causes the loop to break early. Trailing valid commands in v.cmds will be silently dropped without error or diagnostic output.
  • crates/rustyn64-rdp-gpu/src/lib.rs:252: In with_rdram_mut, if prdp_end_write_rdram fails, published.get().then_some(out) returns None and discards out, but host RDRAM memory was already modified in-place by f(slice). If callers attempt to retry or reuse memory state, host RDRAM and device state will be out of sync.

Nitpicks

  • crates/rustyn64-rdp-gpu/shim/prdp_shim.cpp:30: Prefer constexpr size_t over macro #define PRDP_RDRAM_ALIGN_MIN.

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

@doublegate
doublegate merged commit ba26dc1 into main Aug 1, 2026
13 checks passed
@doublegate
doublegate deleted the feat/gpu-rdp-parity-gate branch August 1, 2026 00:43
doublegate added a commit that referenced this pull request Aug 1, 2026
…olds

Three more findings on #244, all correct.

**The gate could pass on an incomplete run.** The corpus check accepted `>= 35`
vectors while claiming 43, and the sequence comparison used `zip`, which stops
at the shorter side — so a second run producing fewer frames would have compared
only the prefix and passed. The corpus count is now asserted against
`RDP_VECTORS.len()` rather than a literal, so it cannot drift from the registry,
and the second sequence's length is asserted before the `zip`.

That is the second time in two PRs that a `>=` threshold sat beside prose
claiming an exact number. The threshold is always the weaker half of the pair.

**The CHANGELOG contradicted itself inside `[Unreleased]`**: the #243 entry
still said the GPU path "makes no determinism claim of its own" a few paragraphs
below the entry making one. Neither has shipped, so the release notes would have
carried both. The older entry now points at ADR 0015 instead of asserting the
negative — the same amendment-rather-than-append treatment the borrowed-RDRAM
entry got in #242.

Plus a comma before an essential `because` clause in the ADR.

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