Skip to content

feat(harness): pin the CI4 + TLUT path against Angrylion (R-18) - #182

Merged
doublegate merged 3 commits into
mainfrom
feat/ci4-tlut-vector
Jul 29, 2026
Merged

feat(harness): pin the CI4 + TLUT path against Angrylion (R-18)#182
doublegate merged 3 commits into
mainfrom
feat/ci4-tlut-vector

Conversation

@doublegate

Copy link
Copy Markdown
Owner

The colour-indexed path had zero oracle coverage

No committed vector exercised Load Tlut at all before this one — which is why
nobody could say whether CI4 worked, and why it looked like a plausible cause for
R-18's black frame.

Verifying the encoding first found two errors in my own vector

The previous PR recorded a provisional result because my first attempt
disagreed with Angrylion and I could not tell whose fault that was. Checking
N64brew …/Commands §0x30 settled it — the vector was wrong, twice:

  • lower_right.s is command bits 23:12. An 8-entry palette is
    (8-1) << 2 = 0x1C shifted by 12. I had shifted by 14, which decodes as 29
    entries — so Angrylion dutifully loaded 29 entries from a 16-byte source.
  • The TLUT tile must be 4-bit, and neither RGBA nor YUV (the section's own
    Hazards). I had set it 16-bit. Only the texture image is 16-bit.

Result

Re-authored to the spec, Angrylion renders the intended palette and RustyN64
matches it byte-for-byte
:

f801 07c1 003f ffff ffc1 07ff
red  green blue white yellow cyan

tex_tri_ci4_tlut_16 is committed. Battery is now 51 probes (38 RDP + 13 VI).

Mutation-checked: changing tlut_lookup's stride from the quadrupled 8 to 4
turns it red at pixel (3,2).

What this settles, and what it does not

Settles: CI4 + TLUT is now genuinely eliminated as R-18's cause — by a
committed oracle rather than by inference. The previous entry's "weakly
de-prioritised" is upgraded and the ledger says so.

Does not settle: tlut_en (bit 47) is still undecoded and still a real
defect — but demonstrably not this one. The vector passes because our lookup
keys off the CI format, which here coincides with tlut_en being set; a vector
with tlut_en clear would still diverge.

R-18's video cause remains open. The remaining texture suspects are precisely
the ones this vector does not cover: Load Block vs Load Tile addressing at
real texture sizes, the odd-line swap, and mip/LOD tile selection under a live
command stream.

Note on process

I discarded the first golden rather than committing it. In a test-as-spec project
a wrong golden is worse than no golden — it converts an authoring error into the
specification, and every later result built on it stops being evidence.

Gates

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 — each run separately, exit
status checked.

Oracle: no emulation behaviour change (a new vector + docs), so n64-systemtest
is unchanged at 90 suite-wide, Phase 1 Failed: 0, per docs/STATUS.md.

🤖 Generated with Claude Code

Verifying the Load Tlut encoding against N64brew ...Commands 0x30 found two
errors in my first attempt, both of which had made Angrylion render a near-empty
palette:

- lower_right.s is command bits 23:12, so an 8-entry palette is (8-1) << 2 =
  0x1C shifted by TWELVE. I had shifted by 14, which decodes as 29 entries.
- the section's own Hazards require the TLUT tile to be 4-bit and neither RGBA
  nor YUV. I had set it 16-bit; only the TEXTURE IMAGE is 16-bit.

Re-authored to the spec, Angrylion renders the intended palette - f801 07c1 003f
ffff ffc1 07ff - and RustyN64 matches it byte-for-byte. tex_tri_ci4_tlut_16 is
now committed, the first vector to exercise Load Tlut at all: the colour-indexed
path had zero oracle coverage before this, which is why nobody could say whether
it worked. Battery is 51 probes (38 RDP + 13 VI).

Mutation-checked: changing tlut_lookup's stride from the quadrupled 8 to 4 turns
it red at pixel (3,2).

This settles the previous entry's provisional result. CI4 + TLUT is now GENUINELY
eliminated as R-18's cause rather than weakly de-prioritised, and it is
eliminated by a committed oracle instead of by inference.

tlut_en remains undecoded and is still a real defect, but demonstrably not this
one - the vector passes because our lookup keys off the CI format, which here
coincides with tlut_en being set.

R-18's video cause stays open. The remaining texture suspects are the ones this
vector does not cover: Load Block vs Load Tile addressing at real texture sizes,
the odd-line swap, and mip/LOD tile selection under a live command stream.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 29, 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: 0daee093-7837-4850-a797-06df2184be7d

📥 Commits

Reviewing files that changed from the base of the PR and between 38b9773 and 2c90e33.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • crates/rustyn64-test-harness/tests/vectors/tex_tri_ci4_tlut_16.rvec
  • crates/rustyn64-test-harness/vectors-gen/driver.c
  • docs/accuracy-ledger.md
📝 Walkthrough

Walkthrough

The PR adds a committed CI4/TLUT Angrylion conformance vector, registers it in the shared RDP battery, adds regression coverage, and updates R-18 and accuracy documentation to reflect the corrected Load Tlut encoding.

Changes

CI4 TLUT conformance

Layer / File(s) Summary
Generate the CI4 TLUT vector
crates/rustyn64-test-harness/vectors-gen/driver.c
V37 defines CI4 texture indices, TLUT palette data, Load Tlut and tile commands, rendering configuration, and emits tex_tri_ci4_tlut_16.rvec.
Register and validate the vector
crates/rustyn64-test-harness/tests/vectors/tex_tri_ci4_tlut_16.rvec, crates/rustyn64-test-harness/src/conformance.rs, crates/rustyn64-test-harness/tests/rdp_conformance.rs
The committed RVEC is added to RDP_VECTORS and validated by an Angrylion byte-comparison regression test.
Record coverage and R-18 status
CHANGELOG.md, docs/accuracy-ledger.md, docs/STATUS.md
Documentation records the corrected TLUT encoding, removal of the CI4/TLUT cause, and the increase from 50 to 51 accuracy probes.

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

Sequence Diagram(s)

sequenceDiagram
  participant VectorDriver
  participant Rvec
  participant ConformanceTest
  participant Angrylion
  VectorDriver->>Rvec: Emit tex_tri_ci4_tlut_16
  ConformanceTest->>Rvec: Load committed vector
  ConformanceTest->>Angrylion: Compare rendered output
  Angrylion-->>ConformanceTest: Return golden result
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 10
✅ Passed checks (10 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the Conventional Commits format and clearly summarises the CI4 + TLUT harness change.
Description check ✅ Passed The description is directly related to the CI4 + TLUT vector, verification, and R-18 ledger update.
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 PASS — the changelog explicitly says n64-systemtest is unchanged at 90 suite-wide and Phase 1 Failed: 0.
Docs-As-Spec Sync ✅ Passed PASS: the PR touches only rustyn64-test-harness plus docs/STATUS.md and docs/accuracy-ledger.md; no rustyn64-{cpu,rsp,rdp,audio,cart,core} code changed, so the sync rule is not triggered.
Changelog Entry For User-Visible Changes ✅ Passed CHANGELOG.md has an [Unreleased] R-18 entry covering the CI4+TLUT fix, including tex_tri_ci4_tlut_16 and the Load Tlut correction.
Measured, Never Tuned ✅ Passed PASS: the CI4/TLUT values are cited to N64brew in driver.c, and R-18 in docs/accuracy-ledger.md records the byte-for-byte plus mutation-checked measurement trail.
Unsafe Stays Out Of The Chip Crates ✅ Passed No unsafe syntax exists in crates; chip crates and rustyn64-core still carry #![forbid(unsafe_code)], and the PR only touches docs/test-harness/vector-gen files.

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

The V37 comment cited N64brew 0x30's hazard that the TLUT tile must not use RGBA
or YUV, and then encoded fmt = 0, which IS RGBA. The comment was right and the
code disagreed with it - the exact hazard this project has hit four times before.

Now fmt = CI(2) with size 0 (4-bit), so the encoding matches both the hazard and
the sentence above it. Regenerated; RustyN64 still matches Angrylion byte-for-byte
and the mutation check still holds, which is itself informative: neither
implementation's Load Tlut consults the format field here, so the hazard is about
hardware behaviour the software reference does not model. Following it anyway
costs nothing and keeps the vector a faithful command stream.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@doublegate

Copy link
Copy Markdown
Owner Author

Adjudicating the Antigravity review — one adopted, two rejected with evidence.

TLUT tile format is RGBA, contradicting the comment — ADOPTED, and it is the best of the three. The comment cited §0x30's hazard that the TLUT tile must not use RGBA or YUV, and then encoded fmt = 0, which is RGBA. The comment was right and the code disagreed with it — the exact hazard this project has recorded four times before.

Now fmt = CI(2), size = 0 (4-bit). Regenerated; RustyN64 still matches Angrylion byte-for-byte and the mutation check still holds. That is itself informative: neither implementation's Load Tlut consults the format field here, so the hazard describes hardware behaviour the software reference does not model. Following it anyway costs nothing and keeps the vector a faithful command stream.

12 as the preload length — REJECTED, and adopting it literally would have broken the vector. The field is a texel count, not a byte count:

uint32_t preload_len = v->n_texels * 2u;

sizeof(TEX_CI4_TLUT) is 24, which would declare twice the payload and read past the array. Every existing texture vector uses the same convention (V20 passes 2 for a uint16_t[2]). The unit is carried by the struct field name n_texels.

Host little-endian byte order — REJECTED, premise incorrect. The emitter composes big-endian explicitly rather than memcpy-ing host uint16_ts:

rdram_put_word(byte_addr + i * 2u, (hi << 16) | lo);

That is the same path every committed texture vector already uses; if it inverted byte order, all 38 of them would be wrong rather than passing. Worth noting the concern was reasonable — it just does not survive reading the emitter.

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

🤖 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 43-48: Correct the vector count from 39 to 38 in CHANGELOG.md
lines 43-48 and docs/accuracy-ledger.md line 413, preserving the surrounding
text; the resulting battery total is 51 with 13 VI vectors. docs/STATUS.md
requires no direct change because it remains the source of truth for current
accuracy-battery counts.

In `@crates/rustyn64-test-harness/vectors-gen/driver.c`:
- Around line 1204-1205: Update the TEX_BLOCK test in the vector-generation
driver to sample all eight TLUT entries, including entries 6 and 7 represented
by 0xF83F and 0x8421. Extend the draw or add a second draw with terminal-entry
addressing, and update the committed golden expectation so mutations truncating
the Load Tlut range or mishandling the final entries fail.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 79a08e9f-077d-4e32-b5fa-aa66fa32dae1

📥 Commits

Reviewing files that changed from the base of the PR and between 62220b6 and 38b9773.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • crates/rustyn64-test-harness/src/conformance.rs
  • crates/rustyn64-test-harness/tests/rdp_conformance.rs
  • crates/rustyn64-test-harness/tests/vectors/tex_tri_ci4_tlut_16.rvec
  • crates/rustyn64-test-harness/vectors-gen/driver.c
  • docs/STATUS.md
  • docs/accuracy-ledger.md

Comment thread CHANGELOG.md
Comment thread crates/rustyn64-test-harness/vectors-gen/driver.c
Two review findings.

The triangle only covered six columns, so palette entries 6 and 7 were declared
and never fetched - the vector claimed eight distinct entries and proved six.
Widening the edges (XH/XM start at column 0, DxMDy 2.0) makes every row reach all
eight, and the golden's last rows now read f801 07c1 003f ffff ffc1 07ff f83f
8421: all eight entries rendered, none missing. RustyN64 still matches
byte-for-byte, and the stride mutation still turns it red.

The count was wrong: 38 RDP vectors are registered, not 39. The 39 is the TEST
count - 38 vectors plus the fuzz-corpus test - and docs/STATUS.md already had
38/51 correctly, so the ledger and CHANGELOG were contradicting the file that is
the single source of truth for it. Both now say 38 RDP vectors, 51 battery
probes.

The first is the more instructive: a vector that declares more coverage than it
exercises is the same shape as an assertion a broken machine also satisfies. The
palette had eight entries because eight made the test look thorough, and two of
them were decoration until now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This PR adds a CI4 texture with TLUT lookup RDP test vector (tex_tri_ci4_tlut_16), generator code in driver.c, and harness assertions matching Angrylion oracle output while updating tracking documentation.

Blocking issues

None found.

Suggestions

  • crates/rustyn64-test-harness/vectors-gen/driver.c:1175: TEX_CI4_TLUT packs CI4 texel indices into 16-bit integer literals (0x0123u, 0x4567u). If emit_vector writes host memory bytes directly without explicit big-endian swapping, little-endian host compilers will emit byte 0x23 before 0x01 at 0x3000. Using uint8_t for raw texel bytes (e.g., {0x01, 0x23, 0x45, 0x67}) avoids host endianness ambiguity.
  • crates/rustyn64-test-harness/vectors-gen/driver.c:1949: Vector v37 uses a hardcoded literal 12 for the length of TEX_CI4_TLUT instead of sizeof(TEX_CI4_TLUT) / sizeof(TEX_CI4_TLUT[0]) (unlike sizeof(V37_TEX_TRI_CI4_TLUT_16) / 4 used for the command buffer).

Nitpicks

  • docs/accuracy-ledger.md:413: The ledger update notes that Set Other Modes.tlut_en (bit 47) is still not decoded in the core decoder; ensure a follow-up item tracks enforcing tlut_en and tlut_type.

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

@doublegate
doublegate merged commit 14ea37a into main Jul 29, 2026
11 checks passed
@doublegate
doublegate deleted the feat/ci4-tlut-vector branch July 29, 2026 09:35
doublegate added a commit that referenced this pull request Jul 29, 2026
… length

Review follow-up, both suggestions adopted.

TEX_BLOCK_COUNT read like a quantity while holding texel colours; it is now
TEX_BLOCK_COUNT_TEXELS. The vector is about a COUNT question, which made the old
name actively misleading next to the lrs/uls fields it sits beside.

The preload length is now sizeof(...) / sizeof(uint16_t) rather than a literal 2,
so it cannot drift if the array changes. Note the divisor is load-bearing: the
field is a texel count, not a byte count, so a bare sizeof would declare twice
the payload - which is why I rejected the same suggestion in its unqualified form
on #182. Here it was offered correctly.

The other 20 vectors still pass literals. Not swept in this PR: that is a
mechanical change across the whole generator and would bury a one-vector fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
doublegate added a commit that referenced this pull request Jul 29, 2026
…on (#184)

* feat(harness): pin Load Block's inclusive texel count against Angrylion (R-18)

Load Block (0x33) had zero vector coverage - every texture vector loaded through
Load Tile - while Ocarina issues 4,374 Load Blocks of its 74,508 commands. It was
the dominant load path in a live retail stream and entirely unpinned.

The open question was the texel count. 0x33's prose says "lower_right.s -
upper_left.s determines the number of texels" with no +1, while Load Tile is
inclusive and load_block computes shi - slo + 1. The two readings disagree and
prose cannot settle it.

A minimal vector can. load_block_count_16 uses uls=0, lrs=1 - a load of either
one texel or two - on a single line with dxt=0, so neither the odd-line swap nor
any multi-line TMEM layout can confound the answer. That minimality is the point:
two earlier, larger Load Block vectors were authored and discarded because their
goldens showed Angrylion reading unwritten TMEM, and the fix was to remove
layout from the experiment rather than to keep guessing at it.

Angrylion loads two. The count is inclusive, our shi - slo + 1 is correct, and
RustyN64 matches the golden byte-for-byte. Mutation-checked: dropping the +1
turns it red. Battery is 52 probes (39 RDP + 13 VI).

So the leading Load Block hypothesis joins the others as refuted. The path is no
longer wholly unpinned, but the coverage is minimal by construction - multi-line
loads, the odd-line swap under a real dxt, and non-16-bit texel sizes all remain
untested, and the ledger says so rather than implying the command is now covered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(vectors): name the texel array for its contents; compute its length

Review follow-up, both suggestions adopted.

TEX_BLOCK_COUNT read like a quantity while holding texel colours; it is now
TEX_BLOCK_COUNT_TEXELS. The vector is about a COUNT question, which made the old
name actively misleading next to the lrs/uls fields it sits beside.

The preload length is now sizeof(...) / sizeof(uint16_t) rather than a literal 2,
so it cannot drift if the array changes. Note the divisor is load-bearing: the
field is a texel count, not a byte count, so a bare sizeof would declare twice
the payload - which is why I rejected the same suggestion in its unqualified form
on #182. Here it was offered correctly.

The other 20 vectors still pass literals. Not swept in this PR: that is a
mechanical change across the whole generator and would bury a one-vector fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(status): refresh the test count and stop calling the battery unexecuted

Two stale claims in the file that is meant to be the single source of truth for
current numbers.

The test count said 712. The actual figure from cargo test --workspace is 803, so
it had been stale across many PRs rather than by the one this branch adds - the
review flagged it as off by one, and it was off by 91.

The corpus paragraph still listed the accuracy battery among the oracles that are
"staged only - an oracle on disk that no gate executes yet", directly
contradicting both the 100% (52/52) row in the table above it and the line that
says the probe battery is now real. The battery does execute; only the real-ROM
visual goldens and the commercial ROMs remain staged, and the sentence now says
that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <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