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).

### Added — 4-bit (I4) textures oracle-validated (gap-analysis Stage D, ledger R-7)

- **The hardware-canonical 4-bit texture path is validated against Angrylion.**
Studying the oracle's source corrected a misconception: **there is no 4-bit
texel *load*** — a 4-bit texture-image load is invalid on hardware (Angrylion
crashes the RDP pipeline). Games load 4-bit textures by setting an 8-bit texture
image + 8-bit LOAD tile (raw packed bytes) and rendering with a separate 4-bit
tile that extracts nibbles at fetch. New conformance vector `tex_tri_i4_16`
drives that idiom — eight I4 texels packed two-per-byte, an 8-bit `Load Tile`,
a 4-bit render tile — and RustyN64's existing 8-bit load + 4-bit `fetch_texel`
reproduce Angrylion **byte-for-byte** (six distinct non-zero texels, so an
all-zero no-op load would render black and fail). No new "4-bit load" code — the
bail on a 4-bit texture image already matches hardware. R-7's remaining gaps
(the 32-bit `Load Block` split; a direct 4-bit LOAD tile with an 8-bit texture
image) are now precisely characterized and stay open.

### Added — memory-access latency `M` charged (gap-analysis Stage D, ledger C-1)

- **Uncached RCP-register `M` = 22 PClocks — MEASURED.** Derived from the
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 @@ -318,6 +318,22 @@ fn tex_tri_fixed_16_matches_angrylion() {
);
}

/// A **4-bit (I4) textured triangle** — the hardware-canonical 4-bit texture path.
/// There is no 4-bit texel *load* on the N64 (a 4-bit texture-image load is invalid;
/// Angrylion crashes the pipeline, ledger R-7): games load 4-bit textures by setting
/// an 8-bit texture image + 8-bit LOAD tile, loading half as many texels raw, then
/// rendering with a SEPARATE 4-bit tile that extracts nibbles at fetch. This vector
/// drives that path — eight I4 texels packed two-per-byte, an 8-bit `Load Tile`, then
/// a 4-bit render tile — and proves RustyN64's existing 8-bit load + 4-bit `fetch_texel`
/// reproduce Angrylion byte-for-byte (no new "4-bit load" code, matching hardware).
#[test]
fn tex_tri_i4_16_matches_angrylion() {
assert_matches(
"tex_tri_i4_16",
include_bytes!("vectors/tex_tri_i4_16.rvec"),
);
}

/// A **COPY-mode Texture Rectangle** (16-bit) — the first texture path validated
/// against Angrylion. Copy mode blits texels straight from TMEM to the colour image
/// (no combiner, no 1-cycle texel pipeline), so it sidesteps the gaps `tex_tri_16`
Expand Down
Binary file not shown.
41 changes: 41 additions & 0 deletions crates/rustyn64-test-harness/vectors-gen/driver.c
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,42 @@ static const uint32_t V19_PRIM_COMBINER_32[] = {
SHADE_BLOCK_FLAT(0x11, 0x22, 0x33, 0xFF), // distinct shade (must NOT appear)
};

// V20: a 4-bit (I4) TEXTURED triangle — the hardware-canonical 4-bit texture path.
// There is NO 4-bit texel LOAD on the N64: a 4-bit texture-image load is invalid
// (Angrylion crashes the pipeline; ledger R-7). Games load 4-bit textures by
// LYING about the format — set an **8-bit** texture image and 8-bit LOAD tile,
// load half as many texels raw, then render with a SEPARATE **4-bit** tile that
// reads the same TMEM and does the nibble extraction at fetch. This vector proves
// that path end to end: eight I4 texels with DESCENDING non-zero intensities
// (0xF,0xD,0xB,0x9,0x7,0x5,0x3,0x1) are packed two-per-byte into four 8-bit bytes
// (0xFD 0xB9 0x75 0x31) at 0x3000, loaded by an 8-bit Load Tile into TMEM word 0,
// then sampled left-to-right by a 4-bit (format I, size 0) render tile whose dx.S
// advances one texel per pixel, so each column is a DISTINCT non-zero intensity.
// Non-zero-and-distinct on purpose: texel 0 = 0 renders black, identical to an
// unloaded (all-zero) TMEM, so a black golden could not tell a correct load from a
// no-op. Angrylion defines the golden; RustyN64's existing 8-bit load + 4-bit fetch
// must match it byte-for-byte.
static const uint16_t TEX_I4_RAMP[2] = {0xFDB9u, 0x7531u}; // 4 bytes = 8 I4 nibbles
static const uint32_t V20_TEX_TRI_I4_16[] = {
0x2F0008F0u, 0x00000000u, // Set Other Modes: 1-cycle, bi_lerp0=1, persp off
0x3C000000u, 0x00000041u, // Set Combine Mode: rgb_d=1 / a_d=1 (texel0 passthrough)
0x3D080003u, 0x00003000u, // Set Texture Image: 8-bit (size=1), width 4, addr 0x3000
0x35080200u, 0x07000000u, // Set Tile 7 (LOAD): 8-bit, line=1 (one 64-bit word), tmem 0
0x32000000u, 0x0700C000u, // Set Tile Size 7: SL0 TL0 SH3 TH0 (4 8-bit bytes)
0x34000000u, 0x0700C000u, // Load Tile 7: SL0 TL0 SH3 TH0
0x35800200u, 0x00000030u, // Set Tile 0 (RENDER): format I(4), size 0(4bit), line 1, mask_s=3
0x32000000u, 0x0001C000u, // Set Tile Size 0: SL0 TL0 SH7 TH0 (8 4-bit 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
0x00020000u, 0x00000000u, // XH = 2.0
0x00020000u, 0x00010000u, // XM = 2.0, DxMDy = 1.0
// S base 0, T base 0, W base 1.0; dx.S int = 0x20 (the R-13 s.5 scale advances
// the texel index one per pixel), so columns sample texels 0,1,2,...; rest 0.
TEX_BLOCK(0, 0, 1, 0x20, 0, 0, 0, 0, 0),
};

// ---- Seeded fuzz generator (SplitMix64) ----
//
// A reproducible pseudo-random corpus: the seed and this generator's source fully
Expand Down Expand Up @@ -917,5 +953,10 @@ int main(int argc, char **argv) {
sizeof(V19_PRIM_COMBINER_32) / 4, V19_PRIM_COMBINER_32};
if (emit_vector(&v19, out_dir)) return 1;

Vector v20 = {"tex_tri_i4_16", 0x2000, 0x1000, 8, 8, 2,
sizeof(V20_TEX_TRI_I4_16) / 4, V20_TEX_TRI_I4_16,
0x3000, 2, TEX_I4_RAMP};
if (emit_vector(&v20, out_dir)) return 1;

return 0;
}
Loading