Skip to content

test(rdp): refute the last two RDP mirrored-text candidates (R-18) - #203

Merged
doublegate merged 3 commits into
mainfrom
test/mirrored-text-dsdx-winding-vectors
Jul 30, 2026
Merged

test(rdp): refute the last two RDP mirrored-text candidates (R-18)#203
doublegate merged 3 commits into
mainfrom
test/mirrored-text-dsdx-winding-vectors

Conversation

@doublegate

Copy link
Copy Markdown
Owner

Motivation

The mirrored-text defect visible in the committed commercial screenshots — GoldenEye 007's "Nintendo" logo and WCW vs. nWo's banner both render left-right flipped, while WCW/nWo Revenge's THQ logo is correct — had two live candidates left after tex_tri_mirror_s_16 cleared the tile mirror path. R-18 named them explicitly: a negative DsDx and the triangle edge/winding path.

Per the project's own rule, author the oracle vector rather than patch on a guess.

Result: both refuted

Both new vectors match RustyN64 byte-for-byte against Angrylion, so the RDP side of this hunt is now exhausted rather than merely narrowed.

tex_tri_neg_dsdx_16 walks S backwards at one texel per pixel from a base of 7 texels, across a full 8×8 rectangle. mask_s = 0 + clamp_s is load-bearing and not tidiness:

column readout
signed (correct) gray yellow cyan magenta white blue green red
unsigned (bug), clamped gray gray gray gray gray gray gray gray

Under an 8-texel wrap these two are indistinguishable — a missing sign extension gives the stride 0xFFE0 = 2047 texels, and 2047 mod 8 == -1 mod 8, so the bug would wrap to exactly the reversed ramp correct code produces. Clamping separates them. Column 0 is gray under both, which is what the eight-column readout defeats.

tex_tri_right_major_16 is the first textured lft = 0 triangle in the battery. Of the 34 triangles in driver.c exactly one set lft = 0 (fill_tri_neg_16) and it is FILL-mode with no texture coefficients, so this path was entirely unpinned. It matters because the RDP evaluates the span's texture coordinate at the major edge and steps by DsDx from there — swapping which edge is major swaps which end holds the base coordinate, and getting that backwards mirrors the span while leaving every left-major vector in the battery green. Angrylion emits the forward ramp; we match.

Battery: 54 → 56 probes (43 RDP + 13 VI), still 100%. Both mutation-checked by corrupting each golden's last byte, which names them individually:

vectors diverged from the Angrylion oracle: ["rdp-conformance/tex_tri_neg_dsdx_16", "rdp-conformance/tex_tri_right_major_16"]

Consequence: the defect is reclassified

Five RDP hypotheses have now been tested and cleared (the Set Tile mirror_s decode, mask_coupled's fold, the mirror_s render path, negative DsDx, right-major winding), and Texture Rectangle Flip is excluded on the separate ground that it bails rather than rendering flipped.

What is left is upstream of the DPC seam — the geometry producing the vertex X coordinates, i.e. the game's graphics microcode on the LLE RSP. A vertex transform, viewport scale, or vector-unit sign error there would flip a quad while every RDP command it then emits is rasterized faithfully. That moves the defect off R-13/RDP and onto the RSP/geometry side, and it is why no further RDP vector is the right next probe.

Stated caveat, not buried: these are unit refutations against synthetic vectors. They prove the paths are bit-exact when driven directly; they do not prove the same paths are configured correctly under a live retail command stream.

Also: two vector comments were describing a test that does not exist

tex_tri_16's comment claimed "a per-x S gradient (dx.S = 1.0/pixel) so each column reads a different texel". Its golden is solid red — every pixel is texel 0:

tex_tri_16:        f801 f801 f801 f801 ...     (dx.S = 1)
tex_tri_mirror_s:  f801 07c1 003f ffff ...     (dx.S = 0x20)

dx.S = 1 is one unit of the s10.5 texel domain — 1/32 of a texel per pixel — so S never leaves texel 0 across eight columns. One texel per pixel is 0x20. tex_tri_base_tile_16 inherited the same coefficient and the same wrong description ("the colorful ramp").

Both vectors pass and keep the coverage they genuinely have (a Load Tile round trip; red-vs-black tile-3 discrimination, which is unaffected), so neither is retired — discarding a passing golden to fix a comment would lose real coverage. But neither ever pinned a gradient, and nothing failed when the claim went stale, which is the hazard docs/engineering-lessons.md §3.3c describes. Comments corrected in place with the reason recorded.

Also fixes a pre-existing MD040 fence in docs/residuals/R-18.md (landed in #201). markdownlint is pre-commit only and not in any CI job, so this had no gate.

Gates run locally

cargo fmt --all --check · cargo clippy --workspace --all-targets -- -D warnings · cargo test --workspace · RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps · pre-commit run markdownlint --all-files — all green.

Regenerating the vectors reproduced all 54 pre-existing goldens byte-for-byte (only the two new files appear in git status), which is an incidental reproducibility check on the driver.

🤖 Generated with Claude Code

The mirrored-text defect (GoldenEye's "Nintendo" logo, WCW vs. nWo's banner)
had two live candidates left after `tex_tri_mirror_s_16` cleared the tile
mirror path. Author both as Angrylion vectors rather than patch on a guess.

Both come back green byte-for-byte, so the RDP side of the hunt is exhausted:

- `tex_tri_neg_dsdx_16` walks S backwards at one texel per pixel from a base
  of 7 texels. `mask_s = 0` + `clamp_s` is load-bearing: under an 8-texel wrap
  the outcomes are indistinguishable, because a missing sign extension gives a
  stride of 2047 texels and 2047 mod 8 == -1 mod 8, i.e. the bug would wrap to
  exactly the reversed ramp correct code produces. Clamped, they separate into
  a reversed ramp versus a solid bar. Column 0 is gray under both, which is why
  the readout is eight columns wide.

- `tex_tri_right_major_16` is the first *textured* `lft = 0` triangle in the
  battery; the only previous `lft = 0` triangle was FILL-mode with no texture
  coefficients. It matters because the RDP evaluates the span's texture
  coordinate at the major edge, so swapping which edge is major swaps which end
  holds the base coordinate -- and getting that backwards mirrors the span while
  leaving every left-major vector green.

Battery: 54 -> 56 probes (43 RDP + 13 VI), still 100%. Both mutation-checked by
corrupting each golden's last byte, which names them individually.

Consequence: five RDP hypotheses are now cleared and `Texture Rectangle Flip`
is excluded on separate grounds, so the defect is reclassified to the
RSP/geometry side -- upstream of the DPC seam, where a vertex transform or
viewport sign error would flip a quad whose RDP commands are then rasterised
faithfully. Recorded in R-18 with the caveat that these are unit refutations
against synthetic vectors, not proof the same paths are configured correctly
under a live retail command stream.

Also corrects two vector comments that claimed `tex_tri_16` and
`tex_tri_base_tile_16` render an eight-texel colour ramp. Their goldens are
solid texel 0: `dx.S = 1` is 1/32 of a texel per pixel in the s10.5 domain, so
S never leaves the first texel. Both vectors pass and keep the coverage they
have, but neither ever pinned a gradient -- and nothing failed when the claim
went stale, which is the hazard exactly. Fixes a pre-existing MD040 fence in
R-18.md that CI cannot catch (markdownlint is pre-commit only).

Gates: fmt, clippy -D warnings, cargo test --workspace, rustdoc -D warnings,
markdownlint -- all green.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@doublegate, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 3 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: 19925c88-0e45-4552-8f26-f9ff1724ef99

📥 Commits

Reviewing files that changed from the base of the PR and between a900da7 and 9c1072d.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • crates/rustyn64-test-harness/src/conformance.rs
  • crates/rustyn64-test-harness/tests/vectors/tex_tri_neg_dsdx_16.rvec
  • crates/rustyn64-test-harness/tests/vectors/tex_tri_right_major_16.rvec
  • crates/rustyn64-test-harness/vectors-gen/driver.c
  • docs/STATUS.md
  • docs/residuals/R-18.md
  • screenshots/README.md

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

Antigravity review on #203, all three items:

- TEX_BLOCK with a negative argument: VERIFIED, not a defect. HALVES masks
  to 16 bits BEFORE the shift, so -0x20 packs as 0xFFE00000 with no bleed
  into the adjacent T field. Confirmed empirically too -- the emitted golden
  is the reversed ramp, which only happens if the coefficient reached
  Angrylion as -32.

- Adjacent "colourful"/"colorful" in driver.c: adopted. The inconsistency
  was mine, introduced into a file with 30 en-GB spellings. The repo is
  moving to en-US, so driver.c is swept whole (75 replacements) rather than
  patched at one line. The driver rebuilds and reproduces all 56 goldens
  byte-for-byte, so no vector changed. The repo-wide sweep follows separately.

- R-18.md readability: adopted additively. Its Status section is a single
  ~48,000-character narrative, which is what made the live scope hard to
  extract. Added a "Current position" INDEX at the top that supersedes
  nothing, states that the record wins on any disagreement, and tabulates
  the nine open items. The history is untouched -- restructuring it would
  breach the append-only rule the document opens by asserting.

Gates: fmt, clippy -D warnings, cargo test --workspace, rustdoc -D warnings,
markdownlint -- all green.
@doublegate

Copy link
Copy Markdown
Owner Author

Adjudication of the Antigravity review

1. driver.c:1374 — verify TEX_BLOCK handles the negative -0x20 safely. VERIFIED, not a defect — no change needed.

HALVES masks to 16 bits before the shift, so there is no sign extension across the packed fields:

#define HALVES(hi16, lo16) ((((uint32_t)(hi16) & 0xFFFFu) << 16) | ((uint32_t)(lo16) & 0xFFFFu))

Compiled and checked: HALVES(-0x20, 0) = 0xFFE00000, HALVES(0xE0, 0) = 0x00E00000. The & 0xFFFFu truncates 0xFFFFFFE0 to 0xFFE0 first, so the adjacent T field stays zero. The frac word is a separate 0u, so DsDx assembles as the 32-bit s15.16 value 0xFFE00000 = −2,097,152 = −32.0 in the s10.5 texel domain = −1 texel per pixel.

It is also confirmed empirically, which is the stronger evidence: the emitted golden is the ramp reversed, and that only happens if the coefficient reached Angrylion as −32. A field-bleed would have produced garbage in T, not a clean reversal.

Good thing to ask about, though — this is the first negative coefficient in the file, so nothing had exercised that macro path before.

2. driver.c:1023 — adjacent "colourful"/"colorful". ADOPTED, and widened.

The inconsistency was mine: I introduced one colorful into a file carrying 30 colours. The repo is moving to en-US, so rather than patch the single line I swept driver.c whole — 75 replacements. The driver rebuilds clean and regenerating reproduces all 56 goldens byte-for-byte, so no vector content changed. A repo-wide sweep follows in its own PR; this file is done because it was already under change here.

3. R-18.md:24 — separate active status from the historical trial-and-error log. ADOPTED additively.

Fair, and understated: that ## Status section is a single ~48,000-character paragraph, which is exactly why the live scope had become hard to extract.

I have not restructured it, because the document opens by asserting the append-only rule — superseded claims stay marked rather than being edited away, and re-sorting the narrative would breach that and destroy the chronology that makes the retractions legible.

Instead I added a ## Current position (index) section at the top that:

  • states explicitly that it supersedes nothing and that the record wins on any disagreement — it is derived from the history, not authoritative over it;
  • separates what is settled from what is open;
  • tabulates the nine open items with their state, including which is the best next target (Donkey Kong 64's BNE v1, v0, -1 with IE = 0) and which two are deliberately not being changed for lack of an oracle (the NMI charge guard, tlut_type IA16).

That gets the readability benefit you asked for without rewriting a word of the history.


CodeRabbit hit its review rate limit on the first run; the push above re-triggers it.

Adopts the second Antigravity review, which is right and overrides its own
earlier nitpick: a test-vector PR should not carry 75 unrelated spelling
edits. driver.c is restored to its pre-sweep content.

The adjacency the first nitpick flagged is resolved without writing either
spelling -- the comment now says "a multi-texel ramp" instead of naming a
colour, so there is nothing to be inconsistent about. The repo-wide en-US
sweep lands in its own PR, driver.c included.

Rebuilt and regenerated: all 56 goldens byte-for-byte unchanged.
@doublegate

Copy link
Copy Markdown
Owner Author

Adjudication of the second Antigravity review

Suggestion — the ~25 unrelated spelling edits in driver.c inflate the diff. ADOPTED, and it overrides your own earlier nitpick.

You are right, and this is the better call of the two. The sweep only got here because I adopted the first review's nitpick about colourful/colorful on adjacent lines, and widening a one-line fix into a 75-replacement file sweep broke "one logical change per commit" to fix a cosmetic inconsistency. That trade was wrong.

driver.c is restored to its pre-sweep content (9c1072d), so the diff is back to the two vectors, their registration, and the two corrected comments.

The adjacency you flagged first is still resolved — but without writing either spelling. The sentence no longer names a colour at all:

-// An earlier revision of this comment said "the colorful ramp"; see V11 for why that
+// An earlier revision of this comment described the picture as a multi-texel ramp;

So there is nothing left to be inconsistent about, and no en-GB text was reintroduced either. Both reviews are satisfied without the scope creep.

The repo-wide en-US sweep lands in its own PR, driver.c included. It is already prepared and validated: 1,452 replacements across 132 files, excluding ref-docs/ (immutable research corpus), n64brew_wiki/ (CC BY-SA mirror), and third_party/ (vendored libdragon); with lightgrey protected because it is a shields.io badge colour parameter inside a URL, not prose, and GitHub Actions' literal `cancelled` status protected because it names an external system's value. Verified in advance that no public item is renamed (so no API break), no CI job filters on an affected test name, and no excluded directory cites one (so nothing desyncs).

Nitpick — the CHANGELOG Fixed entry does not mention the driver.c spelling sweeps. MOOT, resolved by the above. There is no sweep in this PR any more, so the entry is complete as written: it documents exactly the two comment corrections that remain.

Rebuilt and regenerated after the revert — all 56 goldens byte-for-byte unchanged, so nothing about the vectors moved.

@github-actions

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This PR adds two Angrylion conformance test vectors (tex_tri_neg_dsdx_16 and tex_tri_right_major_16) to the test harness, updating vector generator code, changelog, and documentation to record that the mirrored-text defect (R-18) is not caused by the RDP rasterizer.

Blocking issues

None found.

Suggestions

  • crates/rustyn64-test-harness/vectors-gen/driver.c (line 1373): TEX_BLOCK(0xE0, 0, 1, -0x20, ...) passes a negative hex literal (-0x20). Depending on how TEX_BLOCK shifts and masks its arguments, using a signed integer literal can trigger sign-extension edge cases during bitwise operations; consider using explicit two's complement values (e.g., 0xFFE0 or an explicit uint32 cast) for clarity and safety.

Nitpicks

  • crates/rustyn64-test-harness/vectors-gen/driver.c (lines 2175–2184): Struct fields for v41 and v42 repeat identical magic constants (0x2000, 0x1000, 8, 8, 2, 0x3000).

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

@doublegate

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 30, 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.

@doublegate
doublegate merged commit cbf1651 into main Jul 30, 2026
11 checks passed
@doublegate
doublegate deleted the test/mirrored-text-dsdx-winding-vectors branch July 30, 2026 00:21
doublegate added a commit that referenced this pull request Jul 30, 2026
* chore: write the repo in en-US, and gate it so it stays that way

The project standard is en-US. The tree had drifted to en-GB -- and I made that
worse: on #202 I rejected a reviewer's generalising -> generalizing suggestion
on the measured grounds that the repo was consistently en-GB (37 "behaviour" to
3). The measurement was right and the conclusion was backwards: consistency with
a drift is not a reason to keep drifting.

1,505 replacements across 131 files, from 51 stem-level pairs applied
case-preservingly, so every inflection of a stem is covered by one entry.
Touches prose, doc comments, assertion messages, and private test-function
names.

NO public item is renamed, so there is no API change -- checked by enumerating
`pub fn|struct|enum|const|static|trait|type|mod` names against the stem list and
finding none. Also checked in advance:

- no CI job or script filters on an affected test name
- no excluded directory cites an affected test name, so swept and unswept text
  cannot desync
- no verbatim external quotation is altered (searched for quoted phrases beside
  a citation marker; the one hit was our own italic emphasis, not a quote)

Excluded trees, and why:
  ref-docs/       immutable research corpus -- corrections land as new dated
                  supplemental files, never in-place rewrites (module 40)
  n64brew_wiki/   CC BY-SA mirror, quoted verbatim by design
  ref-proj/       study clones of other emulators
  third_party/    vendored libdragon; not our prose to edit

Two literals are preserved because they are values, not words: `lightgrey` in a
shields.io badge URL (a color PARAMETER), and GitHub Actions' own `cancelled`
run status where the CHANGELOG quotes it. Stems whose form is correct in en-US
too were deliberately left out of the list -- analysis, synthesis, hypothesis,
peripheral, exercise, precise, premise, promise, otherwise, likewise, bitwise,
controlled, installed, stalled -- since a stem there would corrupt a correct
word.

Adds scripts/check_en_us.sh and an `en-US spelling` CI job, because a one-off
sweep decays: nothing fails when one en-GB form returns, so it survives review
and the next has precedent. This repo already demonstrates the pattern --
markdownlint runs in pre-commit only, and an MD040 violation sat unnoticed on
main until #203. The gate is mutation-checked in BOTH directions (fails on the
unswept tree at 1,355 lines, passes on the swept one at 443 files), strips
allowed literals rather than skipping their whole line so a permitted value
cannot shield a real hit beside it, and documents a per-line `spell-exempt`
escape hatch. Also wired into pre-commit for a faster local loop, with CI as the
unskippable gate.

Gates: fmt, clippy -D warnings, cargo test --workspace, rustdoc -D warnings,
no_std thumbv7em, markdownlint, check_no_roms, check_en_us -- all green.

* fix(en-us): repair 4 malformed words the sweep produced, and gate the class

Adjudicates the Antigravity review on #204, which caught one of four
corruptions. A before/after word-pair audit over the whole diff caught all
four, and it is the check I should have run before committing the sweep:

  characteris -> characteriz   corrupted  characteristics -> characteriztics
  optimis     -> optimiz       corrupted  optimistic      -> optimiztic
  realis      -> realiz        corrupted  realistically   -> realiztically
  centre      -> center        corrupted  centred         -> centerd

All four are stems matching INSIDE a word already correct in en-US. The gate
passed on the corrupted tree, and correctly so -- the outputs are not en-GB
either, so an en-GB pattern cannot see them.

The same defect was in the gate itself, in both directions: bare -is stems made
it report `characteristics`, `optimistic` and `realistically` as violations.
Split the stem list in two -- PLAIN_STEMS matched as substrings, ISE_STEMS
requiring an en-GB verb/noun ending -- so `optimis+tic` no longer matches while
`optimis+ation` still does.

Also:

- Added a MALFORMED backstop list so these specific non-words cannot return,
  with a comment saying plainly that it is a backstop for forms already seen and
  NOT a substitute for auditing a future sweep's word pairs.
- Added `cancelled`/`cancelling` and `centring` as stems. Their omission left
  real en-GB spellings unswept -- `the_sign_of_a_cancelled_zero_...` (renamed)
  and two prose sites -- while the value references keep the backticked
  `cancelled` form that names GitHub Actions' literal status.
- `centring` and `cancelling` are listed separately because neither CONTAINS its
  sibling stem, which is how a stem list silently under-matches.

The gate is now mutation-checked three ways: it catches an injected en-GB word,
catches an injected malformed word, and does NOT flag optimistic /
realistically / characteristics / synthesis / analysis.

Antigravity also reported scripts/check_en_us.sh as missing from the patch;
rejected as incorrect -- `git ls-files` lists it and the `en-US spelling` CI job
PASSED on this PR, which is impossible if the script were absent.

Gates: fmt, clippy -D warnings, cargo test --workspace, rustdoc -D warnings,
no_std thumbv7em, markdownlint, check_no_roms, check_en_us -- all green.
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