Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ All notable changes to RustyN64 are documented here. The format is based on

Work toward `v0.8.0 "Breadth"` — the accuracy battery (Phase 7).

### Fixed — the TLUT lookup is gated on `tlut_en`, not the tile format (R-18)

- **`Set Other Modes.tlut_en` (bit 47) was not decoded at all**, so the palette
lookup keyed off the tile's *format* field. That is wrong in both directions: a
CI tile with `tlut_en` clear was still palette-mapped, and a non-CI tile with it
set was not.
- The oracle settled the `tlut_en = 0` behaviour instead of it being guessed:
`ci4_tlut_disabled_16` is byte-identical to `tex_tri_ci4_tlut_16` apart from
that one bit, and the goldens are **the full palette versus all black**.
- `tlut_en` and `tlut_type` (bit 46) are now decoded and the lookup is gated.
IA16 palettes remain **deferred** — the flag is decoded so it is no longer
silently ignored, but the lookup still assumes RGBA16 and implementing IA16
without a vector would be inventing behaviour.
- Battery is now **53 probes** (40 RDP + 13 VI); mutation-checked by removing the
gate.

### Added — first `Load Block` oracle coverage; its count is inclusive (R-18)

- **`Load Block` (0x33) had zero vector coverage** — every texture vector loaded
Expand Down
63 changes: 63 additions & 0 deletions crates/rustyn64-rdp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1117,6 +1117,31 @@ pub struct OtherModes {
/// Detail-texture enable (bit 50): keeps the LOD fraction live when
/// magnifying (R-13).
pub detail_tex_en: bool,
/// **TLUT enable** (bit 47). N64brew *…/Commands* §0x2F: *"`tlut_en`: Enables
/// Texture Look-Up Table (TLUT) sampling. Texels are first fetched from low
/// TMEM that are then used to index a palette in high TMEM to find the final
/// color values."*
///
/// This is the flag that decides whether a palette lookup happens — **not**
/// the tile's format field. Keying off the format alone is wrong in two
/// directions, and **only one of them is fixed**:
///
/// - **Implemented:** a CI tile with `tlut_en` clear is no longer
/// palette-mapped. Pinned by `ci4_tlut_disabled_16`, whose golden is all
/// black where the `tlut_en`-set twin renders the full palette.
/// - **Deferred:** a **non-CI** tile with `tlut_en` set is still not
/// palette-mapped, though hardware would sample it through the TLUT. No
/// vector covers that case, and the RGBA/IA/I formats index the palette
/// differently enough that implementing it from the prose alone would be
/// inventing behaviour. It stays wrong-but-honest until a vector defines it,
/// the same posture as `tlut_type`'s IA16 palettes below.
pub tlut_en: bool,
/// TLUT texel format (bit 46): `false` = RGBA16, `true` = IA16
/// (N64brew *…/Commands* §0x2F). Decoded so it is available and so the flag
/// is not silently ignored; **IA16 palettes are still deferred** — the lookup
/// assumes RGBA16, and implementing IA16 without a vector would be inventing
/// behaviour rather than emulating it.
pub tlut_type: bool,
/// Mid-texel filter (bit 44, R-13): when set and the bilinear sample lands
/// exactly on the texel centre (`sfrac == tfrac == 0x10`), the four neighbours
/// are averaged instead of the 3-point triangle pick (Angrylion `tex.c`, the
Expand Down Expand Up @@ -3159,6 +3184,8 @@ impl Rdp {
persp_tex_en: (hi >> 19) & 1 != 0, // command bit 51
aa_enable: (lo >> 3) & 1 != 0, // command bit 3
rgb_dither_mode: ((hi >> 6) & 0x3) as u8, // command bits 39:38
tlut_en: (hi >> 15) & 1 != 0, // command bit 47
tlut_type: (hi >> 14) & 1 != 0, // command bit 46
Comment thread
coderabbitai[bot] marked this conversation as resolved.
sample_type: (hi >> 13) & 1 != 0, // command bit 45
mid_texel: (hi >> 12) & 1 != 0, // command bit 44
detail_tex_en: (hi >> 18) & 1 != 0, // command bit 50
Expand Down Expand Up @@ -3684,6 +3711,19 @@ impl Rdp {
let v = widen4(u32::from(self.nibble_at((off4 ^ swap) as usize, s)));
[v, v, v, v]
}
// The colour-index formats resolve through the palette **only when
// `Set Other Modes.tlut_en` is set** (bit 47) — the format field alone
// does not enable it (N64brew *…/Commands* §0x2F). With the flag clear
// the oracle renders a CI tile entirely black, which
// `ci4_tlut_disabled_16` pins: it is byte-identical to
// `tex_tri_ci4_tlut_16` apart from that one bit, and the two goldens are
// the full palette versus all black.
//
// Zero is what the oracle produces, not a mechanism claim: what the
// hardware *does* with un-TLUT'd index data is not documented in §0x2F,
// so this reproduces the observed result rather than inventing a
// reinterpretation of the index bits.
(2, _) if !self.other_modes.tlut_en => [0, 0, 0, 0],
(2, 1) => {
// CI8: 8-bit index into the TLUT.
let ci = self.tmem_byte(((off8 & 0x7FF) ^ swap) as usize);
Expand Down Expand Up @@ -4949,10 +4989,16 @@ mod tests {
/// **`fetch_texel` resolves CI8 and CI4 through the TLUT.** A CI index selects
/// a quadrupled RGBA16 entry in the high TMEM half; CI4 folds in the tile
/// palette as the high nibble.
///
/// `tlut_en` must be set: the lookup is gated on `Set Other Modes` bit 47, not
/// on the tile format (N64brew *…/Commands* §0x2F). This test previously left
/// it clear and passed only because the gate did not exist — it was asserting
/// the palette path while describing a machine that had not asked for it.
#[test]
fn fetch_texel_ci_through_the_tlut() {
// CI8: index 5 -> TLUT entry at 0x800 + 5*8 = 0x828 = 0xF801 (red).
let mut rdp = Rdp::new();
rdp.other_modes.tlut_en = true;
rdp.tmem_write(0, 5); // the index texel
rdp.tmem_write(0x828, 0xF8);
rdp.tmem_write(0x829, 0x01);
Expand All @@ -4965,6 +5011,7 @@ mod tests {
// CI4: nibble 5 (high, even s) + palette 3 -> index 0x35 -> entry at
// 0x800 + 0x35*8 = 0x9A8 = 0x07C1 (green).
let mut c = Rdp::new();
c.other_modes.tlut_en = true;
c.tmem_write(0, 0x50); // high nibble 5
c.tmem_write(0x9A8, 0x07);
c.tmem_write(0x9A9, 0xC1);
Expand Down Expand Up @@ -5925,6 +5972,22 @@ mod tests {
assert!(om.aa_enable);
assert!(om.alpha_compare_en);
assert_eq!(om.rgb_dither_mode, 1);

// **`tlut_en` (bit 47) and `tlut_type` (bit 46) decode independently.**
// They are ADJACENT bits, so a swapped extraction is the likely error and
// would pass any test that sets both or neither. Assert each with the other
// clear, in both polarities, so a swap fails and so does dropping either.
let om = |hi: u32| {
let mut r = Rdp::new();
r.set_other_modes(hi, 0);
r.other_modes
};
let a = om(1 << 15); // tlut_en only
assert!(a.tlut_en, "bit 47 must set tlut_en");
assert!(!a.tlut_type, "bit 47 must NOT set tlut_type");
let b = om(1 << 14); // tlut_type only
assert!(!b.tlut_en, "bit 46 must NOT set tlut_en");
assert!(b.tlut_type, "bit 46 must set tlut_type");
}

/// **The magic-matrix RGB dither matches Angrylion's `rgb_dither` cell-for-cell.**
Expand Down
4 changes: 4 additions & 0 deletions crates/rustyn64-test-harness/src/conformance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,10 @@ pub const RDP_VECTORS: &[(&str, &[u8])] = &[
"load_block_count_16",
include_bytes!("../tests/vectors/load_block_count_16.rvec"),
),
(
"ci4_tlut_disabled_16",
include_bytes!("../tests/vectors/ci4_tlut_disabled_16.rvec"),
),
];

/// Look up a committed vector's bytes by name.
Expand Down
16 changes: 16 additions & 0 deletions crates/rustyn64-test-harness/tests/rdp_conformance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,22 @@ fn load_block_count_16_matches_angrylion() {
assert_matches("load_block_count_16");
}

/// **A CI tile with `tlut_en` CLEAR gets no palette lookup (R-18).**
///
/// Byte-identical to `tex_tri_ci4_tlut_16` except for `Set Other Modes` bit 47,
/// so any difference is attributable to that one bit. N64brew *…/Commands* §0x2F
/// makes `tlut_en` — not the tile's format field — the flag that enables the
/// lookup, and the oracle agrees emphatically: with the flag set the eight
/// columns render the full palette; with it clear they render **all black**.
///
/// This matters because keying the lookup off the format alone is wrong in *both*
/// directions — it palette-maps a CI tile that asked not to be, and fails to
/// palette-map a non-CI tile that asked to be.
#[test]
fn ci4_tlut_disabled_16_matches_angrylion() {
assert_matches("ci4_tlut_disabled_16");
}

/// **A flat Fill Triangle matches Angrylion (regression guard for R-14).**
///
/// This left-major triangle (a vertical left edge at x=2, the hypotenuse widening
Expand Down
Binary file not shown.
44 changes: 44 additions & 0 deletions crates/rustyn64-test-harness/vectors-gen/driver.c
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,45 @@ static const uint16_t TEX_CI4_TLUT[12] = {
0xF801u, 0x07C1u, 0x003Fu, 0xFFFFu, // 0x3008: TLUT 0-3 red, green, blue, white
0xFFC1u, 0x07FFu, 0xF83Fu, 0x8421u, // TLUT 4-7 yellow, cyan, magenta, grey
};
// V39 (probe): the SAME CI4 tile as V37 but with **`tlut_en` CLEAR**.
//
// N64brew §0x2F makes `tlut_en` (bit 47) the flag that enables the palette
// lookup — not the tile's format field. RustyN64 keyed the lookup off the format
// alone, which is wrong in both directions. This vector asks the oracle what a CI
// tile does when the flag is clear, rather than guessing at it: everything else is
// byte-identical to V37, so any difference in the golden is attributable to the
// one bit.
static const uint32_t V39_CI4_TLUT_DISABLED_16[] = {
// Set Other Modes: 1-cycle, bi_lerp0, persp off, **tlut_en** (bit 47 = word-0
// bit 15; N64brew §0x2F "tlut_en: Enables Texture Look-Up Table (TLUT) sampling").
0x2F0008F0u, 0x00000000u, // tlut_en CLEAR (bit 47 = word-0 bit 15)
0x3C000000u, 0x00000041u, // Set Combine: rgb_d=1 / a_d=1 — pure TEXEL0 passthrough
// --- palette -> TMEM high ---
0x3D100003u, 0x00003008u, // Set Texture Image: **16-bit**, width 4, addr 0x3008
0x35400100u, 0x07000000u, // Set Tile 7 (TLUT): fmt CI(2), size 0 (4-bit), tmem word 0x100
0x30000000u, 0x0701C000u, // Load Tlut 7: uls=0 ult=0 lrs=0x1C (8 entries) lrt=0
// --- CI4 texel bytes -> TMEM low ---
0x3D080003u, 0x00003000u, // Set Texture Image: 8-bit, width 4, addr 0x3000
0x35080200u, 0x06000000u, // Set Tile 6 (LOAD): 8-bit, line 1, tmem 0
0x32000000u, 0x0600C000u, // Set Tile Size 6: uls0 ult0 lrs3 lrt0 (4 bytes)
0x34000000u, 0x0600C000u, // Load Tile 6
// --- render tile: format CI(2), size 0 (4-bit), palette 0 ---
0x35400200u, 0x00000030u, // Set Tile 0 (RENDER): fmt CI, 4-bit, line 1, mask_s=3
0x32000000u, 0x0001C000u, // Set Tile Size 0: uls0 ult0 lrs7 lrt0 (8 CI4 texels)
0x3F100007u, 0x00001000u, // Set Color Image: 16-bit, width 8, addr 0x1000
0x2D000000u, 0x00020020u, // Set Scissor: (0,0)-(8,8)
0x0A800020u, 0x00200000u, // op=0x0A (tex), lft=1, yl=32 ym=32 yh=0, tile 0
0x00000000u, 0x00000000u, // XL, DxLDy
0x00000000u, 0x00000000u, // XH = 0.0 — geometry identical to V37
0x00000000u, 0x00020000u, // XM = 0.0, DxMDy = 2.0
// The geometry spans all eight columns, as V37's does — but do NOT read that as
// eight-entry coverage here. With `tlut_en` clear every column renders black
// whatever its index, so this vector proves the **gate**, not per-index palette
// resolution. V37 is what proves the resolution; this one proves the flag
// suppresses it. Identical geometry is the point: it makes the two goldens
// differ by exactly one bit of input.
TEX_BLOCK(0, 0, 1, 0x20, 0, 0, 0, 0, 0),
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
static const uint32_t V37_TEX_TRI_CI4_TLUT_16[] = {
// Set Other Modes: 1-cycle, bi_lerp0, persp off, **tlut_en** (bit 47 = word-0
// bit 15; N64brew §0x2F "tlut_en: Enables Texture Look-Up Table (TLUT) sampling").
Expand Down Expand Up @@ -1988,6 +2027,11 @@ int main(int argc, char **argv) {
0x3000, sizeof(TEX_BLOCK_COUNT_TEXELS) / sizeof(uint16_t), TEX_BLOCK_COUNT_TEXELS};
if (emit_vector(&v38, out_dir)) return 1;

Vector v39 = {"ci4_tlut_disabled_16", 0x2000, 0x1000, 8, 8, 2,
sizeof(V39_CI4_TLUT_DISABLED_16) / 4, V39_CI4_TLUT_DISABLED_16,
0x3000, sizeof(TEX_CI4_TLUT) / sizeof(uint16_t), TEX_CI4_TLUT};
if (emit_vector(&v39, out_dir)) return 1;

if (emit_vi_vectors(out_dir)) return 1;

return 0;
Expand Down
6 changes: 3 additions & 3 deletions docs/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ category `Failed: 0`; 90 suite-wide). The **golden-log** gate (`--test
golden_log`) replays 50,027 retired records at 0 diff against ares. A fourth,
the **synthetic visual golden** (`--test golden_frame`, T-31-005), executes the
FILL → VI scan-out path against a committed frame hash. The **accuracy battery now executes**
(`AccuracyScorer::default_battery`, 52 probes, 100% — see the table above); the
(`AccuracyScorer::default_battery`, 53 probes, 100% — see the table above); the
rest of the corpus (the real-ROM krom/240p visual goldens and the commercial
ROMs) is staged only — an oracle on disk that no gate executes yet.

Expand Down Expand Up @@ -213,7 +213,7 @@ prompted the change.
| AI audio DMA double-buffer | **done** — registers, FIFO, derived DAC rate, IRQ-on-start, delayed-carry bug (Sprint 1); the real mixer microcode produces PCM on the RSP (Sprint 2); the frontend drain + resampler landed in Phase 6 | Phase 4 |
| PI/SI DMA, PIF/CIC boot, FlashRAM machine, saves | **done** (v0.6.0) — PI/SI DMA, the CIC handshake, all four save backends incl. the FlashRAM command machine, and both HLE and real-PIF boot | Phase 5 |
| Frontend egui shell | **done** (v0.7.0) — presents the real machine (VI scan-out, AI drain, SI input) with save-states / rewind / run-ahead; a wasm browser demo | Phase 6 |
| Accuracy battery / breadth / reach | **battery wired** — it scores the 52 committed Angrylion vectors, RDP + VI (100%); the commercial-corpus breadth and the reach features are Phases 7–8 | Phases 7–8 |
| Accuracy battery / breadth / reach | **battery wired** — it scores the 53 committed Angrylion vectors, RDP + VI (100%); the commercial-corpus breadth and the reach features are Phases 7–8 | Phases 7–8 |

## Chip → crate map

Expand Down Expand Up @@ -255,7 +255,7 @@ entropy, threads and unordered collections anywhere in the core.
| n64-systemtest, **CPU/COP0/TLB/COP1** categories (Phase 1's criterion) | **yes** — ROM committed, and the runner with it | **MET: `Failed: 0`**, across 917 tests started. Reproduce with `cargo test -p rustyn64-test-harness --release --test systemtest -- --ignored`. **90** assertions still fail suite-wide, down from 413 (and from 93 before the Phase 5 cart/PIF/SI work); **none are RSP-prefixed** (the RSP category is Phase 2's criterion and is now 0), leaving the RDP rasterizer (Phase 3), the MI's RDRAM repeat mode, and the remaining cart/PIF corners |
| n64-systemtest, **RSP** category (Phase 2's criterion) | **yes** — same runner | **MET: `Failed: 0`** across 917 tests started — every RSP-prefixed test passes (verified by dumping per-test failures; 0 begin with `RSP`). The full VU ISA, vector load/store, reserved opcodes, `BREAK`-in-delay-slot, and the DPC registers landed in #41–#44 |
| ParaLLEl-RDP fuzz suite (RDP bit-exactness) | source cloned, suite not set up | not started |
| Accuracy battery (`AccuracyScorer::default_battery`) | **yes** — 52 probes across two oracle suites: 39 Angrylion RDP rasteriser vectors + 13 Angrylion VI scan-out vectors (expected values are the oracle's, never our own output; RDP probes are byte-for-byte, VI probes are RGB-only since the 4th byte is coverage) | **100% (52/52)** — asserted by `default_battery_matches_the_oracle`; both suites are asserted to contribute, and an empty battery now scores 0%, not a vacuous 100% |
| Accuracy battery (`AccuracyScorer::default_battery`) | **yes** — 53 probes across two oracle suites: 40 Angrylion RDP rasteriser vectors + 13 Angrylion VI scan-out vectors (expected values are the oracle's, never our own output; RDP probes are byte-for-byte, VI probes are RGB-only since the 4th byte is coverage) | **100% (53/53)** — asserted by `default_battery_matches_the_oracle`; both suites are asserted to contribute, and an empty battery now scores 0%, not a vacuous 100% |
| Visual golden / screenshots | **yes** — krom + 240p + commercial staged | **first frame MET** (T-31-005) — a synthetic RDP FILL list rendered through the full command-decode → FILL → VI scan-out path is pinned byte-exact against a committed golden hash (`--test golden_frame`). Real-ROM krom/240p goldens await cartridge boot (Phase 5) |

The distinction matters: "oracle available" means the ROM is on disk; it says
Expand Down
Loading