diff --git a/docs/accuracy-ledger.md b/docs/accuracy-ledger.md index 26f15608..0a1324c7 100644 --- a/docs/accuracy-ledger.md +++ b/docs/accuracy-ledger.md @@ -389,20 +389,25 @@ labelled as one until something reads the register on hardware. > the count from 93 to **90** (see "Measured n64-systemtest impact" below). > **`docs/STATUS.md` is the single authoritative source for the current count.** +Residuals whose record outgrew a table cell (over 4,000 characters) have their full +text under [`residuals/`](residuals/README.md) and are linked from their row here. +Those files are append-only too: superseded claims stay, marked, rather than being +edited away. + | # | Symptom | Suspected mechanism | Classification | Status | | --- | --- | --- | --- | --- | | R-14 | **RESOLVED** — `triangle_fill` was rendering triangle edges **4× too steep/fast**: a slope that should widen a triangle 0.25 px per pixel-row widened it 1 px per row. Found by the T-33-005 conformance gate on its first triangle vector (Angrylion rendered a near-vertical line, RustyN64 a staircase) | The edge slopes `DxHDy`/`DxMDy`/`DxLDy` are **dx per pixel-row** (N64brew Wiki *Reality Display Processor / Commands* **§0x08 through 0x0F – Fill Triangle**, the Base Command word tables — mirrored at `n64brew_wiki/markdown/Reality Display Processor/Commands.md`. The `dxhdy`/`dxmdy`/`dxldy` fields read "Integer part of **change in x per change in y** of line connecting …" in `s13.16`, and `yh`/`ym`/`yl` are "`s11.2` format" *screen* y coordinates. **Citation corrected 2026-07-28:** this previously cited a "§Edge Coefficients", which does **not exist** in the page — the quoted wording and the fixed-point formats were right, the section name was not). `triangle_fill` evaluated the edge at each sub-scanline as `xh + (y − yh_base) · dxhdy` with `y = line·4 + sub` in **quarter-pixel** units, but never divided the contribution by 4. **Fix:** the three slopes are pre-shifted `>> 2` at decode (parallel-rdp `span_setup.comp:167`: `setup.dxhdy = raw >> 2`) | absolute — a fixed-point unit error, oracle-confirmed | **Closed.** The fix landed; `fill_tri_16` and `fill_tri_wide_16` conformance vectors now pass byte-for-byte against Angrylion. The self-asserted triangle unit tests that baked in the buggy staircase were corrected — their `DxMDy` changed from `0.25` to `1.0` (the value for which the staircase is the *correct* output), which the `fill_tri_wide_16` vector independently confirms against the oracle. Did not change the n64-systemtest count (no systemtest drives the render path); the oracle stays **93** *(as-at — see the note above this table)* | | R-1 | **RESOLVED** — see C-21. The failing instruction was `ADD.S $1, $29, $30`, not the `ADD.S $0` the assertion names; a correlated capture separated cause from visible effect by exactly the pipeline depth | — | absolute | Closed | | R-2 | **RESOLVED** — `BC1` implemented, and the compare forwarded to it (C-25) | — | absolute | Closed | | R-6 | **[HISTORICAL BASELINE — the "only NTSC is modelled" claim is SUPERSEDED by the PAL 50 Hz RESOLUTION in the disposition column; retained per the append-mostly rule.]** The VI scan cadence (T-31-004) *was* anchored to a **nominal 60 Hz field rate** (`VI_FIELD_HZ`) with only NTSC modelled — the per-half-line period was `MASTER_HZ / 60 / (VI_V_TOTAL + 1)`, and `VI_V_CURRENT` / the `VI_V_INTR` interrupt derive from that | The VI dot clock is off a separate crystal the N64brew wiki gives only *roughly* (*Video Interface* §Clocks: "roughly 12.3 megapixels/sec", ×4 ≈ 49 MHz VI clock; the exact NTSC value is not stated). Rather than fit an imprecise dot-clock frequency, the field rate is anchored to the standard NTSC 60 Hz and the half-line count taken from the software-programmed `VI_V_TOTAL` — so the cadence is correct to the field, and only the sub-field phase (which `H_TOTAL`/`H_TOTAL_LEAP` set exactly) is nominal. The interlace `VI_V_INTR` bit-0 quirk (§VI_V_INTR) is also not modelled | absolute — a clock-rate anchor, not a fitted per-ROM constant | **Open.** Correct to the field: `VI_V_CURRENT` advances and wraps at `VI_V_TOTAL + 1`, and the VI interrupt fires once per field at `VI_V_INTR` (pinned by the `vi` unit tests and a scheduler integration test). Deferred: the exact `H_TOTAL`/leap sub-field timing, PAL's 50 Hz field rate, and the interlace `V_INTR` quirk. To be validated against n64-systemtest's `timing`/VI groups when they are run. **PARTIALLY RESOLVED (2026-07-25) — PAL 50 Hz field rate.** `Vi::field_hz` now selects PAL **50 Hz** (`VI_FIELD_HZ_PAL`) when the field is PAL-length (`VI_V_TOTAL > 550`) and NTSC 60 Hz otherwise, so `ticks_per_halfline = MASTER_HZ / field_hz / (VI_V_TOTAL + 1)` scans PAL games at the right cadence — the same `ispal` split the scan-out geometry (R-5 slice 4a) already uses, so cadence and region agree — the `> 550` boundary is now the shared `VI_PAL_V_TOTAL_THRESHOLD` constant (one definition for both `Vi::field_hz` and `bus::scanout_scaled`). **Provenance:** 50 Hz is the documented PAL broadcast standard (N64brew *Video Interface*; counterpart to the anchored NTSC 60 Hz), not a fitted value; the `> 550` split is a wide-margin discriminator between NTSC's ~525-half-line and PAL's ~625-half-line fields (N64brew *Video Interface* §Clocks region field lengths), not a measured edge. Pinned by `a_pal_length_field_scans_at_50hz` (a PAL field advances one half-line in `MASTER_HZ/50/625 = 6000` ticks, distinct from the 60 Hz `5000`; mutation forcing always-60 fails it) and `the_pal_threshold_is_exactly_550` (V_TOTAL 550 = NTSC, 551 = PAL — pins the exact `>` boundary), with NTSC fields unaffected. **n64-systemtest impact: not measured** — the suite runs NTSC and has no VI field-timing group, so the PAL cadence is unreachable by it (the count is unchanged for that reason, not measured against it). **Still deferred:** the exact `H_TOTAL`/leap sub-field timing and the interlace/serrate `V_INTR` bit-0 quirk | -| R-10 | The colour combiner (T-33-002) models the common inputs (combined, texel0/1, primitive, shade, environment, one, zero, and the C-slot alpha taps); the **exotic inputs** — noise, LOD fraction / prim-LOD-fraction, the chroma-key centre/scale, and the convert (`K4`/`K5`) constants — are not modelled and read as **zero** | These inputs need the LOD pipeline (mip level fraction), the key/convert registers (`Set Key`/`Set Convert`), and a noise source, none of which exist yet; they appear in a small minority of combine modes. Reading them as zero is a bounded, documented gap, not a fabricated value | absolute — a coverage boundary, not a fitted constant | **Partially resolved (2026-07-25) — see the RESOLUTION below.** The `(A − B) * C + D` arithmetic (the `special_expand` asymmetric 9-bit fold, the `+0x80`-before-`>>8` rounding, D added unscaled) and the clamp are validated bit-for-bit against hand-computed values; the 16-field decode, the input mux, and the 2-cycle chaining are unit-tested. The remaining exotic inputs land with the LOD/key/noise state and are validated against the ParaLLEl-RDP conformance vectors (T-33-005). **RESOLUTION (2026-07-25) — the register-sourced exotic inputs are wired.** `PRIM_LOD_FRAC` (RGB mul-select 14, alpha mul-select 6, extracted from `Set Prim Color` word-0 low byte; `min_level`, bits 12:8, stays deferred — it is not stored, and lands with its LOD consumer rather than as unread state) and the `Set Convert` (`0x2C`) constants `K4` (RGB sub-B select 7) and `K5` (RGB mul-select 15, both raw 9-bit from `lo[17:9]`/`lo[8:0]`) now route through the combiner instead of reading zero. Validated byte-for-byte against Angrylion by three new non-vacuous conformance vectors — `tex_tri_primlodfrac_16` (`One * prim_lod_frac`, golden `0x8421` gray vs black if unwired), `tex_tri_convert_k45_16` (`(One − K4) * K5` with bit-8-clear K4/K5, golden `0x94a5` vs black if unwired), and `tex_tri_convert_kneg_16` (a **negative** `K4 = 0x1C0` = −64, golden `0x5295` gray — correct only if the raw 448 is `special_9bit`-expanded to −64; a raw-positive read clamps black) — plus two mutation-checked unit tests (`combine_cycle_routes_prim_lod_frac`, `combine_cycle_routes_convert_k4_k5`). The sign path is faithful to Angrylion, which likewise stores K4/K5 **raw 0..511** (`rdp_set_convert`) and sign-extends in the equation: sub-B via `special_9bit_exttable` (`combiner.c:481`, reproduced bit-for-bit by RustyN64's `special_expand`) and the mul via `SIGNF(c, 9)` (RustyN64's `sext9`). **n64-systemtest impact: none** — the suite has no RDP-combiner coverage, so its failing-assertion count is unchanged (90); R-10 is validated only by the Angrylion conformance vectors. **RESOLUTION (2026-07-25) — the chroma-key combiner inputs are wired.** `Set Key GB` (`0x2A`) and `Set Key R` (`0x2B`) now decode the per-channel key **centre** and **scale** (bit-layout ported from Angrylion `rdp_set_key_gb`/`rdp_set_key_r`: GB `lo` = `centre_g[31:24] scale_g[23:16] centre_b[15:8] scale_b[7:0]`, R `lo` = `width_r[31:16] centre_r[15:8] scale_r[7:0]`), and they route through the combiner as **KeyCentre** (RGB sub-B select 6) and **KeyScale** (RGB mul-select 6) instead of reading zero — matching Angrylion `combiner.c` cases 6. The key **width** is not stored: it drives only the deferred chroma-key alpha compare, not the combiner mux, so it lands with that consumer (the `min_level` precedent). Validated by two mutation-checked unit tests — `set_key_decodes_centre_and_scale_per_channel` (distinct per-channel values pin the decode field positions) and `combine_cycle_routes_chroma_key` (`(One − centre) * scale >> 8` with per-channel centre `[32,64,96]`/scale `[64,128,192]` → `[56,96,120]`; unwiring either input changes the result) — **and byte-for-byte against Angrylion** by the end-to-end conformance vector `tex_tri_chromakey_16` (centre `[0x20,0x40,0x60]`, scale `[0x40,0x80,0xC0]` → RGBA5551 `0x3b1f`, black if unwired), which exercises the decode + mux through the real RDP pipeline. **n64-systemtest impact: none** — no RDP-combiner coverage; count unchanged (90). **Still open** (genuinely need machinery that does not exist yet, read as zero): **noise** (RGB sub-A select 7 — needs a per-pixel noise source), the **derivative-computed `lod_frac`** (RGB mul-select 13 / alpha mul-select 0 — needs the LOD/mip pipeline, pairs with R-13's mip tile selection), and the **YUV convert `K0`–`K3`** coefficients (the `Set Convert` hi word, for the YUV texture path). **RESOLUTION (2026-07-26) — the chroma-key alpha compare (`key_en`) is wired.** `Set Other Modes` bit 40 (`hi >> 8 & 1`) decodes to `OtherModes.key_en`, and `Set Key GB`/`R` now also store the per-channel `key_width` (GB `hi[23:12]/[11:0]`, R `lo[31:16]`). When `key_en`, `Rdp::combine` takes the Angrylion `combiner_1cycle` key path (gated so the common path stays byte-identical — all prior 31 `.rvec` vectors unchanged): the RGB output is the sub-A **chromabypass** colour (clamped), and the pixel alpha is `chroma_key_min` over the **pre-`>>8` 17-bit** combined colour (`combine_channel_17bit` = `((A−B)*C + (D<<8) + 0x80) & 0x1ffff`, matching `color_combiner_equation`) and the key widths — per channel `SIGN(col,17)` folded (`-k`, or `-k+0x10` when the low nibble is 8), `+ (width<<4)`, `min`-of-3, clamp `[0,0xff]`. Validated byte-for-byte against Angrylion **end-to-end** by `tex_tri_chromakey_alpha_16`, which makes the key alpha **observable** via alpha-compare (bit 0) at a `Set Blend Color` threshold of `0x80`: the Shade triangle is written only where `chroma_key_min >= 0x80` (the combine yields exactly `0x80`, so it is drawn — a `chroma_key_min` off by −1 shifts it below the threshold and the triangle vanishes, mutation-verified; clearing `key_en` outputs the combined colour instead of Shade). The `chroma_key_min` fold is **additionally** unit-tested directly with hand-computed values including the bit-16-set (negative) branch (`chroma_key_min_folds_and_takes_the_minimum`). **n64-systemtest impact: none** (no RDP-combiner coverage; count 90). **RESOLUTION (2026-07-26) — the derivative `lod_frac` input is wired** (RGB mul-select 13 / alpha mul-select 0), computed by the 2-cycle LOD ported under R-13 and validated by `tex_tri_lodfrac_16`; see R-13 for the full disposition. **Still open under R-10:** noise (un-oracled — Angrylion fakes it) and the YUV `K0`–`K3` convert | -| R-13 | Triangle **texturing** (T-33-004 PR-B 2b) samples the tile per pixel via `decode_texture` + `interpolate_st` + `fetch_texel`, with **both** the non-perspective path and the **perspective divide** (the 64-entry `perspective.h` reciprocal LUT + normalisation shift + out-of-bounds saturation + `w <= 0` carry, gated on `persp_tex_en`) now implemented; the tile shift/clamp/mask, the 3-point bilinear, the mask-wrap seam, the 2-cycle `texel1` (tile+1), the primitive base tile, **`mid_texel`**, the 2-cycle **`lod_frac`**, and the LOD-driven **mip tile selection** are all resolved too (see the dated RESOLUTIONs below), leaving only the **1-cycle** LOD form | The non-perspective path and the sampler wiring are the tractable first step (the flat-coordinate case is scale-independent, so a textured triangle can be validated end-to-end without the divide); the perspective LUT is a precision-critical port best hand-verified in isolation, and the triangle coordinate wrap/clamp is a combinatorial surface for the conformance fuzz | absolute — real texture-path modelling gaps, oracle-confirmed | **RESOLVED (2026-07-23) — see the RESOLUTION at the end of this cell. Retained below is the (thrice-corrected) investigation trail. Open — a REAL divergence is pinned (after two mis-diagnoses, now settled by direct instrumentation).** The `tex_tri_16` conformance vector (committed **`#[ignore]`d**) is the first to drive `interpolate_st` against Angrylion, and its golden differs from RustyN64. Two earlier revisions of this entry were **wrong** and are retracted: (a) a `v >> 16`-vs-s10.5 coordinate claim, and (b) a "malformed vector / unconfigured tile / `SSS = 0`" claim — the latter came from reading the wrong vector's debug output (a global counter had captured an earlier *shade* triangle, not this textured one). Correctly instrumented, the vector is **well-formed**: at its own sample time tile 0 is configured (`size = 2`, `format = 0`), the S coordinate advances (`SSS = 0,1,2,3,4,5` across the drawn columns), and Angrylion **fetches texel 0 = `(255,0,0,255)` = red correctly**. The mismatch is therefore a **real** RDP behaviour RustyN64 does not yet model, from two effects seen in the reference: (1) the **1-cycle TEXEL0 pipeline** — Angrylion's `combiner_1cycle` swaps `texel0_color = texel1_color` (combiner.c) before the combine, the documented hardware quirk that a `TEXEL0` reference in 1-cycle mode is *pipelined*, so a texel0-passthrough does **not** emit the just-fetched texel; and (2) the **s10.5 texel-coordinate scale** — `SSS` spans only `0..5` in `s.5` units (< one texel), so Angrylion point-samples texel 0 across the whole triangle, whereas RustyN64's `interpolate_st` (`v >> 16`) advances one texel per pixel. RustyN64 models neither. The v2 `.rvec` **preload** plumbing is independently verified (an all-white texture renders white; Angrylion's 16-bit `tmem_formatting` loads `tmemidx0 = 0xF801`). **DEFINITIVE CORRECTION (2026-07-23, source-verified — the two "effects" above are BOTH retracted, the third mis-diagnosis of this entry).** (1) The **1-cycle TEXEL0 pipeline is NOT a net offset.** `combiner_1cycle` (`combiner.c:173`) does **not** swap texels; the swap `texel0_color = texel1_color` lives in `combiner_2cycle_cycle1` (`combiner.c:348`, 2-cycle only) and, as a *lookahead*, in `render_spans_1cycle_complete` (`rasterizer.c:336`) — but there it is net-zero: `texel1` is computed at pixel *j*'s next coordinate, then reused as pixel *j+1*'s `texel0`, so `texel0` at every pixel equals that pixel's OWN sampled texel. There is no offset to model. (2) The claim that **Angrylion "fetches red texel 0 correctly"** is **wrong**: the committed `tex_tri_16` golden is uniform `0x0001` (RGB 0 + coverage) across the whole triangle — Angrylion samples a **black** texel at *every* covered pixel, not the ramp. So the real divergence is that Angrylion's texture sample yields **zero** where RustyN64 fetches the loaded texel (`0xF801`). **The coordinate scale is RULED OUT** by a decisive probe (2026-07-23): a *constant*-coordinate variant (`dsdx = dsde = 0`, so `S = T = 0` at every pixel — sampling texel 0 = red everywhere) renders the **same** all-black `0x0001` triangle in Angrylion. If the coordinate were the divergence, a fixed `S = 0` would show red; it does not. So the fault is in the **texture load / tile / sampler configuration for the triangle 1-cycle path**, not the coordinate — Angrylion's triangle sampler returns black even for texel 0. (The copy-mode path samples the loaded texels fine, but it is a *different* sampler — a direct TMEM blit, not `texture_pipeline_cycle` — so it does not prove the triangle sampler config.) Next: isolate load-vs-sampler by driving a **COPY-mode** rectangle through the *identical* 8×1 texture + `Set Tile` + `Load Tile` (if that shows the ramp, the load is fine and the triangle tile/sampler config — `Set Tile` line/mask/format, `Set Tile Size`, and the `Set Other Modes` sample/texture bits — is the fault); then build a **correct** textured-triangle reference whose Angrylion output is verified to show the ramp, and only then compare RustyN64. Do NOT implement against the current `tex_tri_16`, whose oracle output is a degenerate all-black frame. **RECONCILIATION (later same-day, source-verified — partially walks back the "coordinate ruled out" above):** the `0x0001` black is *not* caused by the coordinate, that stands — but there ARE **two separate** issues, not one. (A) A **real RustyN64 coordinate gap IS confirmed**: `texture_pipeline_cycle` (`tex.c:182`) takes `sfrac = sss1 & 0x1f` and hands `sss1` *with its 5 fractional bits* to `fetch_texel_quadro` (`tex.c:246`), which shifts `>> 5` for the integer texel — so the RDP texel coordinate is **s.5** and RustyN64's `interpolate_st` (`v >> 16` used directly as the texel index, no `>> 5`) advances 32× too fast. The original "s10.5" note was right about this; only the *tex_tri black* is unrelated to it. (B) The **black itself** is that Angrylion's texel-0 fetch returns zero even at `SSS = 0` (texel 0) with the combiner (`add_rgb1 = 1 = texel0`, `combiner.c:540`), tile (0, loaded), and render path (`render_spans_1cycle_notexel1`) all verified to match — so the fault is inside `fetch_texel`/`tmem.c` addressing (the load-vs-sample TMEM layout for the 8×1 tile), still open. Fix order: root-cause (B) against `fetch_texel_quadro`/`tcshift`/`tcmask` or a hand-built known-good textured vector, then implement (A)'s `>> 5` and validate. **RESOLUTION (2026-07-23):** both landed. (B) The all-black frame was a **vector bug, not a RustyN64 gap** — `Set Other Modes` had `bi_lerp0 = 0` (bit 11), which selects the RDP's **YUV colour-convert** texture path (`tex.c` `texture_pipeline_cycle`, the `!bilerp` branch: `TEX->r = t3.b + ((k*_tf·t3.g + 0x80) >> 8)`); with the convert coefficients unset that computes an RGBA texel's output from its chroma (`t3.b = 0` for red) → black. Setting `bi_lerp0 = 1` selects the normal RGBA fetch. (A) RustyN64's `interpolate_st` returned `v >> 16` as the texel index directly; the RDP coordinate is **s.5**, so the index is `(v >> 16) >> 5` — added the `>> 5`. With both, the corrected `tex_tri_16` (advancing S, which the `>> 5` collapses to a solid-red triangle) and a new constant-coordinate `tex_tri_fixed_16` **both pass byte-for-byte vs Angrylion** and `tex_tri_16` is un-ignored. One self-asserted unit test (`shaded_and_textured_triangle_reads_texture_past_shade`) was corrected to the s.5 convention (its `S = 1` became `S = 32`). **TILE COORDINATE TRANSFORM RESOLVED (2026-07-25).** The triangle sampler now applies the tile **shift → tile-origin subtraction → clamp → mask/mirror** to the raw `s10.5` coordinate before `fetch_texel` (`sample_coord`, wired into `combined_color`), a bit-exact port of the ParaLLEl-RDP sampler order (`tcshift_cycle` → `TRELATIVE(SL<<3)` → `tcclamp_cycle_light` → `tcmask`): clamp is active when `clamp_s \|\| mask_s == 0`, the over-`SH` test is against the **raw absolute** `SH` (pre-subtraction) and substitutes the **relative** width `(SH>>2)−(SL>>2)`, and it sits *before* the mask (masking first corrupts the over-max/negative detection). Validated byte-for-byte against Angrylion by two new vectors — `tex_tri_clamp_16` (a 4-texel tile, `clamp_s`, `S` running past `SH` → `R,G,B,W,W,W`) and `tex_tri_wrap_16` (`mask_s = 2` → `R,G,B,W,R,G`) — plus a mutation-checked `sample_coord` unit test (shift/subtract/clamp/mask/mirror/negative-clamp). `interpolate_st` now returns the pre-`>>5` `s10.5` coordinate so the shift and tile-size clamp operate on the true value; the self-asserted `shaded_and_textured_triangle_reads_texture_past_shade` was given a valid `SH`/`TH` (the clamp needs a tile size). **BILINEAR RESOLVED (2026-07-25).** The N64's characteristic **3-point (triangular)** filter is now modelled (`sample_type` bit 45 decoded; `bilinear_3point` + `sample_axis`, wired into `sample_texel`): the base coordinate runs shift → subtract → clamp (zeroing the sub-texel fraction on clamp, per `tcclamp_cycle`) → mask, then four texels `(s,t)/(s+1,t)/(s,t+1)/(s+1,t+1)` are blended by `upper = (sfrac+tfrac) & 0x20` — the lower-left triangle uses `t0,t1,t2`, the upper-right `t3,t2,t1` with inverted fractions, each channel a `+0x10 >> 5` round (a faithful port of ParaLLEl-RDP `texture_pipeline_cycle`). Validated byte-for-byte against Angrylion by `tex_tri_bilinear_16` (an 8×8 gradient sampled at 0.5 texel/pixel in **both** axes, so R interpolates `0,2,4,6,8,10` where point sampling would step `0,0,4,4,8,8`, and pixels hit **both** triangle branches), plus mutation-checked `bilinear_3point` (both triangles, hand-computed) and `sample_axis` (fraction capture + zero-on-clamp) unit tests. **MASK-WRAP SEAM RESOLVED (2026-07-25).** The bilinear neighbour is no longer a hardcoded `+1`: `mask_coupled` ports `tcmask_coupled`'s `sdiff`/`tdiff` — `+1` normally, `0` at a wrap seam (the "duplicate the last texel" quirk), `-base` at a mirror-off period end (neighbour wraps to 0), `-1` in a mirrored half; the neighbour is `base + diff`, *not* re-masked (`is_t` uses the T `-(base & 0xff)`). Validated byte-for-byte against Angrylion by `tex_tri_bilinear_wrap_16` (a 2-texel `mask_s = 1` tile whose `S = 1.5` seam column blends green+red — the wrapped texel 0 — not green+black; non-vacuous because the wrapped texel differs from the pre-fix unloaded read) plus a mutation-checked `mask_coupled` unit test. **2-CYCLE `texel1` RESOLVED (2026-07-25).** 2-cycle mode now samples a **second texel from `tile+1`** at the same coordinate (`combined_color`), and `combine` swaps `texel0`/`texel1` before cycle 1 (`combiner_2cycle_cycle1`), so cycle 1's `TEXEL0` reads `tile+1`. Validated byte-for-byte against Angrylion by `tex_tri_2cycle_16` (two 1-texel tiles red/green, both cycles output `TEXEL0` → the pixel is green because the swap brings `tile+1` into cycle 1; red without the swap) plus a mutation-checked `combine_two_cycle_swaps_texels` unit test. **PRIMITIVE BASE TILE RESOLVED (2026-07-26).** The sampler no longer hardwires tiles 0/1: `triangle_fill` decodes the command's base tile (`(hi >> 16) & 7` = bits 50:48, `ewdata[0]` in Angrylion `rasterizer.c:1887`) and threads it through `combined_color`/`depth_span`, which sample `tiles[base_tile]` (and `tiles[(base_tile + 1) & 7]` for the 2-cycle `texel1`). Validated byte-for-byte against Angrylion by `tex_tri_base_tile_16`: the ramp is loaded into **tile 3** at a non-zero TMEM word and the triangle names tile 3, so its golden is identical to `tex_tri_16` (same picture, different tile) while the pre-fix `tiles[0]` read samples the unloaded low TMEM and renders black (mutation-witnessed: got `[00,01]` vs golden `[F8,01]`). The 2-cycle `base + 1` path is guarded by `tex_tri_2cycle_16`, which still passes. **MID-TEXEL RESOLVED (2026-07-26).** `Set Other Modes` bit 44 (`mid_texel`) now feeds `bilinear_3point`: when set and the bilinear sample lands exactly on the texel centre (`sfrac == tfrac == 0x10`), the four neighbours are averaged (`t3 + ((((t1+t2)<<6) − (t3<<7) + ((!t3+t0)<<6) + 0xc0) >> 8)`, Angrylion `tex.c` `center`/`centerrg` case) instead of the 3-point triangle pick — which, unlike the triangle pick, uses all four texels including `t0`. Validated byte-for-byte against Angrylion by `tex_tri_mid_texel_16`: the bilinear setup of `tex_tri_bilinear_16` over a **non-planar checkerboard** (a smooth gradient is planar, so its centre average equals its 3-point pick and bit 44 would be invisible — a vacuous golden), whose golden carries the midpoint value `0x8001` (R=16) that a 3-point pick of the checkerboard extremes never produces. Mutation-witnessed: disabling the centre branch turns pixel (3,3) from the golden `[80,01]` to `[F8,01]` (the 3-point extreme). Plus a mutation-checked `bilinear_3point` unit test (`t0` off the gradient plane so the centre average `98` differs from the 3-point `48`). **n64-systemtest impact: none** — the suite has no RDP texture-path coverage (nothing renders a textured triangle during a systemtest run), so its failing-assertion count is unchanged (90); mid-texel is validated only by the Angrylion conformance vector. **LOD FRACTION RESOLVED (2026-07-26).** The derivative-computed `lod_frac` is now modelled for **2-cycle** mode — a port of Angrylion `tclod_2cycle` + `lodfrac_lodtile_signals` (`tcoord.c`) down to their `lf` output. The texture setup gained the per-**Y** derivative (`DsDy`/`DtDy`/`DwDy`, block words 5/7, previously unparsed — the LOD's second delta pair uses the true vertical gradient, *not* the major-edge `de` the scanline walk steps by); `Set Prim Color`'s `min_level` (bits 12:8) and the triangle command's `level[2:0]` (`max_level`, bits 53:51) are now stored; and `Set Other Modes` `tex_lod_en`/`sharpen_tex_en`/`detail_tex_en` (bits 48/49/50) are decoded. The LOD is the larger of the `+dsdx` and `+dsdy` coordinate deltas (each through the same perspective divide as the pixel's own coordinate, `lod_delta` = `tclod_4x17_to_15`'s magnitude fold), then `lod_frac_of` maps it to the raw 9-bit fraction — sign-extended downstream by `sext9`, matching how `K4`/`K5` are stored. It reaches the combiner through the previously-dead **RGB mul-select 13 / alpha mul-select 0** (closing that half of R-10). Computation is gated on Angrylion's `dolod` (`tex_lod_en` or the combiner actually selecting `LODFrac`), so the common path is unchanged and pays nothing. Validated byte-for-byte against Angrylion by `tex_tri_lodfrac_16`: a 2-cycle textured triangle with `dx.S = 48` but `dy.S = 112` and `de = 0`, so the LOD settles at **112** and — with `level = 2`, which keeps it out of the "distant" saturation — the fraction is the real weight `((112 << 3) >> 1) & 0xff = 0xc0`, emitted as the pixel colour (golden `0xc631`). Three mutation checks: unwiring select 13 renders black; reading `de` instead of `dy` for the Y tap gives LOD 48 → `0x80`; dropping the `l_tile` shift gives `0x80`. (The first draft of this vector left `dy = 0`, which made the `de`-vs-`dy` mutation **pass** — the vector was rebuilt with `dy` dominating so the tap is genuinely pinned.) Plus hand-computed `lod_delta` and `lod_frac_of` unit tests covering the fold, the max, the saturation marker, and each `lf` branch (in-range / distant / lodclamp / magnify / sharpen / `min_level` floor). **n64-systemtest impact: none** (no RDP texture-path coverage; count stays 90). Still open: the **1-cycle** LOD form (`tclod_1cycle_current_simple` compares the `x+1` and `x+2` taps and needs the span-edge signals `endspan`/`longspan`/`midspan`/`validline` the rasteriser does not model — deferred rather than approximated with the 2-cycle formula, and it reads zero meanwhile) **MIP TILE SELECTION RESOLVED (2026-07-28).** With `tex_lod_en` (bit 48) the 2-cycle sampler now reads the mip pair the LOD selects instead of `base`/`base+1` — a port of the tile-selection tail of Angrylion `tclod_2cycle`, via `lod_mip_tiles`: a *distant* LOD pins the level to `max_level`; otherwise it is `l_tile`; the pair straddles the mip boundary (`base+level`, `base+level+1`) and collapses to one tile where there is nothing to blend toward (distant, or magnifying without `sharpen_tex_en`); `detail_tex_en` shifts both one level finer; every index wraps mod 8. `lod_frac_of` was generalised to `lod_signals`, returning all four Angrylion outputs (`frac`/`l_tile`/`magnify`/`distant`) rather than discarding three. Validated byte-for-byte against Angrylion by `tex_tri_mip_tile_16`: the LOD-112/`level = 2` setup gives `l_tile = 1`, so from base tile 0 the pair is (1, 2) and — with three 1-texel tiles holding red/green/blue and a `TEXEL0` passthrough — the pixel is **blue** (tile 2, via the cycle-1 swap); disabling the selection samples the un-selected pair and renders green, mutation-verified. Plus a hand-computed `lod_mip_tiles` unit test covering the straddle, the distant pin-and-collapse, plain-magnify collapse, the `sharpen` exception, the `detail` shift, and the mod-8 wrap. **A vector-authoring trap worth recording:** the first draft set only `bi_lerp0` (bit 11) and not **`bi_lerp1` (bit 10)**, so cycle 1's `texture_pipeline_cycle` took the YUV colour-convert path and Angrylion rendered **white** — the same class of vector bug as the original `tex_tri_16` `bi_lerp0` mistake. A 2-cycle textured vector must set both. **n64-systemtest impact: none** (no RDP texture-path coverage; count stays 90). Still open under R-13: only the **1-cycle** LOD form | +| R-10 | Colour-combiner **exotic inputs** — noise, LOD fraction, chroma key and YUV convert — beyond the common input set. | They need the LOD pipeline, the `Set Key`/`Set Convert` registers and a noise source, none of which existed when the combiner landed. | absolute — a coverage boundary, not a fitted constant | **Partially resolved.** Chroma key (`key_en`) and the `Set Key` combiner inputs are implemented and oracle-pinned; noise is deliberately **not** attempted, because Angrylion fakes it with a shadertoy PRNG rather than modelling hardware. **Full record: [R-10](residuals/R-10.md).** | +| R-13 | Triangle **texturing** — the perspective divide, bilinear sampling, and LOD/mip tile selection. | The non-perspective path and the sampler wiring were the tractable first step; perspective and LOD needed the fuller pipeline. | absolute — a coverage boundary, not a fitted constant | **Partially resolved.** Clamp/mask/mirror/shift, the 3-point filter, the mid-texel bilinear and the primitive base tile are implemented and oracle-pinned; `mirror_s` is verified byte-for-byte against Angrylion. LOD/mip selection under a live stream remains open. **Full record: [R-13](residuals/R-13.md).** | | R-12 | The Z-buffer machinery — the depth **codec** and **`depth_test`** with the depth-source commands (**PR-A**), the **Z-buffer RDRAM read/write** + **hidden bits** (**PR-B part 1**), and the **per-pixel depth test + Z-write** in the triangle rasteriser (**PR-B part 2a**: z-suffix decode, `interpolate_z`, `depth_span`) — is in place; the **combiner→blender colour routing** (part 2b, the colour is still the FILL register) and the **coverage accumulator** at edges (part 2c) are not yet wired, and the `dz` derivation is a first-cut integer gradient | These land — and are tested — ahead of the pipeline integration, which is the larger, riskier surface (the flat-fill→per-pixel rewrite that also closes R-9). Splitting keeps each PR reviewable (the project's split-large-tickets rule). The hidden-bit RAM is modelled accurately (additive default-no-op `RdramBus` methods + a lazily-allocated Bus store) rather than approximated, so the exact `dz` precision the conformance gate needs is preserved | absolute — a coverage boundary, not a fitted constant | **Open.** The codec is validated by boundary values + a `z_compress ∘ z_decompress` round-trip; `depth_test` by observable occluding-vs-occluded pairs per Z mode; the storage by a Bus hidden-bit round-trip and a full-`dz` Z-buffer round-trip (nine `rdp` + one `core` unit tests). `depth_test`/`zbuffer_*` have **no runtime caller** yet, so the oracle stays **93** *(as-at — see the note above this table)*. The coverage and routing land in **PR-B part 2** and are validated against the ParaLLEl-RDP conformance vectors (T-33-005) | | R-11 | The blender (T-33-003) implements the divide-free `(P * a0 + M * (a1 + 1)) >> 5` with the `P/A/M/B` input muxes, both cycles, and `force_blend`; the **anti-aliased-edge divider LUT** (`uBlenderDividerLUT` — the coverage-weighted divide the RDP uses on partially-covered edge pixels), the **memory-alpha interpenetrating-Z blend-shift** path, **alpha-compare**, **dither**, the **`color_on_cvg`** early-return, and the **coverage write-back** (`cvg_dest`) are decoded but unused | These paths need the framebuffer read (`image_read_en` memory colour), the coverage accumulator, and the Z buffer — none of which reach the blender until the triangle pipeline routes combiner→blender per pixel (T-33-004). The no-divide form is the one every non-edge pixel uses, so it is the honest first target; emitting nothing for the deferred paths (rather than a fabricated divide) keeps the gap falsifiable | absolute — a coverage boundary, not a fitted constant | **Open.** The no-divide equation (the `>> 5` fold and the `+ 1` on the `M` term), the `Set Other Modes` (0x2F) field decode, the `P/A/M/B` muxes, and the 2-cycle forward chain are validated bit-for-bit against hand-computed values (four `rdp` unit tests). `blend` now **has a runtime caller** — `depth_span` (T-33-004 PR-B 2b-blend) reads the framebuffer pixel and routes the combiner colour through `blend` when the depth test enables blending, gated on `force_blend` (the reference's `!blend_en` fast-path keeps opaque pixels on the combiner colour). A translucent-triangle integration test proves a 50/50 blend of the combiner colour over a pre-filled background. **Dither is now present** (T-33-004 2c): the ordered RGB dither (magic/bayer matrix) is applied to the combined colour on both the no-Z and depth pixel paths, validated byte-for-byte against Angrylion by `dither_tri_32`. **Alpha-compare is now present on BOTH the no-Z and depth paths** (`Set Other Modes` bit 0): `alpha_compare_passes` gates the pixel write on `combiner_alpha >= Set-Blend-Color alpha`, evaluated before coverage overwrites the alpha byte — validated byte-for-byte against Angrylion by `alpha_compare_16` (no-Z) and `alpha_compare_z_16` (a z-suffixed triangle with `z_update` on / `z_compare` off) plus a boundary unit test. On the depth path the gate sits after the depth test and `continue`s before both the colour write and the z-write, which is observably equivalent to the RDP's pre-depth ordering because the compare is depth-independent (a pixel is written and its depth stored only when both depth and alpha pass). Still deferred here: the dithered-threshold variant (`dither_alpha_en`); the AA-edge divider LUT, the interpenetrating-Z blend-shift, `color_on_cvg`, and coverage write-back — these need the sub-pixel coverage accumulator (slice 2c). The oracle stays **93** (no systemtest drives the render path) *(as-at — see the note above this table)*. The deferred paths are validated against the ParaLLEl-RDP conformance vectors (T-33-005) | | R-9 | The triangle rasteriser now interpolates **depth** (T-33-004 PR-B 2a), **shade** (2b), and **texture** (`S/T/W` → `fetch_texel`, 2b-texture) per pixel, and routes the combiner colour through the **memory-read blender** when the depth test enables blending (2b-blend, gated on `force_blend`) — depth-tested, Gouraud-shaded, textured, and translucent triangles render; what remains is the bit-exact **sub-pixel coverage** (ParaLLEl-RDP's `quantize_x` sticky-bit edge rounding and the `do_offset` last-subpixel latch) and the coverage-driven AA blend, so each edge is still reduced to whole pixels (`>> 16`), and the `dz` derivation is a first-cut gradient | Landing the edge-walk, then the depth and shade interpolators (each hand-verified), then texture and the sub-pixel coverage is the tractable order; the coverage rule and the full combiner→blender→memory surface are a combinatorial space best pinned by the conformance fuzz (T-33-005) | absolute — a coverage boundary, not a fitted constant | **Open.** The flat fill, the depth test (occluding-triangle pairs), the shade interpolation (hand-computed base colour + a combiner-routed shaded triangle), the texture sample, and the memory-read blender (a 50/50 translucent triangle over a pre-filled background) are each unit/integration-tested. The sub-pixel coverage **primitives** — `compute_coverage` (the 4×2 diamond-sample 8-bit mask) and `quantize_x` (the `s.16`→`s.3` sticky-bit snap) — are now a bit-exact port of parallel-rdp `coverage.h`/`span_setup.comp`, pinned by hand-computed unit tests derived from the oracle's arithmetic (full/partial/empty masks, the sticky bit). The primitives are now **wired into the 1-/2-cycle rasteriser**: the edge-walk builds per-Y-subpixel `s.3` edges, `pixel_coverage` gates each pixel (AA-off top-left-sample rule) and stores the coverage count in the pixel alpha (`(count − 1) & 7`, the `cvg_dest` clamp write-back). FILL/COPY mode keeps the whole-pixel span (correct — FILL renders "without subpixel accuracy"). This is validated against Angrylion: `fill_tri_frac_16` (FILL rounds fractional edges to whole pixels) and `shade_tri_frac_16` (a 1-cycle shaded triangle excluding a fractional-edge column and leaving a partially-covered column at reduced coverage) pass byte-for-byte. The **depth path** applies the same coverage (`shade_depth_tri_frac_16` — a z-suffixed fractional triangle — renders identically to `shade_tri_frac_16`, byte-exact vs Angrylion). **Ordered RGB dither is now wired** (T-33-004 2c): the combined RGB is dithered per pixel by the magic/bayer 4×4 matrix before write-back (`apply_rgb_dither`, a bit-exact port of Angrylion `dither.c` `rgb_dither`), validated byte-for-byte by `dither_tri_32` (the RDP default magic dither over a `0x112233` shade). **Alpha-compare and `cvg_dest = full` are now wired**: `pixel_coverage` stores full coverage (7) when `cvg_dest = 2` (validated by `cvg_dest_full_16` — a fractional triangle whose partial edge column stores `0xf801` instead of the clamp `0xf800`), and the alpha-compare write gate is on both pixel paths (R-11). Remaining slice-2c residual: the coverage-weighted **interpenetration Z** path, the **AA-edge blend**, and the **wrap/save `cvg_dest`** modes (these need the memory-read coverage accumulator) are not wired — each to be pinned by further conformance vectors | | R-8 | Copy-mode `Texture Rectangle` (T-32-004) is wired for a **16-bit tile → 16-bit colour image** (the first-picture path), including the **4-pixels-per-cycle sub-texel selection** under non-1:1 `DsDx`; `Texture Rectangle Flip` (0x25), the 8/32-bit and TLUT copy paths, and the copy alpha-compare are not modelled — an unsupported configuration draws nothing | The full copy pipeline (per-format `dx_shift`/`s_offset` 64-bit-group fetch, the 8-bit high-word replication quirk, the RGBA5551 alpha-on-LSB test) is a combinatorial surface best pinned by the bit-exact fuzz rather than by hand. The 16-bit 1:1 path is the one a first textured frame needs, and its horizontal step is scaled by `>> (5 + dx_shift)` so a canonical `DsDx = 4.0` advances one texel per pixel | absolute — a coverage boundary, not a fitted constant | **Open — but the 16-bit 1:1 copy is now ORACLE-VALIDATED.** Beyond the internal **round-trip identity** test (`Load Tile` loads a 4×2 texture and `Texture Rectangle` blits it back byte-for-byte; load and fetch share the odd-row swap) and the `wrap_coord` unit test, the `tex_rect_copy_16` conformance vector (T-33-005) now drives the full command sequence — Set Texture Image → Set Tile → Set Tile Size → Load Tile → COPY-mode Texture Rectangle — through **Angrylion** and RustyN64 matches the golden byte-for-byte. This is the **first texture path checked against the oracle** (copy mode bypasses the combiner and the 1-cycle texel pipeline, so it is clean where the `tex_tri_16` triangle path is not — R-13). **The 4-pixels-per-cycle sub-texel selection under non-1:1 `DsDx` is now modelled and oracle-validated too:** COPY mode reads a 64-bit TMEM word (4 consecutive texels) per cycle, so the base texel is evaluated at each cycle's first column (advancing `DsDx × 4` texels/cycle) and the within-cycle offset is a direct `+0..3` TMEM increment, **not** a per-pixel step. The `tex_rect_mag_16` vector (a 2× magnify, `DsDx = 2.0`) reads texels `0,1,2,3,2,3,4,5` and RustyN64 matches Angrylion byte-for-byte; the 1:1 case (`DsDx = 4.0`) is the special case `s = col` and the round-trip + `tex_rect_copy_16`/`_offset_16`/`_8x8_16` vectors still pass. **n64-systemtest impact: not measured** — the copy path has no runtime driver in the suite (nothing calls `texture_rectangle` during a systemtest run), so it is unreachable by n64-systemtest and the oracle count stays **93** *(as-at — see the note above this table)*. Still deferred (each an unsupported config that draws nothing): Flip 0x25, 8/32-bit, TLUT copy, and copy alpha-compare — validated against further conformance vectors as they land | | R-7 | The TMEM loads (T-32-002) cover **8/16/32-bit** texels for `Load Tile` and **8/16-bit** for `Load Block`. **There is no 4-bit texel *load*** — a 4-bit *texture-image* load is invalid on hardware (Angrylion sets `rdp_pipeline_crashed` and bails, `tex.c:526-533`); 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. That canonical path already works. Genuinely deferred: the **32-bit split** path of `Load Block`, and a *direct* 4-bit LOAD tile paired with an 8-bit texture image (the `ti_size`-vs-`tile.size` load granularity — Angrylion keys the copy stride off `ti_size` and the destination short-index off `tile.size` via `sshorts = s >> 2`) | The earlier "4-bit loading needs nibble addressing" framing was a **misconception**: 4-bit texels are never DMA'd as nibbles — the load is a raw byte stream sized by the texture image, and the nibble semantics live only in `fetch_texel` (already implemented for I4/IA4/CI4). RustyN64's bail on a 4-bit texture image thus *matches* the hardware's invalid-load behaviour. Writing nothing for the two genuinely-deferred cases keeps them falsifiable rather than emitting fabricated texels | absolute — a coverage boundary, not a fitted constant | **The canonical 4-bit path is now ORACLE-VALIDATED (2026-07-25).** `tex_tri_i4_16` (T-33-005) drives the full hardware idiom — 8 I4 texels packed two-per-byte, an **8-bit** `Load Tile`, then a **4-bit** (format I, size 0) render tile sampled across the triangle — and RustyN64's existing 8-bit load + 4-bit `fetch_texel` reproduce **Angrylion byte-for-byte**. Non-vacuous by construction: the eight intensities are DESCENDING and non-zero (texel 0 = white `0xFFFF`, not black), so an all-zero (no-op) load would render black and fail. Since the load is format-agnostic (a raw byte copy), this validates the load mechanism for **all** 4-bit formats; the I4/IA4/CI4 decoders are separately unit-tested. The two remaining gaps above are still `Open` (no runtime driver in n64-systemtest, so they do not change the suite-wide failing count — `docs/STATUS.md` is authoritative for it), to be pinned by further conformance vectors. The supported sizes remain byte-exact against hand-computed expectations, including the odd-row 32-bit-word swap, the 32-bit `Load Tile` split, and the `Load Block` dxt line-parity swap (five `rdp` unit tests) | -| R-5 | **[HISTORICAL BASELINE — SUPERSEDED by the dated RESOLUTIONs in the disposition column, which are the current normative answer. Retained per the immutable-reference rule.]** VI scan-out (T-31-004) *was* a **1:1 copy** — `VI_X_SCALE`/`VI_Y_SCALE` resampling and the AA / divot / de-dither post-filters were not applied, and the height was derived directly from `VI_V_VIDEO`'s active half-lines rather than from the scale-accumulated framebuffer walk (this is the live `Bus::scanout`; the accurate `Bus::scanout_scaled` now supersedes it — see the RESOLUTIONs) | The framebuffer→RGBA8 pixel conversion is exact and cited: the pixel *format* is selected by `VI_CTRL.TYPE[1:0]` (N64brew *Video Interface* §VI_CTRL — 2 = RGBA5551, 3 = RGBA8888), the RGBA5551 bit layout (R[15:11] G[10:6] B[5:1] A[0]) is the N64 16-bit colour format (N64brew *Reality Display Processor/Commands* §Set Color Image, texture/format enum; *Video DAC*), and the 5→8-bit widening by high-bit replication is the standard N64 convention (the value the VI DAC emits). What is **deferred**: the geometric resampling — the VI accumulates a sub-pixel step of `VI_X_SCALE`/`VI_Y_SCALE` per pixel/line (N64brew *Video Interface* §VI_X_SCALE, §VI_Y_SCALE) — and the analog post-filters `AA_MODE`/`DIVOT_ENABLE`/de-dither (§VI_CTRL), which only matter once scaled or anti-aliased content is scanned | absolute — a resampling/filter geometry choice, not a timing interval | **Open.** Byte-exact for a 1:1, unfiltered scan of a framebuffer whose width matches `VI_WIDTH` — which is what the FILL pipeline produces and the T-31-004 unit tests pin. Scaling and the post-filters **will be** validated bit-for-bit against Angrylion via the ParaLLEl-RDP fuzz suite / VI golden frames (Sprint 3), and superseded here if they diverge — this entry stays open until then. **n64-systemtest impact: not measured** — `Bus::scanout` has no runtime driver (nothing in the run loop calls it), so it is unreachable by the suite and cannot change the count, which stands at 90 (`docs/STATUS.md` is authoritative). **PARTIALLY RESOLVED (2026-07-25) — Slice 1, the scale accumulator + geometry.** A VI oracle now exists (earlier "no VI oracle" assumption is retired): the Angrylion driver's `vdac_write` captures `vi_process_full`'s output after `n64video_update_screen`, emitting `.vivec` goldens (`crates/rustyn64-test-harness/vectors-gen/driver.c`, format = 15-u32 header + logical source + golden RGBA8). A new `Bus::scanout_scaled` reproduces the hardware geometry bit-for-bit: the **2.10 fixed-point X/Y accumulator** (`line_x = x_offs >> 10`, source index `stride*srcY + srcX`), the **NTSC 108-px horizontal overscan** (`h_start -= 108`) with the left/top clamps folding the crop into the accumulator start, the `minhpass=8`/`maxhpass=hres-7` overscan crop, the `PRESCALE 640×625` clamp, and the **truncating RGBA5551→8** conversion the VI actually uses (`(px>>8)&0xF8` — *not* `expand5`'s high-bit replication; corrects the "5→8 by replication" claim above for the VI path specifically). Validated by `vi_scale_1x_16` (1:1, the overscan makes output column 0 sample source column 8) and `vi_scale_down2x_16` (2× downscale, the accumulator steps two source pixels) — both byte-for-byte (RGB) vs Angrylion — plus mutation-checked unit tests (`scanout_scaled_geometry_and_truncating_convert`, and the conformance mutation dropping the overscan offset goes red). This slice is `aa_mode = REPLICATE` (nearest, no lerp). **Slice 2 (2026-07-25) — the 5-bit bilinear lerp.** `aa_mode != REPLICATE` with a non-zero fraction now bilinearly resamples: four texels `(sx,sy)/(sx+1,sy)/(sx,sy+1)/(sx+1,sy+1)`, vertical-lerped per column by `yfrac` then horizontal-lerped by `xfrac`, each channel `a + (((b-a)*frac + 16) >> 5)` (`vi_lerp3`, a port of Angrylion `vi_vl_lerp`; `frac` = fraction bits `[9:5]`). Validated byte-for-byte vs Angrylion by `vi_scale_bilinear_16` (2× upscale, `aa_mode = RESAMP_ONLY`, `xfrac`/`yfrac` alternating 0/0x10 — the clean 50 % blend) and `vi_scale_bilinear_odd_16` (scale `0x240`, fractions ≢ 0 mod 4 so the `+16` rounding actually flips the result — dropping the `+16` fails *this* vector while the 0x200 one stays green, because its products are all multiples of 32), plus a mutation-checked `vi_lerp3` unit test. Bilinear applies to the 16-bit path in this slice; **32-bit source bilinear followed in slice 4b** (`vi_scale_bilinear_32`, oracle-validated), so bilinear now applies to both source formats. **Slice 3 (2026-07-25) — the gamma curve.** `gamma_enable` (VI_CTRL bit 3), with `gamma_dither` (bit 2) clear, applies the sqrt gamma table to the final RGB: `gamma(v) = sqrt(v << 6) << 1` (`vi_gamma` / `vi_integer_sqrt`, a port of Angrylion `vi_gamma_init`). Validated byte-for-byte vs Angrylion by `vi_gamma_1x_16` (`VI_STATUS = 0x030A`, nearest + gamma; `gamma(0x40) = 0x80`, non-vacuous vs the raw sample) plus a mutation-checked `vi_gamma_curve` unit test. The **dithered** gamma variants (bit 2 set, cases 1 and 3) are noise-based and stay deferred. **Slice 4a (2026-07-25) — the PAL active-span geometry.** `v_sync > 550` selects the PAL branch — the horizontal overscan is 128 px not 108 and `vstartoffset` is 44 not 34. **Provenance:** these are Angrylion source constants, not fitted values — `angrylion-rdp-plus/src/core/n64video/vi.c:688` (`h_start -= ispal ? 128 : 108`, `ispal = v_sync > V_SYNC_NTSC + 25 = 550`) and `vi.c:700` (`vstartoffset = ispal ? 44 : 34`); the `vi_pal_geometry_16` golden is generated *by that Angrylion source* through `crates/rustyn64-test-harness/vectors-gen/driver.c` (`make ANGRYLION_CORE=… driver && ./driver …/tests/vectors`), so the values are traceable to the oracle's code, not only to the fixture. That code was present in `scanout_scaled` since slice 1 but unverified; it is now pinned by `vi_pal_geometry_16` (`v_sync = 625`, `h_start = 115` so PAL's −128 clamps to sample source column 13 while a mis-applied NTSC −108 would sample column 8) byte-for-byte vs Angrylion, and a mutation forcing the PAL overscan to 108 fails it. **Slice 4b (2026-07-25) — 32-bit RGBA8888 source with bilinear.** The `.vivec` harness now carries a 32-bit source framebuffer (driver `bpp` field + `rdram_put_fb32`), and `scanout_scaled` dispatches the per-pixel fetch on the format (`vi_fetch16`/`vi_fetch32`) so the bilinear + gamma path runs for both — the 16- and 32-bit paths are now one unified loop. Validated by `vi_scale_bilinear_32` (`VI_STATUS = 0x0203`, 2× upscale) byte-for-byte vs Angrylion; a mutation breaking the 32-bit stride multiply fails it. **Slice 4c (2026-07-25) — the de-dither restore filter.** `aa_mode` 0/1 now reads the real per-pixel coverage (32-bit: alpha bits 7:5 = `(px>>5)&7`), and a fully-covered pixel (`cvg == 7`) with `dither_filter_enable` (VI_CTRL bit 16) takes the **de-dither** `restore_filter32`: over the 3×3-minus-centre 8 taps, each channel is nudged ±1 toward the neighbour's top-5-bit value (a port of Angrylion `restore.c`; the `vi_restore_table` reduces to `+1` if centre5 < neighbour5, `-1` if greater). Validated by `vi_dedither_32` (`VI_STATUS = 0x00010003`, all `cvg == 7`, 1:1 scale so no lerp) byte-for-byte vs Angrylion — non-vacuous (output col 0 = `0x1b` vs the raw `0x20`, the row-0 top taps reading 0) — with a mutation flipping the correction sign failing it. **Slice 4d (2026-07-25) — the AA edge filter.** A partial-coverage pixel (`cvg < 7`) now takes `video_filter32` (Angrylion `video.c`): it gathers the fully-covered pixels among its 6 taps (the up/down diagonals and the two-away left/right), takes the per-channel penultimate min/max (`vi_video_max`, the exact single-pass runner-up algorithm, ported verbatim for its tie-handling), and pulls the centre toward their midpoint — `centre + (((penmin + penmax − 2·centre)·(7 − cvg)) + 4 >> 3)` masked to 8 bits (unsigned two's-complement wrap). Validated by `vi_aa_edge_32` (`VI_STATUS = 0x00000003`, 32-bit with every 4th column partial so each partial pixel's taps are fully covered; the partial pixels are a fixed dark colour, *not* the smooth gradient's local midpoint, so the filter changes them at INTERIOR pixels too rather than only where the top-boundary taps break the gradient symmetry — a CodeRabbit catch) byte-for-byte vs Angrylion — with both a blend-coefficient mutation and a skip-the-filter (return-raw) mutation failing it. **Slice 4e (2026-07-25) — the divot median filter.** With `divot_enable` (VI_CTRL bit 4), a pixel whose 3 horizontal neighbours are not all fully covered takes the per-channel **median** of the post-de-dither/AA-edge values of itself and its left/right source-column neighbours (Angrylion `divot_filter`, the branch-expanded median-of-3 ported for its tie-handling); it is skipped (`(c.a & l.a & r.a) == 7`) where all three are fully covered, so it only touches partial-coverage edges. The coverage-exposing `vi_fetch32_cov` feeds it. Validated by `vi_divot_32` (`VI_STATUS = 0x00000013`, the every-4th-column-partial source) byte-for-byte vs Angrylion — non-vacuous (the partial columns and their neighbours differ from the non-divot AA-edge output, the median ≠ the AA blend), and a mutation forcing the median (removing the median-of-3) fails it. The all-fully-covered **early-return** is separately made observable (CodeRabbit #155): a **non-monotonic** fully-covered probe triplet (source columns 17/18/19 row 10, values low/high/mid per channel so the median ≠ the centre) lands on output pixel (10,10) with zero scale fraction; deleting the early-return computes the median there and fails the vector (`got [80,90,A0]` vs golden centre `[F0,E0,D0]`), where a monotonic gradient would have hidden it (median = centre). **Slice 4f (2026-07-25) — the 16-bit RGBA5551 coverage path.** The coverage machinery is now format-generic. A single primitive `Bus::vi_read_cov(x, y, bpp)` returns raw RGB8 + 3-bit coverage — 32-bit from alpha bits 7:5 (`(px>>5)&7`), **16-bit** from the 9th-bit **hidden plane** (`((px & 1) << 2) \| rdram_hidden`, the pixel's bit 0 as the coverage MSB and the two hidden bits as the low bits, so `cvg == 7` needs bit 0 set **and** hidden `0b11`); every downstream filter (de-dither / AA-edge / divot) then operates on 8-bit channels, so `vi_fetch_cov`, `vi_video_filter`, and `vi_divot` gained a `bpp` argument and the 32-bit path is byte-identical (all ten prior vectors stay green). The hidden read reuses the pre-existing `Bus::rdram_hidden` plane (2 bits/halfword) that until now only served the Z-buffer `dz` path; RustyN64's default-0 borders match the oracle because the harness memsets Angrylion's default-3 plane to 0 and sets the source region explicitly. The `.vivec` format gained **version 2** (a trailing hidden-bits plane, one byte per source pixel) — the driver populates Angrylion's `rdram_hidden` via a new `extern` and emits the plane, the loader packs it into `Bus.rdram_hidden`. Validated byte-for-byte vs Angrylion by three vectors, all with the same geometry as their 32-bit twins but `type = 2`: `vi_dedither_16` (`0x00010002`, all fully covered → the 5-bit de-dither unpack), `vi_aa_edge_16` (`0x00000002`, every 4th column partial → the AA-edge over hidden-plane coverage), and `vi_divot_16` (`0x00000012`, the partial pattern + a non-monotonic fully-covered probe triplet so the divot early-return is observable). Mutation-checked: forcing coverage to 7 (ignoring the hidden read) fails `vi_aa_edge_16`+`vi_divot_16`; neutralising the de-dither nudge fails `vi_dedither_16`+`vi_dedither_32`; removing the divot bypass fails `vi_divot_16`+`vi_divot_32`. **Still deferred within `scanout_scaled` (later slices):** the **gamma-dither** variants (`aa_mode` 0/1 coverage-gated / noise-based), and the **coverage filters under `aa_mode == 2`** (RESAMP_ONLY forces `cvg = 7` on hardware so de-dither can still apply; currently gated to `aa_mode ≤ 1`), and the **field-rate half of R-6** (the 50 Hz PAL field cadence + interlace/serrate + exact H_TOTAL, which live in the `Vi::tick` scan timing, not this scan-out geometry). `scanout_scaled` is **now wired into the frontend** (2026-07-25): `emu::EmuCore::produce_frame` calls `scanout_scaled` instead of the 1:1 `Bus::scanout`, so the presented picture uses the accurate scale-resample + filters — the R-12-style land-ahead-of-caller is now complete for the live path. `Bus::scanout` is retained for the harness frame tests (`composite_frame`/`real_rom_frame`/`golden_frame`/`commercial_boot`), which migrate as R-18 needs them. **`commercial_boot` has since MIGRATED (2026-07-29)** — it now measures through `Bus::scanout_scaled`, so it is a caller of the accurate path, not the 1:1 one; the preceding clause is retained as the historical statement. The frontend's `FB_MAX 640×480` backing store bounds the presented output via two guards: `scanout_scaled` returns `(0, 0)` (→ black) when `w * h * 4` overflows `frame.rgba`, and `presentable_geometry` rejects any dimension past `FB_MAX` (in practice a tall height — `scanout_scaled`'s width is prescale-bounded to `FB_MAX_W`, but its height can reach the 625-line prescale). **n64-systemtest impact: not measured** — `scanout_scaled` now runs in the frontend's `EmuCore::produce_frame` (wired 2026-07-25), but n64-systemtest is a headless CPU/RSP oracle with no VI presentation, so it does not drive the frontend and the scan-out stays unreachable by the suite; the failing-assertion count (90) is unchanged for that reason, not measured against it | +| R-5 | VI scan-out geometry and the analogue post-filters — `VI_X_SCALE`/`VI_Y_SCALE` resampling plus AA, divot and de-dither. | The pixel conversion is exact and cited; the resampling geometry and the coverage-gated filters were the deferred part. | absolute — a resampling/filter geometry choice, not a timing interval | **Largely resolved.** `Bus::scanout_scaled` implements the geometry, bilinear lerp, gamma and the coverage filters, validated RGB byte-for-byte against Angrylion through the 13 committed `.vivec` vectors. **Full record: [R-5](residuals/R-5.md).** | | R-4 | The VI register file (T-31-004) stores the **full 32-bit value** written to each register; the per-register write masks the hardware enforces (`VI_ORIGIN` 24-bit, `VI_WIDTH` 12-bit, `VI_V_INTR` 10-bit, the multi-field `VI_CTRL`/`VI_H_VIDEO`/scale registers, …) are not applied | The masks are documented as *field widths* in N64brew *Video Interface* per register, but the exact discard behaviour on write (which reserved bits read back 0 vs. retain) is what n64-systemtest's VI-register group actually pins, and that has not been run against a masked implementation | absolute — a register-decode fact, not a timing interval | **Open.** In-range writes (every value the register's own fields can hold) round-trip correctly, which the T-31-004 unit tests pin; out-of-range bits are retained rather than dropped. To be measured against n64-systemtest's VI group and masked per register when that group is exercised (measure, don't guess). No assertion currently exercises it (count unchanged at 93 *(as-at — see the note above this table)*) | | R-3 | FILL-mode `Fill Rectangle` (T-31-003) rasterises the rectangle with an **inclusive lower-right pixel** — floor the upper-left, and draw through the pixel that *contains* the lower-right coordinate; in FILL/COPY mode the low two bits of `yl` are forced set before the shift (`yl \| 3`) so the final scanline fills whole. The **scissor** clips separately with an **asymmetric** lower-right (inclusive X, exclusive Y — see R-15) | The N64brew wiki says only "upper-left rounded down, lower-right rounded up"; the exact edge behaviour is what the Angrylion oracle pins, and it is *inclusive of the lower-right pixel* (a rect `(0,3)-(1,4)` draws columns 0 **and** 1), plus the FILL/COPY `yl \| 3` (Angrylion `rasterizer.c` `rdp_fill_rect`). The earlier `(coord + 3) >> 2` half-open span dropped the boundary row/column — a realisation of the cited prose that disagreed with the hardware. Read from the oracle's output, not computed | absolute — a rasterisation geometry rule, not a timing interval, so the differential/re-phasing test is N/A | **Closed for the integer-coordinate FILL rect (oracle-validated).** The inclusive lower-right + `yl \| 3` rule is now pinned bit-for-bit against Angrylion by the **seeded-fuzz corpus** (`tests/vectors/fuzz/`, 48 random FILL rectangles sweeping colour/size/position with a full-image scissor, all byte-exact) and a mutation-checked unit test (`fill_rectangle_lower_right_edge_is_inclusive`). The fuzz gate **found** the pre-fix off-by-one. Still unverified: **sub-pixel** (fractional-coordinate) rect edges, which the whole-pixel fuzz does not exercise; the **scissor** lower-right rounding is resolved separately (**R-15**, asymmetric X/Y). No n64-systemtest assertion drives the render path (count unchanged at 93 *(as-at — see the note above this table)*) | | R-16 | The AI's `AI_STATUS` `COUNT` (bits 14:1), `WC` (bit 19), and `BC` (bit 16) readbacks, and the `AI_BITRATE` bit-clock timing, are a **best-effort** derived model, not oracle-pinned. `COUNT` is derived from `last_tick × video_clock / MASTER_HZ` as a sawtooth from `DACRATE/2` (wiki §COUNT); `WC` toggles on its second half-period; **`BC` is now modelled** (see the resolution) | These are the DAC's internal sample/bit-clock phase, which the CPU "cannot reliably sample rapidly enough" (wiki §BC) — so no public capture pins their exact value, and n64-systemtest has **no AI coverage** to gate against (verified by grep of `ref-proj/n64-systemtest`). The flags software actually polls (`FULL`/`BUSY`/`ENABLED`) are exact (ares `io.cpp`); only the sub-sample readback is nominal | absolute — a register-readback phase, no oracle | **Open (striven, ungated).** `FULL`/`BUSY`/`ENABLED` are unit-tested exactly; `COUNT`/`WC` are derived but unverifiable without a hardware logic-analyser capture (the user asked to strive for cycle-exactness and to search for capture data — none was found). To be pinned if an AI-status capture or an n64-systemtest AI group ever surfaces. Emits a plausible value rather than a fabricated constant. **`BC` MODELLED (2026-07-28).** `AI_BITRATE` is documented as "Half of bit clock period of I²S output to DAC", and the bit clock as "the Video clock, divided by two, divided by one more than this number" (N64brew Wiki *Audio Interface* §AI_BITRATE, mirrored at `n64brew_wiki/markdown/Audio Interface.md`) — so one half-period is `BITRATE + 1` video clocks and BCLK toggles once per half-period. `BC` (bit 16) is derived from the same video-clock tick count `COUNT`/`WC` already use, and `BITRATE = 0` stops the clock as documented. The readback gate is **`AI_BITRATE` alone** — the wiki says the counter "always ticks unless `AI_BITRATE` is 0" and never conditions either clock on `AI_DACRATE`, so BCLK keeps running with no DAC rate programmed (pinned by `the_bit_clock_runs_without_a_dac_rate`); `COUNT`/`WC` additionally need a DAC rate to have a range and report nothing without one. **It stays UNGATED, and the source itself is hedged:** the wiki says only that this is "(probably)" toggled per bit and that it "is believed" to be the BCLK line, precisely because the CPU "cannot reliably sample it rapidly enough even when `BITRATE` is set to 15" — i.e. it is not **CPU-observable**. Note the precise scope: that is a limit on *software* sampling, **not** a claim that BC is unpinnable in principle — an external logic-analyser probe on the BCLK line could establish it, and this entry would be updated if such a capture surfaced. What is claimed here is only that **no public capture is known** and none is asserted. Pinned by two tests: the transition count over a fixed span must equal the documented half-period relation derived independently from `video_clock` and `BITRATE` (mutation-checked — using `BITRATE` instead of `BITRATE + 1`, or dropping the bit, both fail), and a `BITRATE` of 0 must hold the line low. **n64-systemtest impact: not measured** — no AI test drives this path (the suite has no AI coverage), so the oracle count is **unchanged at 93** *(as-at — see the note above this table)* | @@ -410,10 +415,10 @@ labelled as one until something reads the register on hardware. | R-21 | `Fill Rectangle` (0x36) always writes the **`SET_FILL_COLOR` register**, whatever the cycle type. On hardware only FILL mode does that — in 1-/2-cycle mode the rectangle is rasterised through the **combiner/blender** like any other primitive | `fill_rectangle` calls `fill_pixel` unconditionally; the cycle type is never consulted. Every committed conformance vector that exercises `Fill Rectangle` sets FILL mode, so the non-FILL path has no coverage in either direction and the gap was invisible | absolute — a coverage boundary, not a fitted constant | **Open (found 2026-07-28, not yet oracled).** Surfaced *accidentally* while building the microcode end-to-end test: that test's queue initially mis-packed `SET_OTHER_MODES` (the cycle type went into word1 instead of word0 bits 21:20, so the RDP never entered FILL mode) and **the picture was still correct**, which is only possible because `Fill Rectangle` ignores the cycle type. A reviewer flagged the mis-packing; chasing it found this. The queue is fixed and the test now asserts the *emitted* `SET_OTHER_MODES` carries FILL, so the two are no longer confounded. **RESOLVED 2026-07-28 (oracle-confirmed).** Vector `fill_rect_1cycle_16` settles it: a 1-cycle rectangle with prim `(0x22,0x44,0x66)`, a combine selecting prim for RGB and alpha, and a *deliberately different* green fill register `0x07C1` renders **`0x2219` in all 64 pixels** — the prim colour in RGBA5551, not the fill register. So hardware routes a non-FILL rectangle through the combiner and never reads the fill register. `fill_rectangle` now takes the combiner path (with alpha-compare and dither, as the triangle path does) unless the cycle type is `CYCLE_TYPE_COPY` or `CYCLE_TYPE_FILL`. Mutation-checked: reverting the guard reproduces `07 C1` against golden `22 19` at pixel (0,0), and *only* that vector regresses. A second vector `fill_rect_2cycle_16` pins the **2-cycle** branch, which the 1-cycle vector leaves untested because `combine()` takes a distinct path there (cycle 0 runs first and feeds cycle 1 as the `COMBINED` input): its combine makes cycle 0 emit the env colour and cycle 1 select `COMBINED`, so env `0x8D73` means the chain ran, black means cycle 0 was skipped, and green `0x07C1` means the cycle type was ignored. Angrylion renders **`0x8D73`** and we already match. **Fallout worth recording:** six existing tests — the five `fill_rectangle_*` unit tests and the `golden_frame` end-to-end test — were named for FILL-mode behaviour but **never selected FILL mode**, so they were passing on this bug. They now emit a `Set Other Modes` with `cycle_type = FILL` and test what their names claim. **Still open:** the same question for a *flat* `Fill Triangle` (0x08) with no shade/texture block, which likewise takes the fill register unconditionally (the presence of a shade/texture block selects the combiner there, not the cycle type); no vector exercises it yet. **n64-systemtest impact: not measured** — the suite has no RDP render-path coverage | | R-17 | The AI DMA models the sample **rate** exactly (`MASTER_HZ / (video_clock / (DACRATE + 1))` per sample-pair) but charges **no DMA setup/arbitration latency and no RDRAM bank-state cost**: the transfer begins, and the start-interrupt fires, at the derived sample boundary rather than after the real DMA-engine delay. The underrun behaviour is a defined **hold-and-decay** (integer `× 63/64` per sample) rather than the analog decay curve | The AI DMA is "directly connected to the DAC" and "progresses as samples are physically put through the DAC" (wiki §DMA), so the per-sample rate is the dominant timing term and is exact; the fixed setup latency `M` and the RDRAM bank costs are the same unmeasured constants flagged for the CPU/PI (they belong in this ledger with provenance when measured, never tuned). The decay shape is deterministic and no-`std`-friendly; ares uses `exp(-1/(freq·0.003))` | absolute — an unmeasured latency, not a differential re-phasing | **Open.** The rate and the FIFO/interrupt sequencing are unit- and integration-tested; the setup latency stays unmeasured (measure, don't guess) and the decay is defined-but-unpinned. No AI-timing oracle exists in the committed suites, so nothing gates it yet — to be validated against the project64 `DoubleShot` PCM ROM (Sprint 2) and any AI-timing capture that surfaces. **n64-systemtest impact: not measured** — no AI test drives the DMA-timing path, so the oracle count is **unchanged at 93** *(as-at — see the note above this table)* | | R-15 | The **scissor** lower-right bound in FILL mode is **asymmetric**: the **X** bound is **inclusive** of its boundary pixel while the **Y** bound is **exclusive**, and a rectangle lying entirely at or past the scissor's right edge draws nothing (`allover`). `fill_rectangle` previously clipped both bounds exclusively (`(coord + 3) >> 2`) | Isolated cleanly against the Angrylion oracle by a scissor-clip fuzz batch (rectangles extending past the scissor on each edge). The X clip keeps the pixel containing `scissor.xl` (a rect spanning past `xl = 8.0` fills column 8), but the Y clip drops row `scissor.yl >> 2` (a scissor `yl = 5.0` fills up to row 4). The asymmetry is `edgewalker_for_prims`: the rectangle's `yl` is `\| 3`'d (FILL/COPY) so its own last scanline fills, but the scissor's raw `clip.yl` makes `invaly = k >= yllimit` drop that boundary row; the horizontal clip (`curover = xlsc >= clip.xl << 1`, `allover` ⇒ `!validline`) keeps the boundary column unless the whole span is over it. Read from the oracle's output, not computed | absolute — a rasterisation geometry rule, oracle-confirmed | **Closed for the integer-coordinate FILL scissor (oracle-validated).** `fill_rectangle` now clips X inclusive with the `allover` guard (`rect_xh >= scissor.xl` ⇒ nothing) and Y exclusive, plus a hard width clamp. Pinned by a 48-vector scissor-clip fuzz family (`tests/vectors/fuzz/fz_scis_*`, all byte-exact) and the reconciled `fill_rectangle_is_clipped_to_the_scissor` unit test (which previously asserted an unverified exclusive X edge). Sub-pixel (fractional-coordinate) scissor edges remain unexercised. No n64-systemtest driver (count 93 *(as-at — see the note above this table)*) | -| R-18 | A **commercial ROM boots and executes real code but does not reach video** (Phase 5 capstone). Through the retail HLE boot (`rom::hle_boot`) the game's own IPL3 runs, the CPU fetches the cartridge's instruction stream, and the PC advances through hundreds of millions of retired instructions across varied routines — but no frame is scanned out: over ~10 s of emulated time `VI_CTRL` stays 0, `VI_ORIGIN` is never set, and **no interrupt of any kind fires** (SM64 witnessed at `retired ≈ 9.4×10⁸`, all MI interrupt lines clear) | The retail OS-boot runtime the game waits on is not yet modelled. A commercial title's boot is interrupt-driven: after its OS initialises, its main loop blocks on the **VI vblank interrupt**, which the emulator only raises once the game programs `VI_CTRL`/`VI_V_INTR` — and the game does not reach that programming, indicating an earlier dependency (the **RI/RDRAM interface** registers used for RDRAM sizing, and/or the OS thread/interrupt setup). This is a cross-subsystem gap spanning the VI vblank loop, the RI registers, and the F3DEX graphics microcode — all **outside the Phase 5 cart/boot/saves boundary** (ADR 0003; the cart phase delivers PI/SI/PIF/CIC + saves, not the OS runtime) | absolute — a coverage boundary across subsystems, not a fitted constant or a timing interval | **Open — characterised, not a regression.** The committable Phase 5 gate (n64-systemtest cart/PIF/SI, save round-trips, homebrew boot) is met; the commercial capstone is asserted at its honest achievable level — `a_commercial_rom_boots_and_executes` (local, `#[ignore]`d) proves the ROM boots and retires ≥ 10⁶ real instructions without panicking, and *reports* the lit-pixel count (0) rather than asserting it. Reaching a title frame is deferred to the VI/RI/F3DEX work of a later phase and validated then. This gap was surfaced by the capstone exactly as the plan's escalation gate intended: **ship v0.6.0 on the committable gates + an honest "boots and executes" capstone, not a faked pass or an unbounded chase.** n64-systemtest impact: none — the boot/video path has no systemtest driver; the suite-wide count is **90** (see C-32) **SUBSTANTIALLY RESOLVED 2026-07-29 — the root cause was NOT the theory above.** It was `hle_boot` never seeding **`sp`**. IPL1 sets it before handing off (N64brew *IPL2* §IPL1 listing, `0xBFC000D0`: `ORI sp, sp, 0x1FF0 # sp = 0xA4001FF0`), and `hle_boot` skips IPL1/IPL2 without standing in for it. With `sp = 0`, IPL3's opening `ADDIU sp, sp, -24` / `SW s3, 0(sp)` prologue stored to `0xFFFF_FFE8` — KSEG3, TLB-mapped, no entries — taking a TLB-refill exception to `0x8000_0000` in empty RDRAM and executing a **NOP sled to the end of memory**. That is why the game never programmed `VI_CTRL`: it never ran at all. The symptom hid perfectly behind the capstone's own metric, because a sledding machine still retires ~180 million instructions. Found by tracing the instruction stream rather than the state (`docs/engineering-lessons.md`), and by noticing `retired` was *identical across four different games* — the 'same value regardless of input' signature. With `sp` seeded (and the **RI register block** decoded so IPL3's `RI_SELECT` read is coherent), retail titles now boot into their own code: Super Mario 64 reaches `pc=0x80246ddc` with 928 KiB of RDRAM populated, Star Fox 64 submits **122** RDP commands, World Driver Championship **45**, and Super Mario 64, Star Fox 64 and World Driver Championship upload graphics microcode into IMEM. **"Lit pixels" has been RETIRED as evidence — it never meant what earlier revisions of this row implied.** It counts non-black scanned-out pixels, and *uninitialised RDRAM is non-black*. Rogue Squadron and Jet Force Gemini score 68 527 and 69 479 of 75 840 (90-92%) with 4 790 and 6 203 distinct colours, which sounds like a picture; **rendered to PNG and looked at, both are pure noise** — RDRAM garbage scanned out, no rendered content whatever, and neither title even runs microcode. This is the `retired > 1_000_000` failure again: a metric a broken machine satisfies exactly as easily as a working one, cited for weeks because nobody opened the image. Only two things are evidence of video now: **(a)** a byte-comparison against a committed golden frame, and **(b)** a human or oracle actually viewing the output. The capstone still *reports* the count, but as a diagnostic, never as a pass condition. **Where the video gap actually is — localised, and NOT where an earlier revision said.** That revision blamed the "RDP → VI presentation path"; **wrong**. Ocarina of Time's framebuffer at `VI_ORIGIN` is uniformly `0x0001` — RGBA5551 with R=G=B=0 and the coverage bit set — so **the VI is faithfully presenting a genuinely black buffer** and `scanout` is correct. Nor is submission at fault: a DPC-seam opcode census over 300 frames shows the real F3DEX stream arriving — **7,412 `TRIANGLE` (0x0F: shade + texture + Z)**, 1,630 `TEXTURE_RECTANGLE`, 4,374 `LOAD_BLOCK`, 7,705 `SET_TILE`, 1,589 `SET_COMBINE`, of 74,508 commands. So geometry, textures and tile state all reach the RDP, and the frame still ends as the clear colour: **the RDP rasterises real geometry to black**. That is the gap. **Narrowed again, by inspecting the RDP state Ocarina actually leaves.** The **Z path is refuted**: `z_compare_en = false` and `z_update_en = false`, so nothing is depth-rejected (`SET_Z_IMAGE` *is* issued, `z_image = 0x12c700`, but the compare is off). The **combiner is refuted as a *collapse***, and is in fact the key evidence: `cyc1` decodes to `rgb_a=15, rgb_b=15, rgb_c=31, rgb_d=1` = `(0 - 0) x 0 + TEXEL0`, a pure **texture pass-through** — so the pixel colour *is* the texel, and a black frame means **the texel fetch resolves to 0**. The tiles are `fmt=2, size=0` = **CI4**, 4-bit colour-indexed textures resolved through a **TLUT**. Both TMEM halves are populated after 4,374 `LOAD_BLOCK`s — 1,508/2,048 non-zero bytes in the texture half, 760/2,048 in the TLUT half — and TLUT entry 0 reads `0x0000` (black), with entries alternating `0000 ffff 0000 ffff` at the 8-byte stride `tlut_lookup` uses. So the remaining gap is the **CI4 + TLUT texel path**: real indices and a real palette are present, and the resolution yields black. **That probe has been run. Its result is PROVISIONAL and is recorded as such.** A CI4-with-TLUT vector was authored (eight indices 0..7 against eight distinct non-zero palette entries) and replayed: RustyN64 produced `f801 07c1 003f ffff ffc1 07ff` — red, green, blue, white, yellow, cyan, exactly the authored TLUT — while **Angrylion produced mostly `0x0001`** from the same command list. Angrylion is the oracle, so the disagreement means the authored **`Load Tlut` encoding is wrong**, not that our decoder is right: Angrylion loaded a near-empty palette and we were more permissive about where the entries came from. The vector was therefore **deliberately not committed** — committing that golden would pin an authoring error as the spec. **What this does and does not establish:** it shows the CI4 index→palette mapping resolves against whatever TMEM *we* loaded, which makes a totally-dead CI4 decoder unlikely; it does **not** establish that the load path is correct, because the probe never exercised a verified `Load Tlut`. So CI4 is *weakly* de-prioritised as R-18's cause, not eliminated. **A separate, independently real defect surfaced:** `Set Other Modes.tlut_en` — **bit 47**, N64brew *Reality Display Processor/Commands* §0x2F, *"tlut_en: Enables Texture Look-Up Table (TLUT) sampling"*, with `tlut_type` at bit 46 selecting RGBA16 vs IA16 — is **not decoded at all**. Our TLUT lookup was driven purely by the tile's *format* field, so a CI tile with `tlut_en` clear still got a palette lookup and a non-CI tile with it set did not: wrong in both directions. **FIXED 2026-07-29.** `tlut_en` (bit 47) and `tlut_type` (bit 46) are now decoded, and the colour-index lookup is gated on `tlut_en` rather than on the format. The oracle settled the `tlut_en = 0` behaviour rather than it being guessed: `ci4_tlut_disabled_16` is byte-identical to `tex_tri_ci4_tlut_16` apart from that single bit, and the two goldens are **the full palette versus all black** — so an un-TLUT'd CI tile renders black. That is reproduced as the observed result, *not* as a mechanism claim: §0x2F does not document what the hardware does with un-TLUT'd index data, so no reinterpretation of the index bits is invented. `tlut_type`'s **IA16** palettes remain deferred (the lookup assumes RGBA16) — decoded so the flag is no longer silently ignored, but unimplemented until a vector exists. Battery is 53 probes (40 RDP + 13 VI) — the authoritative current count, matching `docs/STATUS.md`; the **38/51 and 39/52 figures earlier in this row are dated measurements, correct when written and kept under the append-only rule**, not competing claims about the present; mutation-checked by removing the gate. **Only one of the two directions is fixed:** a CI tile with `tlut_en` clear is no longer palette-mapped, but 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 prose would be inventing behaviour, so it stays wrong-but-recorded until a vector defines it. **SETTLED 2026-07-29.** The `Load Tlut` encoding was verified against N64brew *…/Commands* §0x30 and the first attempt had **two** errors: `lower_right.s` is command bits **23:12** (`lo >> 12`, so an 8-entry palette is `(8-1) << 2 = 0x1C` shifted by 12 — I had shifted by 14, decoding as 29 entries), and 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 correctly, Angrylion now renders the intended palette — `f801 07c1 003f ffff ffc1 07ff` — and **RustyN64 matches it byte-for-byte**. The vector `tex_tri_ci4_tlut_16` is committed (38 RDP vectors, 51 battery probes) and mutation-checked: changing `tlut_lookup`'s stride from the quadrupled 8 to 4 turns it red at pixel (3,2). So the **CI4 + TLUT path is now genuinely ELIMINATED** as R-18's cause — not weakly de-prioritised — and the colour-indexed path finally has oracle coverage it never had. **`tlut_en` remained undecoded** at the time of writing (above) — a real defect but demonstrably not this one; it was **FIXED 2026-07-29** in #185, recorded later in this row, and this clause is retained as the historical statement under the append-only rule rather than rewritten: the vector passes because our lookup keys off the CI format, which happens to coincide with `tlut_en` being set here. **R-18's video cause is therefore still open**, and 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. **`Load Block` (0x33) has ZERO oracle coverage**, which is the sharpest of those: every committed texture vector loads through `Load Tile` (0x34), while Ocarina issues **4,374 `Load Block`s** of 74,508 commands — it is the dominant texture-load path in a live retail stream and is entirely unpinned. Reading the implementation, `load_block` *does* handle the two details §0x33 calls out — coordinates as **u12.0** (not `Load Tile`'s u10.2) and the **dxt-driven odd-line 32-bit word swap** (`line = (word * dxt) >> 11`, `swap = (line & 1) << 2`) — so a static read finds nothing wrong. **Two attempts to author a `Load Block` vector were made and both DISCARDED (not committed).** Recording the evidence so the next attempt starts from data rather than from my summary of it. Candidate `load_block_odd_line_16`: 16 distinct non-zero RGBA16 texels at `0x3000`; `Set Texture Image` `0x3D100007 0x00003000` (16-bit, width 8); load tile 7 `0x35100400 0x07000000` (16-bit, line 2 words, tmem 0); `Load Block` `0x33000000 0x0700F800` (uls=0, ult=0, lrs=15, dxt=0x800); render tile 0 `0x35100402 0x00000030` (16-bit, line 2 words, tmem word 2, mask_s=3). **Observed:** Angrylion's row 7 came back `003f 0001 0001 0001 f83f 8421 ffc1 07ff` — containing `0001` fill (unwritten TMEM) interleaved with line-0 texels — and at pixel (0,1) Angrylion gave `003f` where RustyN64 gave `0841` (line 1's first texel). **What that establishes:** the golden is unusable as an oracle, because part of it reflects TMEM the load never wrote. **What it does NOT establish:** *why*. The plausible **hypothesis** is that the authored layout is wrong — tile `line`, `tmem_addr`, or the texel count's inclusivity — but that is unverified, and the divergence is therefore **not attributable to RustyN64 either**. **That question is now SETTLED, and `Load Block` finally has coverage.** §0x33's prose says *"`lower_right.s - upper_left.s` determines the number of texels"* (no `+1`) while `Load Tile` is inclusive, so the readings disagreed and prose could not decide it. A **minimal** vector could: `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 layout can confound it (precisely what sank the two discarded attempts). **Angrylion loads two.** The count is therefore **inclusive** and `load_block`'s `shi - slo + 1` is correct; RustyN64 matches the golden byte-for-byte, and the vector is committed (39 RDP vectors, 52 battery probes) and mutation-checked — dropping the `+1` turns it red. So this hypothesis joins the others as **refuted**, and `Load Block` is no longer wholly unpinned, though the coverage it now has is minimal: multi-line loads, the odd-line swap under a real `dxt`, and non-16-bit texel sizes all remain untested. **CORRECTION (same day).** This row briefly claimed "the RSP is never started — no title unhalts it, so those RDP commands come from the CPU driving the DPC directly". **That was wrong**, and wrong for an instructive reason: it was measured off `Rsp::halted` / `Rsp::pc`, two `pub` struct fields that are **never written**. The authoritative state is `SP_STATUS` (`sp.halted()` / `sp.pc()`), which is what `su_step` itself gates on. Sampling the dead fields reports "halted forever at PC 0" for a *running* RSP — the inert-API hazard of `docs/engineering-lessons.md` §3.2, and it produced two confident wrong conclusions in one session. The fields are now private with `Rsp::halted()` / `Rsp::pc()` accessors delegating to `SP_STATUS`; they are kept in the struct only because removing them would change the save-state layout (ADR 0005). Measured correctly, **retail microcode executes**: Castlevania Legacy of Darkness visits 805 distinct RSP PCs, 007 TWINE 459, Beetle Adventure Racing 356, Star Fox 64 331, Super Mario 64 236, Mega Man 64 229, World Driver Championship 148 — see T-71-003's witness, `tests/game_microcode.rs`. **Provenance for `T-71-003`'s witness parameters** (recorded because a threshold without it is a fitted constant). `MIN_DISTINCT_PCS = 32` separates two **measured** populations more than an order of magnitude apart: titles whose RSP never runs measure **0** distinct PCs (Blast Corps, Bomberman 64, Donkey Kong 64, Jet Force Gemini, Rogue Squadron), while titles whose microcode runs measure **148-815** (World Driver Championship 148, Mega Man 64 229, Super Mario 64 236, Star Fox 64 331, Beetle Adventure Racing 356, 007 TWINE 459, Castlevania Legacy of Darkness 805). `SAMPLE_TICKS = 24` is a sampling cadence, not a hardware value: it is 8 RCP steps (the RCP advances every 3 master ticks, ADR 0006), sampling can only **under**-count so every figure is a lower bound, and re-running at the finest possible cadence of 3 (8× finer) leaves the verdict unchanged — 805→815, 459→463, 356→356, 229→258, same four witnesses. `FRAMES = 90` doubled to 180 yields an **identical** witness set with unchanged counts. **Corpus evidence for the capstone's `MIN_RDRAM_NONZERO` floor** (recorded here because a threshold with no provenance is a fitted constant): a booted title leaves **787 KiB - 1.23 MiB** of RDRAM non-zero (Mega Man 64 539 KiB at the low end, World Driver Championship 1.23 MiB at the high), while a machine that faults out of IPL3 leaves **exactly 0**. The capstone's floor is therefore derived as `IPL3_COPY_BYTES / 4` (256 KiB) from the documented 1 MiB IPL3 copy, comfortably between the two populations. **R-18 remainder — diagnosed to a point, and the limits of that diagnosis stated.** Five staged titles still do not run microcode, in three groups. (1) **CIC-6103/6106** — Banjo-Kazooie, 1080 Snowboarding, F-Zero X — *do* boot and execute game code (Banjo-Kazooie reaches `pc=0x80268fcc` with 1.2 MiB of RDRAM populated) and fill IMEM to 4094 bytes, but never leave `SP_STATUS.halt` even over **600 frames / 10 s emulated**, so this is a stall, not a slow init. (2) **Blast Corps, Bomberman 64** boot but never load microcode at all (IMEM stays 0). (3) **Rogue Squadron** loads no microcode; its high lit-pixel count is **noise**, not output (see the lit-pixel retirement above), so it renders nothing at all. **Three hypotheses have now been tested and REFUTED; record them so they are not re-chased.** (a) *KSEG0 under 64-bit addressing* — the segment map returns `Direct` for `0xFFFF_FFFF_8028_4C78` in wide kernel mode, pinned by `r18_kseg0_is_direct_in_wide_kernel_mode`. (b) *"These titles sit in the exception vector"* — a **correlated** capture (armed on first entry to `0x8000_0000..0x8000_0200`, per `docs/engineering-lessons.md`) showed **no exception at all**: `ExcCode=0`, `EPC=0`, `Cause=0`. The instruction stream shows IPL3 executing `LUI t4,0x8000` / `ADDIU t4,t4,0` / `JR t4` — for **CIC-6103/6106 the game's entry point simply IS `0x8000_0000`**, so PCs like `0x8000_018c` are game code, not a vector. An earlier revision of this row read those PCs as an exception loop; that was wrong, and it was wrong because the first reading sampled `Cause`/`EPC` uncorrelated, long after the fact. (c) *R-18's original "the VI vblank interrupt never fires" theory* — F-Zero X programs the VI (`VI_CTRL=0x3102`, `VI_V_INTR=2`), and the interrupt **does** fire and reach the CPU: 1228 `MI_INTR.vi` and 3304 `Cause.IP2` samples over 120 frames with `MI_MASK=0x3f`, comparable to Super Mario 64's 1024/7219 — and SM64 runs microcode fine. The VI interrupt path is working. **What the evidence does still show:** Banjo-Kazooie alternates between game code (`0x80268fxx`) and `0x8000_0184` at *identical* sample counts, which is the signature of a tight fault-and-return loop; 1080 spins at one PC; F-Zero X spins while receiving interrupts. So the remaining cause is per-title and downstream of boot, VI and addressing — not one shared subsystem gap. **Genuinely still open:** several titles (Blast Corps, Bomberman 64, Donkey Kong 64, Jet Force Gemini) never load microcode into IMEM at all, and Rogue Squadron never starts the RSP, so their boots stall earlier than the RSP seam. n64-systemtest is unchanged at 90 (it uses the ELF load path, not IPL3). **FIRST RENDERED COMMERCIAL FRAME — 2026-07-29.** R-18's headline claim is now falsified by a picture: **Paper Mario renders real geometry through the full LLE path** (retail HLE boot -> the game's own code -> its graphics microcode on the LLE RSP -> DPC seam -> LLE RDP -> `Bus::scanout_scaled`), committed at `screenshots/paper-mario-first-commercial-frame.png`. Its colour image holds **87 distinct RGBA5551 values** (dominant `0xE739` = (28,28,28,1) at 72,156 of 76,800), and the frame is fully lit on **both** scan-out paths — **75,840 / 75,840** at 320x237 through the unscaled 1:1 `Bus::scanout` and **148,125 / 148,125** at 625x237 through the presented `Bus::scanout_scaled` (the two denominators are kept apart deliberately; quoting one beside the other path's dimensions is an error this row's first revision made) — held stably from frame 120 through 270 of a 300-frame run — flat-shaded quads with clean edge-walked slopes on a light-grey clear, **viewed, not inferred**. So the earlier conclusion that 'the RDP rasterises real geometry to black' was **title-specific, not a pipeline defect**: the pipeline produces a correct picture end-to-end. **The remaining gap is coverage, and it stratifies by title**, measured over 120 frames: Ocarina of Time 27,651 RDP commands but 98% of its colour image is `0x0001` (the black clear) and only 1,440 px lit; Majora's Mask 2,310 commands; World Driver Championship 45 commands and **exactly one distinct value** (`0x0001`) — it clears and draws nothing; Super Mario 64 and Banjo-Tooie submit **zero** commands. **Two claims in this row's opening column are now WRONG and are retained only as the historical observation** (per the immutable-reference rule, cf. R-5): `VI_CTRL` does **not** stay 0 and `VI_ORIGIN` is **not** never set — both are programmed by every title that boots (SM64 `VI_CTRL = 0x13016`, Mario Kart 64 `0x3116`, both TYPE=2), and interrupts do fire. **The noise trap was re-tested, not merely re-asserted:** on the same run Ocarina scored 62,963 lit pixels at frame 30 and, rendered and viewed, is pure uninitialised-RDRAM noise — indistinguishable from Paper Mario's real frame by pixel count alone, which is why the screenshot policy requires looking. **Measurement defect fixed in the same change:** `commercial_boot` measured through the superseded 1:1 `Bus::scanout` rather than `Bus::scanout_scaled`, the path the frontend actually presents, so every lit-pixel number this row ever quoted was from a buffer no user sees; and `Bus::scanout_scaled`'s own rustdoc still claimed it was 'not yet wired into the frontend', false since #158. **A suspicion raised and withdrawn by measurement:** the 625-wide scan-out looked like `PRESCALE_H` leaking into the width term; it is **correct** — `VI_X_SCALE = 0x200` is 0.5 in 2.10, so 320 upscales to 640, less the 8/7-px `minhpass`/`maxhpass` crop = 625. **THE NON-RENDERING TITLES ARE WAITING, NOT FAULTING — measured 2026-07-29, and it corrected two of my own claims in the same session.** With a picture now proven possible (Paper Mario), the question became why the others do not, and the `scanout_dims` field added to `commercial_boot` in **#186** split them into groups that a lit-pixel count had merged: **VI never enabled** (Banjo-Kazooie, Banjo-Tooie, 1080 Snowboarding), **VI enabled but zero RDP commands** (Conker `VI_CTRL=0x3116` w=292, Super Mario 64 `0x13016`, Jet Force Gemini `0x1311E`), and **real frame scanned but black** (007 `625x237` at `0/148125`, World Driver Championship). **CORRECTION 1 — the 'hard fault loop' was a sticky-register artefact.** Sampling `Cause.ExcCode` showed AdES on 10,575 of 12,000 samples for Banjo-Kazooie, which reads as a permanent fault loop and was reported as one. `Cause` is **sticky** — it survives the handler returning — so counting *transitions* instead of samples gives the real number: **exactly 1** address-error transition over **1,332,906,106** retired instructions (first at 14,432,774), and **exactly 1** for 1080 over **1,995,894,174** retired (first at 7,724,174). One fault, then over a billion instructions of spinning. The fault is **not** what stops these titles, and is very likely handled normally. This is the same failure mode as `retired > 1_000_000` and 'lit pixels': a number that a broken machine and a working one both produce. **CORRECTION 2 — our AdES is CORRECT in both cases; the emulator is not at fault where I first looked.** Banjo-Kazooie faults on `SD t0, 88(k0)` at `epc=0xFFFF_FFFF_8026_8FC0` (`k0`/`k1` = libultra's exception preamble saving thread context) to `0xFFFF_FFFF_8028_4C78` — 8-byte aligned and correctly sign-extended, so neither documented AdES cause applies. `Status = 0x6D016CAA` gives **`KSU = 1` (Supervisor), `SX = 0`**, and **KSEG0 does not exist in Supervisor mode**, so the address-space check *must* reject it. 1080 faults at `epc=0xFFFF_FFFF_A400_02F4` (IPL3 executing from **DMEM**) on `0xFFFF_FFFF_A400_02F3` — `Status = 0x34000002`, `KSU = 0` (Kernel), and the address is simply **odd**, so AdES is again correct. **Three emulator-side hypotheses were refuted by reading the code rather than patching it:** `EXL`/`ERL` forcing Kernel regardless of `KSU` is implemented correctly (`pipeline.rs` `access_mode`); the 64-bit-operation reservation is correct (`sixty_four_bit_is_reserved` — never reserved in Kernel, whatever `KX` says); and `kernel_segment` classifies `0xFFFF_FFFF_8xxx_xxxx` as CKSEG0 through its `0xFFFF_FFFF_8000_0000..=` arm. **`CpU` exceptions are benign — established by a CONTROL, not by argument:** Jet Force Gemini raises 174 and Paper Mario, which renders correctly in the same run, raises 18. It is libultra enabling the FPU per thread on demand. **What is genuinely open:** all four non-rendering titles **spin**, and two of them (Banjo-Tooie, Jet Force Gemini) never raise an address error at all — so the shared cause is something they are *waiting* for that never arrives, not a fault. The open questions are therefore (a) why Banjo-Kazooie's `Status.KSU` is 1 when libultra runs in Kernel mode, (b) what 1080's IPL3 is storing to an odd DMEM address, and (c) **what all four are blocked on** — the interrupt/DMA-completion path is the first place to look, since a title that never gets its completion signal spins exactly like this. Note the grouping above is provisional: Jet Force Gemini *does* enable the VI, so 'blanked VI' was too coarse a bucket for it. **BANJO-TOOIE LOCALISED TO A TWO-INSTRUCTION LOOP — 2026-07-29, and a sampling artefact was caught on the way.** Banjo-Tooie's CPU **is** executing — `retired` advances by a perfectly constant **781,250 per frame** (exactly one instruction per 2 CPU cycles) — while the fetch PC never leaves a two-instruction window. **The first measurement of this was an ALIASING ARTEFACT and is recorded because the correction matters:** sampling every 400 master ticks (= ~100 instructions) reported **one** distinct PC over 60,000 samples, which reads as a hard deadlock. The sample period was commensurate with the loop, so it landed on the same instruction every time. Re-sampled at coprime periods (397, 1009) and at **step = 2 master ticks — one CPU cycle, the finest granularity there is** — the answer is stable and different: **exactly two PCs, 50/50**, `0x8003_29A8` = `JAL 0x8002_E9C0` (`0x0C00BA70`) and `0x8003_29AC` = its delay slot `OR a0, zero, zero` (`0x00002025`). No exception vector ever appears at one-cycle sampling, so **nothing is faulting** — this is not an exception loop. **The evidence, stated without a cause attached:** (1) control never reaches `0x8002_E9C0`, which **does** contain a valid function prologue (`ADDIU sp, sp, -0x38` / `SW ra, 0x24(sp)` / `SW a0, 0x38(sp)` ...), so the target is loaded and the transfer simply does not happen; (2) ~~`ra` holds the JAL's own address rather than the required `PC + 8`~~ — **RETRACTED before merge by the retirement measurement below: the JAL never executes, so `ra` was never its business and the observation carried no information**; (3) the instruction immediately before the loop, at `0x8003_29A4`, is **`ERET`** (`0x42000018`), and code after an `ERET` is normally unreachable — so execution arriving at `0x8003_29A8` at all is itself suspect; (4) the RSP is halted throughout. **Caveat kept deliberately:** `Cpu::pc` is the **fetch** PC of a five-stage pipeline (ADR 0007), not the retiring position, so 'the PC is here' means the fetch position is here. **Next step is to confirm which of the two candidates it is before touching either** — a `JAL` link-value/target defect, or an `ERET` that falls through instead of redirecting to `EPC` — because naming a cause before measuring its boundary has been wrong here four times running. **Both candidates were then WEAKENED by checking before theorising further:** the isolated behaviours are already pinned and green — `jal_links_past_the_delay_slot_and_jr_returns_there` and `eret_resumes_at_epc_and_has_no_delay_slot` (plus `a_jal_in_a_delay_slot_links_past_the_outer_target`) — and the CPU category of n64-systemtest passes at `Failed: 0`. So neither `JAL` nor `ERET` is simply broken; whatever is happening is **conditional on state these unit tests do not set up**, which is a materially different search than 'fix JAL'. **MEASURED — and the answer is that NOTHING retires at `0x8003_29A8`.** Dumping all four inter-stage latches at one-cycle granularity: the `JAL` enters `IC/RF` and is **discarded there, never reaching `RF/EX`**, while the only instruction to reach `DC/WB` in 400 cycles is the **`ERET` at `0x8003_29A4`**, **200 times** — once every two cycles, which is exactly the constant 781,250/frame retirement rate. The loop is `ERET` → fetch `0x8003_29A8` → `ERET`, and the `JAL` is never more than a fetched-and-flushed shadow. **`EPC = 0xFFFF_FFFF_8003_29A4` — the `ERET`'s own address.** With `Status = 0x0000_FF01` (`EXL = 0`, `ERL = 0`, `IE = 1`, `IM = 0xFF`), `ERET` correctly takes the `ERL = 0` path and sets `PC <- EPC`, landing back on itself. **So `ERET` is behaving exactly to specification; the defect is that `EPC` points at the `ERET` instruction.** `Cause.ExcCode` is 0 and `EXL` is clear, so **no exception is being taken in the loop** — the corruption happened once (`EPC` becomes `0x8003_29A4` at frame 12, alongside `Status` changing from `0x3400_0000` to `0x0000_FF01`) and the machine then settled into a deterministic self-loop. **The one remaining question is what wrote that `EPC`**, and there are exactly two sources: libultra's thread dispatcher writes `EPC` explicitly via `MTC0` before its `ERET` (so a thread context holding the dispatcher's own resume address would do it), or an exception was taken **on the `ERET` itself**, which sets `EPC` to the `ERET`'s address. Distinguishing those two is a single probe on the `MTC0 EPC` write path versus the exception-entry path, and it is the next step. The `CACHE`/I-cache hypothesis is **eliminated**: the fetched words match RDRAM exactly. **ROOT CAUSE FOUND AND REPRODUCED — an interrupt is charged to the `ERET`, not to the instruction it returns to (2026-07-29).** Catching the fatal `EPC` write in the act (cycle-stepping with a pipeline-history ring buffer) gives an unambiguous verdict: `EPC 0xFFFF_FFFF_8001_3678 -> 0xFFFF_FFFF_8003_29A4`, **`EXL 0->1`**, `Cause = 0x0000_0400` (`IP2`, the RCP line), **verdict: EXCEPTION ENTRY**, with the history showing `EX/DC 0x8003_29A4:ERET` -> `DC/WB 0x8003_29A4:ERET` at that exact cycle. The preceding instructions are `LUI k0,0xA430` / `ADDIU k0,k0,12` / `SW k1,0(k0)` — a write to **`MI_INTR_MASK` (`0xA430_000C`)**, i.e. an interrupt handler's epilogue unmasking the RCP and returning. **The mechanism:** `ERET` resolves in **EX** (`ex_stage`), where it clears `Status.EXL` and points `next_pc` at `EPC`. `dc_stage` runs its interrupt check one cycle later; `EXL` now reads 0, so `interrupt_pending()` is true and `abort_from(Stage::Dc, ..)` charges the interrupt to whatever sits in `ex_dc` — **the `ERET` itself**. `EPC` is overwritten with the `ERET`'s own address, the return address it was about to consume is destroyed, and the handler returns to the `ERET`, which resumes at itself. This is the reverse-cascade stage hazard the conventions warn about: an instruction's architectural effect becoming visible to an earlier-running stage one cycle before it retires. **Reproduced in a unit test** — `an_interrupt_across_an_eret_is_charged_to_the_target_not_the_eret` — which yields `EPC == KSEG0_PROG`, the `ERET`'s own address, exactly matching the live signature. **It is `#[ignore]`d because it reproduces an OPEN defect; no fix is shipped.** **Two false starts are recorded because each was caught by a rule rather than by luck.** (a) The first version of the test **passed vacuously**: `dc_stage` re-samples `Cause.IP2` from `bus.poll_irq()` every cycle, so seeding `IP2` on the COP0 block is erased on the next cycle and *no interrupt was ever taken* — the assertions passed on a machine that did nothing. A witness assertion (`Status.EXL` must have been set) exposed it; the fix was a test bus that actually holds the line asserted. (b) A first candidate fix additionally required `ex_dc.occupied`, which **broke `interrupt_is_not_accepted_on_the_cycle_after_a_stall`** — a test that pins an interrupt being accepted while the pipeline is still filling. Narrowing the guard to `ERET` alone then left `EPC == 0`, because after the `ERET`'s redirect the pipeline refills and the *next* cycle charges the interrupt to a **bubble** (`ex_dc.pc` is 0 in a default latch). **So a correct fix must defer past both the redirecting `ERET` and the refill bubbles** — which changes *when* interrupts are accepted, is timing-visible, and contradicts a currently-green test that only ever asserted the abort flag and never `EPC`. That is a decision about the exception model, not a patch, and it is deliberately left open here. **FIXED 2026-07-29 — `interrupt_has_an_instruction_to_charge`.** An interrupt is now deferred when `ex_dc` holds no instruction to charge it to: a **bubble** (`pc == 0` in a default latch) or an **`ERET` that has already redirected in EX**. Both exclusions are load-bearing and neither works alone — excluding only the `ERET` moves the corruption onto the refill bubble that follows its redirect (`EPC == 0`), and excluding only bubbles leaves the livelock intact. Deferring loses nothing: `Cause.IP` is a level re-sampled from the bus every cycle, so a still-asserted interrupt is taken on the next cycle that does present a real instruction — after an `ERET`, the instruction it returned to. **Oracle-validated as the decision required:** n64-systemtest Phase 1 categories remain **`Failed: 0`** and the suite-wide count remains **90** — no regression. Mutation-checked: forcing the predicate to `true` turns **both** the new test and `interrupt_is_not_accepted_on_the_cycle_after_a_stall` red. **Measured effect on the stalled titles** (240 frames): **Banjo-Tooie 1 -> 80 distinct PCs** with `VI_CTRL` going **`0x0000_0000` -> `0x0001_3006`** (it now programs the VI at all, having previously never reached that code), and **Banjo-Kazooie now scans out real geometry, `0x0` -> `570x213`**. Paper Mario, the rendering control, is unaffected except for doing more work (4,687 -> 12,218 RDP commands over the longer run) and remains fully lit. **A pinned test was corrected in the same change, and it is the reason this hid for so long:** `interrupt_is_not_accepted_on_the_cycle_after_a_stall` advanced exactly twice from a cold pipeline, so it accepted its interrupt while `ex_dc` was still a fill bubble and `EPC` was charged to address 0 — and it never noticed, because it asserted only the abort **flag** and never `EPC`. That is the *assertion a broken machine also satisfies* pattern; it now waits for a real instruction to reach DC and asserts `EPC != 0`. **OPEN, adjacent, and deliberately NOT changed: the NMI path.** `nmi_pending` reaches `abort_from(Stage::Dc, Exception::Nmi)` **without** the same `interrupt_has_an_instruction_to_charge` guard, so an NMI arriving while `ex_dc` holds a redirecting `ERET` or a fill bubble would corrupt `ErrorEPC` exactly as the interrupt path corrupted `EPC`. Raised in review and **rejected as a code change here**: NMI is documented as *"not maskable; it occurs regardless of the settings of the EXL, ERL, and the IE bits"* (UM §6.4.6), nothing in the vendored oracles exercises NMI-on-`ERET`, and deferring an NMI on the strength of an analogy would be inventing behaviour — the one thing this ledger exists to prevent. Recorded as a real adjacent risk to be closed when an oracle for it exists. **Still stuck for other reasons:** Super Mario 64, 1080 Snowboarding and Jet Force Gemini remain at 1-2 distinct PCs, and 1080 still never programs `VI_CTRL` at all — separate causes, not this one. **R-18 CAPSTONE MET — SUPER MARIO 64 RENDERS ITS TITLE SCREEN (2026-07-29).** `screenshots/super-mario-64-title.png`: Mario's head, textured cap with the M logo, shaded, over the tiled *SUPER MARIO 64* background — **125,278 RDP commands**, 138,474 of 148,125 pixels lit at 625x237, viewed and confirmed rather than counted. **The unblocking defect was in the PIF, not the RDP.** SM64 was halting in its own assert path — a `B -1` self-loop at `0x8024_6DD8`, reached by falling through `BNE t1, zero, +4` where `t1` is a byte at `0x8032_D590` that is **never written** — immediately after an `osCreateThread(..., pri=100)` call. The cause: our PIF answered the `0x00`/`0xFF` info command as a **connected controller on all four joybus channels**, so `osContInit` reported four pads on a one-pad console. `PIF-NUS.md` §*RX byte: special flags* is explicit — bit 7 (`0x80`), *"set if the handshake failed because no device appears to be connected to the joybus channel"* — and we never set it. **A second, pre-existing defect sat next to it:** `mark_no_device` wrote the flag to `resp - 1`, which for a block laid out `TX RX tt[tx_len] rr[rx_len]` with `resp = i + 2 + tx_len` is the **last TX data byte**, not the RX byte at `i + 1`; since `run_channel` returns early on `tx_len == 0` the two offsets could never coincide, so the flag had **never once** landed where software reads it — while a doc comment asserted it did. Both are fixed and separately mutation-checked, and the second test only exists because the first mutation **passed**: the empty-port guard short-circuits before `run_channel`, so nothing exercised `mark_no_device` at all until a test was written for a *connected* channel given an unsupported command. **Effect, measured:** Super Mario 64 **0 -> 125,278** RDP commands and a title screen; Banjo-Kazooie **0 -> 133,625** commands and real 3D geometry (`screenshots/banjo-kazooie-first-3d-scene.png`, kept as a known-imperfect frame — geometry, textures and depth are right, colours carry a heavy blue/yellow cast, an open combiner/texel-format issue). Oracle unchanged: n64-systemtest Phase 1 `Failed: 0`, suite-wide **90**. **A measurement trap worth recording:** a 240-frame sample reported SM64 at `scanout = 0x0` and it looked like a geometry failure — it was a **transient**, one frame in which the game had `H_VIDEO = 0` mid-reprogram of the VI. Sampling at 60/120/180/300 shows a stable 625x237 throughout. A single-point sample of a value the guest rewrites can land on the rewrite. **Still open:** Jet Force Gemini and Banjo-Tooie remain at 0 RDP commands, and 1080 Snowboarding still faults in IPL3 on an odd DMEM address — separate causes. **NEXT LEAD, measured but NOT yet the cause: `osMemSize` is never written.** The OS boot block at `0x8000_0300` comes out of `hle_boot` correctly for `osTvType` (`0x300 = 1`, NTSC) and `osRomBase` (`0x308 = 0xB000_0000`), but **`osMemSize` at `0x8000_0318` is `0x0000_0000`** — on hardware IPL3 writes the detected RDRAM size there (`0x0040_0000`, or `0x0080_0000` with an Expansion Pak). `RDRAM_SIZE` is 8 MiB in this emulator, so the memory exists; what does not happen is IPL3's sizing writing the value software reads. A game that computes a heap from `osMemSize` gets a zero-sized heap, which is a plausible route to the silent titles — several of them (Donkey Kong 64, Banjo-Tooie, Conker, Jet Force Gemini) are exactly the large-heap / Expansion-Pak titles. **It is recorded as a LEAD and not a cause, because it is measurably NOT the discriminator: Super Mario 64 renders its title screen with `osMemSize == 0` too.** Naming it as the cause on the strength of the correlation is the mistake this row has recorded four times; the next step is to find why IPL3's sizing leaves it zero (the RI/RDRAM-register model is the first suspect) rather than to seed the value, which would fabricate a number the boot is supposed to compute. **NEW DEFECT FOUND BY LOOKING AT THE REJECTED FRAMES: text renders MIRRORED.** Capturing the corpus for `screenshots/` and inspecting each frame turned up a signature no counter would have surfaced — **GoldenEye 007's "Nintendo" logo and WCW vs. nWo's "WCW World Championship Wrestling" banner both render left-right FLIPPED**. Two independent titles, different studios, same flip, so it is the **texture S-axis mirror path** and not either game. Both titles issue large command counts (GoldenEye 649,481), so submission is fine and the defect is in tile addressing — the `mirror_s` handling in the clamp/mask/mirror wrap order (R-13). **It is tile-specific, not global** — WCW/nWo Revenge, the *same publisher*, renders its THQ logo correctly oriented (committed), so the defect is in how a particular tile's `mirror_s` resolves rather than in every textured quad. Recorded but not yet fixed; it wants an Angrylion vector with `mirror_s` set rather than a guess. **A second R-13-shaped defect surfaced the same way:** Ocarina of Time renders a recognisable night sky over Hyrule Field, with a **visible rectangular texture-clamp box around the moon** — committed as a known-imperfect frame, and pointing at the clamp/border path rather than the mirror one. **Other rejected frames, also recorded rather than discarded:** Blast Corps renders garbled colour blocks and Turok a glitchy textured plane, both at high command counts, so both fail downstream of submission too. **Screenshot selection is by INSPECTION, and the census justified that rule on its own data** — Rayman 2 and Namco Museum 64 report **zero** RDP commands with **123,540** and **137,681** lit pixels; a lit-pixel-only rule would have committed two frames of pure uninitialised RDRAM as evidence of rendering. **Eleven** titles are committed as rendering correctly (Super Mario 64, Pokemon Snap, Pokemon Stadium, Mario Kart 64, Castlevania: Legacy of Darkness, Bomberman Hero, Bomberman 64, Super Smash Bros., Mario Golf, Resident Evil 2, WCW/nWo Revenge) and **three** as known-imperfect (Banjo-Kazooie's colour cast, Ocarina of Time's texture-clamp box, Paper Mario's first-frame historical record) — 14 in total; `screenshots/README.md` is the authoritative list. **This sentence first said seven and two**, written before the capture pass had finished: seven of its 31 frames landed after the contact sheets were built, and the count was not revised when the extra titles were committed. Caught in review, and worth recording as an instance of the same sampling error this row keeps catching — judging on what had finished rather than on what the run produced. Castlevania is worth singling out: it is a **legible text/UI dialog**, so it exercises a different path from the 3D frames and shows the texture and blend pipeline producing readable glyphs. **MIRRORED-TEXT DEFECT NARROWED (not yet fixed).** Two things were checked before guessing, and both came back clean: (a) the `Set Tile` (0x35) decode of **`mirror_s` is correct** — bit 8 of the low word, `(lo >> 8) & 1`, matching N64brew *…/Commands* §0x35; and (b) `mask_coupled` implements the mirror fold (`s ^ -wrap`) and its neighbour-diff seam handling. So the flip is **not** a mis-decoded tile flag. **`Texture Rectangle Flip` (0x25) is separately unimplemented in COPY mode** — `texture_rectangle_copy` bails on `flip \|\| tile.size != 2 \|\| color_image_size != 2` (R-8) — but that cannot be the explanation on its own, because a bail-out renders *nothing* and the mirrored logos **do** appear. Something is therefore applying a flip on a path that renders. The negative control matters here: WCW/nWo Revenge's THQ logo is correctly oriented while WCW vs. nWo World Tour's banner is flipped, so whatever it is, it is per-draw rather than global. **Next step is an Angrylion vector, not a patch** — a texture rectangle with `mirror_s` set and a second with the 0x25 opcode, compared byte-for-byte, which distinguishes 'we mirror when we should not' from 'we fail to mirror and the game pre-flips'. Guessing between those two without the oracle is how three earlier localisations in this row went wrong. **MIRROR HYPOTHESIS REFUTED BY THE ORACLE — `mirror_s` is CORRECT (2026-07-29).** The vector was authored rather than a patch guessed, and it settled the question against the hypothesis: `tex_tri_mirror_s_16` (V40) renders four distinct 16-bit texels with `mask_s = 2, mirror_s = 1` across **eight** columns, so the fold is unmistakable — Angrylion emits `RED GREEN BLUE WHITE` then `WHITE BLUE GREEN RED`, and **RustyN64 matches it byte-for-byte**. Eight columns are load-bearing: a four-column render samples only the forward half and mirrored and non-mirrored outputs are identical, the same converging-paths trap that let an earlier CI4 vector declare eight palette entries while exercising six. Mutation-checked by replacing the fold (`s ^ -wrap`) with a plain mask, which names the vector directly: *"vectors diverged from the Angrylion oracle: [rdp-conformance/tex_tri_mirror_s_16]"*. **So the mirrored text in GoldenEye 007 and WCW vs. nWo is NOT the S-axis mirror path**, and the earlier note recording that as the likely cause is superseded — the third suspect cleared by measurement in this investigation, after the `Set Tile` decode and `mask_coupled`. What remains: `Texture Rectangle Flip` (0x25) is unimplemented in COPY mode (R-8) but bails out rather than rendering flipped, so it cannot be it either; the live candidates are now a **negative `DsDx`** (texture coordinate stepping backwards) and the triangle edge/winding path. The vector is committed regardless of the refutation — it is permanent coverage for a path that had none, and the battery is now **54 probes (41 RDP + 13 VI)**. **THE SILENT COHORT CHARACTERISED — they HALT, each differently (2026-07-29).** Dumping what actually reaches `WB` at one-cycle granularity, the titles issuing zero RDP commands are not stalled mid-work; they have **deliberately stopped**, the same shape Super Mario 64 had before the PIF fix. **Banjo-Tooie**: `B -1` at `0x8001_36A0` with `Status = 0x0000_FF01` (`IE = 1`, `IM = 0xFF`, `EXL = 0`) and `Cause.IP = 0` — an unconditional self-loop with interrupts enabled and nothing pending, so nothing can ever release it. **Donkey Kong 64**: `BNE v1, v0, -1` at `0x8000_0A04` with **`IE = 0`** (registers given by ABI name, as elsewhere in this row; the raw encoding is `r3`/`r2`, and that they are **`v0`/`v1` — the return-value pair** — is itself a hint: the loop is comparing two returned values, so tracing what last wrote them is the way in) — a spin-poll comparing two registers with interrupts *disabled*, and the delay slot is a `NOP`, so **neither register is reloaded inside the loop**; it is unexitable by construction unless something outside changes them, which with `IE = 0` cannot be an interrupt. That is the most distinctive of the three and the best next target. **Jet Force Gemini**: `B -1` at `0x8003_F5D8` with `IE = 0` **and `Cause.IP = 0x04` (IP2, the RCP line) pending but masked**, while still servicing the occasional exception — its preamble `SD t0, 88(k0)` retires — so it is halted rather than frozen. **`osMemSize` is now doubly weakened as a lead:** seeding `RI_SELECT = 0x14` (the documented IPL3 configuration, `TSEL = 0b0001`, `RSEL = 0b0100`, N64brew *RDRAM Interface* §RI_SELECT) so IPL3 takes the RDRAM-already-up branch changed **nothing** — `osMemSize` stayed `0`, the silent titles stayed silent, and Super Mario 64 was unaffected. Refuted cheaply rather than assumed. **So the remaining work is per-title root-causing, not one shared fix**: each of these reached a halt for its own reason, exactly as SM64 reached its assert because `osContInit` miscounted controllers. The method that worked there is the method to repeat — find the branch into the halt, read what it tested, and trace that value back. | +| R-18 | A **commercial ROM boots and executes real code but does not reach video** (the Phase 5 capstone). | Recorded at the time as an un-modelled retail OS-boot runtime; the actual causes proved to be an `ERET` interrupt livelock in the CPU and a PIF that reported four controllers on a one-pad console. | absolute — a coverage boundary across subsystems, not a fitted constant or a timing interval | **Capstone met.** Super Mario 64 renders its title screen and 29 of 66 staged titles render through their own microcode. 24 titles still issue zero RDP commands — they *halt*, each for its own reason, and each is a separate root-cause hunt. **Full record: [R-18](residuals/R-18.md).** | | R-22 | The **RI register block** (`0x0470_0000..0x0470_0020`) is modelled as **plain storage**: writes stick, reads return them. Hardware read behaviour differs for at least three of the eight — N64brew *RDRAM Interface* documents `RI_CURRENT_LOAD` as intended write-only, its read returning "a collection of bits from other registers" (`RI_ERROR` Ack, `RI_MODE` STOP_R, `RI_SELECT` TSEL[0], and two bits marked TOVERIFY), while `RI_ERROR` and `RI_BANK_STATUS` reflect controller state rather than the last write | Storage is enough for the only consumer today: IPL3 reads `RI_SELECT` and branches on it. Modelling the readback quirks would mean inventing the parts the wiki itself marks TOVERIFY | absolute — a coverage boundary | **Open, deliberately.** n64-systemtest has **no RI group**, so there is no oracle in the vendored set; per measure-don't-tune these stay honest storage rather than fabricated behaviour. Decoding the block at all is what R-18 needed. The separate **RDRAM device registers** (`0x03F0_0000`) remain undecoded | | R-23 | **CIC-6105 titles do not boot through `hle_boot`** (Banjo-Tooie, Ocarina of Time, Majora's Mask, and the rest of the 4.5% 6105 share). They boot correctly through `real_pif_boot` | The 6105 IPL3 is a *different program*: it opens with a self-descrambling XOR loop — `LW t0, -0xFF0(t1)` / `LW t2, 0x44(t3)` / `XOR t2, t2, t0` / `SW t2, -0xFF0(t1)` — over `t1`/`t3` that only the **real IPL2** leaves set. `hle_boot` seeds `sp` and `s3`-`s7` but not those, so the first load faults to KSEG3 and the machine sleds exactly as R-18 did. The values are not in the wiki's IPL1/IPL2 listing, so seeding them would be inventing a constant | absolute — a coverage boundary | **RESOLVED 2026-07-29.** Closed without inventing anything: the missing registers were **measured** by running the console's real IPL1/IPL2 out of a PIF ROM dump via `real_pif_boot`, capturing the register file at IPL3's entry (`0xA400_0040`), and keeping only the values **identical across ROMs of different CIC variants** (Banjo-Tooie/6105 vs Super Mario 64/6102): `at=1`, `a2=0xA400_1F0C`, `a3=0xA400_1F08`, `t0=0xC0`, `t2=0x40`, `t3=0xA400_0000`, `s4=1`, `ra=0xA400_1550`. `v0`/`v1`/`a0`/`a1`/`t4`-`t9` are deliberately **excluded** — they carry IPL2's running checksum of that cartridge's IPL3 and differ per ROM, so freezing them would fabricate a value the boot computes. `t3` is the decisive one: 6105's IPL3 descrambles itself by reading `0x44(t3)` = DMEM + 0x40, its own image. Corroborated independently — Banjo-Tooie under HLE now halts at **`pc=0x800329a8`, the exact PC `real_pif_boot` reaches**, with retired counts within 0.3%. All four staged 6105 titles boot (Banjo-Tooie, Donkey Kong 64, Ocarina of Time, Majora's Mask), the capstone's 6105 skip is deleted, and the T-71-003 witness set **doubled from 4 titles to 8** — Ocarina of Time alone now executes 733 distinct RSP instructions and submits **17 900 RDP commands**. n64-systemtest unchanged at 90. **Superseded scope note:** detects 6105 from the cartridge header and skips those titles with a message naming this row, rather than passing quietly; the real-PIF capstone boots them and asserts on them. Closing this means either deriving the IPL2 exit state or preferring `real_pif_boot` when a PIF ROM is available | -| R-19 | **The emulator hangs on the n64-systemtest test `TLB: Execute mapped branch with a non-mapped delay slot`** — a mapped branch whose delay slot lies in a page not currently in the TLB. Both the committed **base** ROM and the `--features timing` ROM stop dead there: `started = 917`, `emux_exited = false`, no test after it ever starts, at an 8×10⁹-tick budget (~2× a normal base completion). It is a genuine loop, not slowness. | **The committed `systemtest` gate masks it**: `tests/systemtest.rs` asserts Phase-1 *category* `Failed: 0` (those results are captured before test 917) and witnesses `started > 0`, but never requires the ROM to run to `xioctl(EXIT)` — so a mid-suite hang is invisible (the failure mode engineering-lessons §2.2 warns about, one level up). **Fully traced 2026-07-24 — every architectural field is CORRECT, so the defect is NOT in the delivered exception state.** The loop oscillates between `pc = 0x1234_5000` (the non-mapped delay-slot fetch) and `pc = 0x8000_0180` (the general vector), sustaining `EXL = 1`, with: `BadVAddr = 0x1234_5000` (✓ the delay slot), `EPC = 0x1234_4FFC` (✓ the branch, `pc − 4`), `Cause.BD = 1` (✓), `Cause.ExcCode = 2` = `TLBL` (✓), `Context`/`XContext` `BadVPN2 = 0x0_91A2` (✓ `= BadVAddr >> 13`), `EntryHi` VPN2 `= 0x1234_4000` + ASID (✓), 32-bit mode (`Status.KX/SX/UX = 0`). The general vector is correct *given* `EXL = 1` (a refill with `EXL` set uses `0x180`, S-3). `ERET` clears `EXL` correctly (tested). **So `EPC`, `BD`, `Cause`, `BadVAddr`, `Context`, `XContext`, `EntryHi`, the vector, and `ERET` are all right** — the earlier "vector/EPC/EXL is off" guess is disproven. The remaining suspects are in the finer *sequencing* the trace hasn't yet caught: (a) the **`EXL = 0` first fault** (does it reach the refill vector `0x8000_0000` and n64-systemtest's *test* handler, or does the refill handler's own page-table load fault nested straight to the general/"unexpected-exception spin" handler?); and (b) whether n64-systemtest's handler **maps the page and ERETs** expecting the fetch to now hit — in which case a stale **micro-ITLB** (not refilled from the JTLB after the map) would keep the fetch missing. | absolute — a hang is a coverage boundary, not a fitted constant | **Open — fully characterised, not yet root-caused. Blocks the `timing` suite from completing (so it blocks the clean `M` measurement, C-1).** **The test + handler are now understood** (`tlb/exceptions.rs:388` + `exception_handler.rs:247`): the JALR is the last instruction of the mapped page, its delay slot is the first of the next (unmapped) page; the test runs it under `expect_exception(TLBL, -4, …)`, which sets `EXCEPTION_SKIP = -4`, so the handler resumes at `return_to = exceptpc + skip*4 = EPC − 16` — back inside the mapped block, expecting the block's own code there to escape back to the `0x80…` test. n64-systemtest asserts `exceptpc == fault_address − 4` (line 436), and **our `EPC = 0x1234_4FFC` matches that exactly** — a third confirmation the exception state is right. So the loop is not a wrong `EPC`/vector; it is that after the skip-return our CPU re-reaches the JALR and re-faults instead of escaping. **RESOLVED 2026-07-24 (root-caused by a full pipeline-latch trace, not by reasoning).** The defect was NOT in the exception state (all correct, as characterised) but in the **branch-redirect vs. exception-vector race** in `ex_stage`. Sequence: the delay-slot fetch (`0x1234_5000`) faults and the exception is dispatched at the end of the cycle, setting `next_pc = 0x8000_0180` — but the JALR is still sitting **unexecuted** in `rf_ex`. On the next active cycle the JALR reaches EX and unconditionally applied its redirect (`*next_pc = r.target`), and this JALR's target is its **own address** (`v1 = 0x1234_4FFC`), so it clobbered the vector, re-fetched itself, re-faulted its delay slot, and looped forever — exactly the two-state oscillation the latch trace showed (JALR + aborted delay slot circulating, never retiring). Fix (`pipeline.rs::resolve_branch_control`): a branch whose delay slot has aborted (its `ic_rf` latch carries `in_delay_slot` + an abort) **still writes its link** — from the architectural `pc + 8`, since `next_pc` now holds the vector — but its **redirect is suppressed**, so the exception PC wins. This is hardware-accurate: the older branch retires and links (n64-systemtest asserts `RA == fault_address + 4`) while the precise exception on the younger delay slot takes over control flow. With the fix the delay-slot test passes and **the full suite runs to `xioctl(EXIT)` for the first time (950 tests, ~30 s), so `emux_exited` is now `true`.** No regression: golden-log 0-diff, determinism, residue-invariant, and all workspace tests stay green. Completing the run unmasked a distinct pre-existing cluster the hang had hidden — see **R-20**. Discovered + traced + fixed 2026-07-24 during the Stage-C/D timing work | +| R-19 | **The emulator hung on the n64-systemtest case `TLB: Execute mapped branch with a non-mapped delay slot`** — a mapped branch whose delay slot lies in an unmapped page. | A genuine loop rather than slowness, and the committed `systemtest` gate masked it by asserting Phase-1 *category* results only. | absolute — a hang is a coverage boundary, not a fitted constant | **Resolved 2026-07-24**, discovered and traced during the Stage-C/D timing work. **Full record: [R-19](residuals/R-19.md).** | | R-20 | **64-bit addressing mode is not implemented** — the n64-systemtest `tlb64` group reports **18 failures** (14 `LW TLB Miss or Address Exception (64 bit addressing mode)` cases where `EntryHi`/`Context`/`XContext` read back `0` instead of the 64-bit VPN2, plus 4 `Loads from 32/64 bit address while using 64 bit addressing mode` returning wrong data). These tests run only in 64-bit addressing mode (`Status.KX/SX/UX = 1`) and exercise the `XKPHYS`/`XKSEG` segments and the `R` (region) field of the 64-bit `EntryHi`/`Context`/`XContext` decomposition | The emulator's segment map and TLB-miss register write-back model the **32-bit** address decomposition; the 64-bit `R:VPN2` layout (bits 63:62 region + the wider VPN2) and the wide-address segment ranges are not decoded, so a 64-bit TLB miss leaves `EntryHi`/`Context` at their reset `0`. **This cluster was masked by R-19**: the `tlb64` tests run *after* the delay-slot test that hung, so the suite never reached them — Phase 1's `Failed: 0` was only ever true *up to the hang point*, which is precisely the vacuous-pass failure mode the R-19 gate now witnesses against (`emux_exited`) | absolute — an address-decode / register-decode fact, not a timing interval | **Open — newly exposed, Stage D (CPU accuracy).** A genuine 64-bit-addressing feature gap (region-field decode + wide segment map + 64-bit miss write-back), not a regression from the R-19 fix (the fix touches only branch-delay-slot control flow). Pin against the `tlb64` group and implement the `R:VPN2` decomposition + `XKPHYS`/`XKSEG` ranges; read the expected `EntryHi`/`Context` values as a table from the suite's own assertions (do not compute against them — engineering-lessons §3.x). Surfaced 2026-07-24 the moment the suite could complete. **Progress 2026-07-24: 14 of 18 closed.** Root cause of the 14 `LW TLB Miss…(false, …)` cases was that **`EntryHi`'s VPN2/R was not written on a data address error** — the UM (§6.4.7) calls it "undefined", but the oracle pins `(VPN2 << 13) \| (R << 62)` from the faulting address, exactly as `Context`/`XContext` are already filled (which is why only `EntryHi` mismatched). Fixed by gating the `EntryHi` write on `writes_bad_vaddr` (address errors included), deleting the superseded `writes_tlb_context`, and replacing the wrong `an_address_error_leaves_entry_hi_alone` unit test with `an_address_error_writes_entry_hi_vpn2_and_region` (mutation-checked). Suite-wide 108→94. **Closed 2026-07-24 (18/18).** The last 4 were the `do_all_loads` battery (`Loads from 0x80/0xA0/0x90/0x98 … in 64-bit mode`). A focused reproduction harness (call `Pipeline::access_unaligned` directly with the four base addresses in 64-bit kernel mode, compare per-load against the ROM's `EXPECTED`) pinned the bug precisely and proved it **mode-independent**: **`mem::lwr` was unconditionally sign-extending**, but the VR4300 sign-extends `LWR` only for the **full-word** case (`byte == 3`, which writes bit 31); a **partial** `LWR` (bytes 0–2) leaves bits 63:32 of `rt` UNCHANGED. The `tlb64` battery exposes it because its sentinel's upper half (`0xBEEF_0000`) is non-zero — `LWL`, `LDL`, `LDR` all passed (they always write bit 31 or the whole register). Fixed in `mem::lwr` (sign-extend iff `byte == 3`, else preserve `rt & 0xFFFF_FFFF_0000_0000`), pinned by the mutation-checked `a_partial_lwr_preserves_rt_upper_half_and_only_the_full_word_sign_extends`. Result: **Phase 1 categories `Failed: 0` with the suite running to `xioctl(EXIT)`; suite-wide 94 → 90** (the rest are RSP/RCP/RDP, later phases). With R-20 closed, `tests/systemtest.rs` gained the `emux_exited` **completion witness** promised in R-19, so this class of mid-suite hang can never hide behind a partial Phase-1 zero again | Every entry must carry a **classification** of the failing measurement as **absolute** or diff --git a/docs/residuals/R-10.md b/docs/residuals/R-10.md new file mode 100644 index 00000000..22d5eb70 --- /dev/null +++ b/docs/residuals/R-10.md @@ -0,0 +1,24 @@ +# R-10 + +> Extracted verbatim from the `R-10` row of `../accuracy-ledger.md` on +> 2026-07-29, because the row had grown past 4,000 characters in a single +> Markdown table cell — which had already begun corrupting the table (see +> below). **Nothing was summarised, reworded or dropped**: every cell is +> reproduced as written, and the append-only rule continues to apply here — +> superseded claims stay, marked, rather than being edited away. + +## Symptom + +The colour combiner (T-33-002) models the common inputs (combined, texel0/1, primitive, shade, environment, one, zero, and the C-slot alpha taps); the **exotic inputs** — noise, LOD fraction / prim-LOD-fraction, the chroma-key centre/scale, and the convert (`K4`/`K5`) constants — are not modelled and read as **zero** + +## Suspected mechanism + +These inputs need the LOD pipeline (mip level fraction), the key/convert registers (`Set Key`/`Set Convert`), and a noise source, none of which exist yet; they appear in a small minority of combine modes. Reading them as zero is a bounded, documented gap, not a fabricated value + +## Classification + +absolute — a coverage boundary, not a fitted constant + +## Status + +**Partially resolved (2026-07-25) — see the RESOLUTION below.** The `(A − B) * C + D` arithmetic (the `special_expand` asymmetric 9-bit fold, the `+0x80`-before-`>>8` rounding, D added unscaled) and the clamp are validated bit-for-bit against hand-computed values; the 16-field decode, the input mux, and the 2-cycle chaining are unit-tested. The remaining exotic inputs land with the LOD/key/noise state and are validated against the ParaLLEl-RDP conformance vectors (T-33-005). **RESOLUTION (2026-07-25) — the register-sourced exotic inputs are wired.** `PRIM_LOD_FRAC` (RGB mul-select 14, alpha mul-select 6, extracted from `Set Prim Color` word-0 low byte; `min_level`, bits 12:8, stays deferred — it is not stored, and lands with its LOD consumer rather than as unread state) and the `Set Convert` (`0x2C`) constants `K4` (RGB sub-B select 7) and `K5` (RGB mul-select 15, both raw 9-bit from `lo[17:9]`/`lo[8:0]`) now route through the combiner instead of reading zero. Validated byte-for-byte against Angrylion by three new non-vacuous conformance vectors — `tex_tri_primlodfrac_16` (`One * prim_lod_frac`, golden `0x8421` gray vs black if unwired), `tex_tri_convert_k45_16` (`(One − K4) * K5` with bit-8-clear K4/K5, golden `0x94a5` vs black if unwired), and `tex_tri_convert_kneg_16` (a **negative** `K4 = 0x1C0` = −64, golden `0x5295` gray — correct only if the raw 448 is `special_9bit`-expanded to −64; a raw-positive read clamps black) — plus two mutation-checked unit tests (`combine_cycle_routes_prim_lod_frac`, `combine_cycle_routes_convert_k4_k5`). The sign path is faithful to Angrylion, which likewise stores K4/K5 **raw 0..511** (`rdp_set_convert`) and sign-extends in the equation: sub-B via `special_9bit_exttable` (`combiner.c:481`, reproduced bit-for-bit by RustyN64's `special_expand`) and the mul via `SIGNF(c, 9)` (RustyN64's `sext9`). **n64-systemtest impact: none** — the suite has no RDP-combiner coverage, so its failing-assertion count is unchanged (90); R-10 is validated only by the Angrylion conformance vectors. **RESOLUTION (2026-07-25) — the chroma-key combiner inputs are wired.** `Set Key GB` (`0x2A`) and `Set Key R` (`0x2B`) now decode the per-channel key **centre** and **scale** (bit-layout ported from Angrylion `rdp_set_key_gb`/`rdp_set_key_r`: GB `lo` = `centre_g[31:24] scale_g[23:16] centre_b[15:8] scale_b[7:0]`, R `lo` = `width_r[31:16] centre_r[15:8] scale_r[7:0]`), and they route through the combiner as **KeyCentre** (RGB sub-B select 6) and **KeyScale** (RGB mul-select 6) instead of reading zero — matching Angrylion `combiner.c` cases 6. The key **width** is not stored: it drives only the deferred chroma-key alpha compare, not the combiner mux, so it lands with that consumer (the `min_level` precedent). Validated by two mutation-checked unit tests — `set_key_decodes_centre_and_scale_per_channel` (distinct per-channel values pin the decode field positions) and `combine_cycle_routes_chroma_key` (`(One − centre) * scale >> 8` with per-channel centre `[32,64,96]`/scale `[64,128,192]` → `[56,96,120]`; unwiring either input changes the result) — **and byte-for-byte against Angrylion** by the end-to-end conformance vector `tex_tri_chromakey_16` (centre `[0x20,0x40,0x60]`, scale `[0x40,0x80,0xC0]` → RGBA5551 `0x3b1f`, black if unwired), which exercises the decode + mux through the real RDP pipeline. **n64-systemtest impact: none** — no RDP-combiner coverage; count unchanged (90). **Still open** (genuinely need machinery that does not exist yet, read as zero): **noise** (RGB sub-A select 7 — needs a per-pixel noise source), the **derivative-computed `lod_frac`** (RGB mul-select 13 / alpha mul-select 0 — needs the LOD/mip pipeline, pairs with R-13's mip tile selection), and the **YUV convert `K0`–`K3`** coefficients (the `Set Convert` hi word, for the YUV texture path). **RESOLUTION (2026-07-26) — the chroma-key alpha compare (`key_en`) is wired.** `Set Other Modes` bit 40 (`hi >> 8 & 1`) decodes to `OtherModes.key_en`, and `Set Key GB`/`R` now also store the per-channel `key_width` (GB `hi[23:12]/[11:0]`, R `lo[31:16]`). When `key_en`, `Rdp::combine` takes the Angrylion `combiner_1cycle` key path (gated so the common path stays byte-identical — all prior 31 `.rvec` vectors unchanged): the RGB output is the sub-A **chromabypass** colour (clamped), and the pixel alpha is `chroma_key_min` over the **pre-`>>8` 17-bit** combined colour (`combine_channel_17bit` = `((A−B)*C + (D<<8) + 0x80) & 0x1ffff`, matching `color_combiner_equation`) and the key widths — per channel `SIGN(col,17)` folded (`-k`, or `-k+0x10` when the low nibble is 8), `+ (width<<4)`, `min`-of-3, clamp `[0,0xff]`. Validated byte-for-byte against Angrylion **end-to-end** by `tex_tri_chromakey_alpha_16`, which makes the key alpha **observable** via alpha-compare (bit 0) at a `Set Blend Color` threshold of `0x80`: the Shade triangle is written only where `chroma_key_min >= 0x80` (the combine yields exactly `0x80`, so it is drawn — a `chroma_key_min` off by −1 shifts it below the threshold and the triangle vanishes, mutation-verified; clearing `key_en` outputs the combined colour instead of Shade). The `chroma_key_min` fold is **additionally** unit-tested directly with hand-computed values including the bit-16-set (negative) branch (`chroma_key_min_folds_and_takes_the_minimum`). **n64-systemtest impact: none** (no RDP-combiner coverage; count 90). **RESOLUTION (2026-07-26) — the derivative `lod_frac` input is wired** (RGB mul-select 13 / alpha mul-select 0), computed by the 2-cycle LOD ported under R-13 and validated by `tex_tri_lodfrac_16`; see R-13 for the full disposition. **Still open under R-10:** noise (un-oracled — Angrylion fakes it) and the YUV `K0`–`K3` convert diff --git a/docs/residuals/R-13.md b/docs/residuals/R-13.md new file mode 100644 index 00000000..78b63f99 --- /dev/null +++ b/docs/residuals/R-13.md @@ -0,0 +1,24 @@ +# R-13 + +> Extracted verbatim from the `R-13` row of `../accuracy-ledger.md` on +> 2026-07-29, because the row had grown past 4,000 characters in a single +> Markdown table cell — which had already begun corrupting the table (see +> below). **Nothing was summarised, reworded or dropped**: every cell is +> reproduced as written, and the append-only rule continues to apply here — +> superseded claims stay, marked, rather than being edited away. + +## Symptom + +Triangle **texturing** (T-33-004 PR-B 2b) samples the tile per pixel via `decode_texture` + `interpolate_st` + `fetch_texel`, with **both** the non-perspective path and the **perspective divide** (the 64-entry `perspective.h` reciprocal LUT + normalisation shift + out-of-bounds saturation + `w <= 0` carry, gated on `persp_tex_en`) now implemented; the tile shift/clamp/mask, the 3-point bilinear, the mask-wrap seam, the 2-cycle `texel1` (tile+1), the primitive base tile, **`mid_texel`**, the 2-cycle **`lod_frac`**, and the LOD-driven **mip tile selection** are all resolved too (see the dated RESOLUTIONs below), leaving only the **1-cycle** LOD form + +## Suspected mechanism + +The non-perspective path and the sampler wiring are the tractable first step (the flat-coordinate case is scale-independent, so a textured triangle can be validated end-to-end without the divide); the perspective LUT is a precision-critical port best hand-verified in isolation, and the triangle coordinate wrap/clamp is a combinatorial surface for the conformance fuzz + +## Classification + +absolute — real texture-path modelling gaps, oracle-confirmed + +## Status + +**RESOLVED (2026-07-23) — see the RESOLUTION at the end of this cell. Retained below is the (thrice-corrected) investigation trail. Open — a REAL divergence is pinned (after two mis-diagnoses, now settled by direct instrumentation).** The `tex_tri_16` conformance vector (committed **`#[ignore]`d**) is the first to drive `interpolate_st` against Angrylion, and its golden differs from RustyN64. Two earlier revisions of this entry were **wrong** and are retracted: (a) a `v >> 16`-vs-s10.5 coordinate claim, and (b) a "malformed vector / unconfigured tile / `SSS = 0`" claim — the latter came from reading the wrong vector's debug output (a global counter had captured an earlier *shade* triangle, not this textured one). Correctly instrumented, the vector is **well-formed**: at its own sample time tile 0 is configured (`size = 2`, `format = 0`), the S coordinate advances (`SSS = 0,1,2,3,4,5` across the drawn columns), and Angrylion **fetches texel 0 = `(255,0,0,255)` = red correctly**. The mismatch is therefore a **real** RDP behaviour RustyN64 does not yet model, from two effects seen in the reference: (1) the **1-cycle TEXEL0 pipeline** — Angrylion's `combiner_1cycle` swaps `texel0_color = texel1_color` (combiner.c) before the combine, the documented hardware quirk that a `TEXEL0` reference in 1-cycle mode is *pipelined*, so a texel0-passthrough does **not** emit the just-fetched texel; and (2) the **s10.5 texel-coordinate scale** — `SSS` spans only `0..5` in `s.5` units (< one texel), so Angrylion point-samples texel 0 across the whole triangle, whereas RustyN64's `interpolate_st` (`v >> 16`) advances one texel per pixel. RustyN64 models neither. The v2 `.rvec` **preload** plumbing is independently verified (an all-white texture renders white; Angrylion's 16-bit `tmem_formatting` loads `tmemidx0 = 0xF801`). **DEFINITIVE CORRECTION (2026-07-23, source-verified — the two "effects" above are BOTH retracted, the third mis-diagnosis of this entry).** (1) The **1-cycle TEXEL0 pipeline is NOT a net offset.** `combiner_1cycle` (`combiner.c:173`) does **not** swap texels; the swap `texel0_color = texel1_color` lives in `combiner_2cycle_cycle1` (`combiner.c:348`, 2-cycle only) and, as a *lookahead*, in `render_spans_1cycle_complete` (`rasterizer.c:336`) — but there it is net-zero: `texel1` is computed at pixel *j*'s next coordinate, then reused as pixel *j+1*'s `texel0`, so `texel0` at every pixel equals that pixel's OWN sampled texel. There is no offset to model. (2) The claim that **Angrylion "fetches red texel 0 correctly"** is **wrong**: the committed `tex_tri_16` golden is uniform `0x0001` (RGB 0 + coverage) across the whole triangle — Angrylion samples a **black** texel at *every* covered pixel, not the ramp. So the real divergence is that Angrylion's texture sample yields **zero** where RustyN64 fetches the loaded texel (`0xF801`). **The coordinate scale is RULED OUT** by a decisive probe (2026-07-23): a *constant*-coordinate variant (`dsdx = dsde = 0`, so `S = T = 0` at every pixel — sampling texel 0 = red everywhere) renders the **same** all-black `0x0001` triangle in Angrylion. If the coordinate were the divergence, a fixed `S = 0` would show red; it does not. So the fault is in the **texture load / tile / sampler configuration for the triangle 1-cycle path**, not the coordinate — Angrylion's triangle sampler returns black even for texel 0. (The copy-mode path samples the loaded texels fine, but it is a *different* sampler — a direct TMEM blit, not `texture_pipeline_cycle` — so it does not prove the triangle sampler config.) Next: isolate load-vs-sampler by driving a **COPY-mode** rectangle through the *identical* 8×1 texture + `Set Tile` + `Load Tile` (if that shows the ramp, the load is fine and the triangle tile/sampler config — `Set Tile` line/mask/format, `Set Tile Size`, and the `Set Other Modes` sample/texture bits — is the fault); then build a **correct** textured-triangle reference whose Angrylion output is verified to show the ramp, and only then compare RustyN64. Do NOT implement against the current `tex_tri_16`, whose oracle output is a degenerate all-black frame. **RECONCILIATION (later same-day, source-verified — partially walks back the "coordinate ruled out" above):** the `0x0001` black is *not* caused by the coordinate, that stands — but there ARE **two separate** issues, not one. (A) A **real RustyN64 coordinate gap IS confirmed**: `texture_pipeline_cycle` (`tex.c:182`) takes `sfrac = sss1 & 0x1f` and hands `sss1` *with its 5 fractional bits* to `fetch_texel_quadro` (`tex.c:246`), which shifts `>> 5` for the integer texel — so the RDP texel coordinate is **s.5** and RustyN64's `interpolate_st` (`v >> 16` used directly as the texel index, no `>> 5`) advances 32× too fast. The original "s10.5" note was right about this; only the *tex_tri black* is unrelated to it. (B) The **black itself** is that Angrylion's texel-0 fetch returns zero even at `SSS = 0` (texel 0) with the combiner (`add_rgb1 = 1 = texel0`, `combiner.c:540`), tile (0, loaded), and render path (`render_spans_1cycle_notexel1`) all verified to match — so the fault is inside `fetch_texel`/`tmem.c` addressing (the load-vs-sample TMEM layout for the 8×1 tile), still open. Fix order: root-cause (B) against `fetch_texel_quadro`/`tcshift`/`tcmask` or a hand-built known-good textured vector, then implement (A)'s `>> 5` and validate. **RESOLUTION (2026-07-23):** both landed. (B) The all-black frame was a **vector bug, not a RustyN64 gap** — `Set Other Modes` had `bi_lerp0 = 0` (bit 11), which selects the RDP's **YUV colour-convert** texture path (`tex.c` `texture_pipeline_cycle`, the `!bilerp` branch: `TEX->r = t3.b + ((k*_tf·t3.g + 0x80) >> 8)`); with the convert coefficients unset that computes an RGBA texel's output from its chroma (`t3.b = 0` for red) → black. Setting `bi_lerp0 = 1` selects the normal RGBA fetch. (A) RustyN64's `interpolate_st` returned `v >> 16` as the texel index directly; the RDP coordinate is **s.5**, so the index is `(v >> 16) >> 5` — added the `>> 5`. With both, the corrected `tex_tri_16` (advancing S, which the `>> 5` collapses to a solid-red triangle) and a new constant-coordinate `tex_tri_fixed_16` **both pass byte-for-byte vs Angrylion** and `tex_tri_16` is un-ignored. One self-asserted unit test (`shaded_and_textured_triangle_reads_texture_past_shade`) was corrected to the s.5 convention (its `S = 1` became `S = 32`). **TILE COORDINATE TRANSFORM RESOLVED (2026-07-25).** The triangle sampler now applies the tile **shift → tile-origin subtraction → clamp → mask/mirror** to the raw `s10.5` coordinate before `fetch_texel` (`sample_coord`, wired into `combined_color`), a bit-exact port of the ParaLLEl-RDP sampler order (`tcshift_cycle` → `TRELATIVE(SL<<3)` → `tcclamp_cycle_light` → `tcmask`): clamp is active when `clamp_s || mask_s == 0`, the over-`SH` test is against the **raw absolute** `SH` (pre-subtraction) and substitutes the **relative** width `(SH>>2)−(SL>>2)`, and it sits *before* the mask (masking first corrupts the over-max/negative detection). Validated byte-for-byte against Angrylion by two new vectors — `tex_tri_clamp_16` (a 4-texel tile, `clamp_s`, `S` running past `SH` → `R,G,B,W,W,W`) and `tex_tri_wrap_16` (`mask_s = 2` → `R,G,B,W,R,G`) — plus a mutation-checked `sample_coord` unit test (shift/subtract/clamp/mask/mirror/negative-clamp). `interpolate_st` now returns the pre-`>>5` `s10.5` coordinate so the shift and tile-size clamp operate on the true value; the self-asserted `shaded_and_textured_triangle_reads_texture_past_shade` was given a valid `SH`/`TH` (the clamp needs a tile size). **BILINEAR RESOLVED (2026-07-25).** The N64's characteristic **3-point (triangular)** filter is now modelled (`sample_type` bit 45 decoded; `bilinear_3point` + `sample_axis`, wired into `sample_texel`): the base coordinate runs shift → subtract → clamp (zeroing the sub-texel fraction on clamp, per `tcclamp_cycle`) → mask, then four texels `(s,t)/(s+1,t)/(s,t+1)/(s+1,t+1)` are blended by `upper = (sfrac+tfrac) & 0x20` — the lower-left triangle uses `t0,t1,t2`, the upper-right `t3,t2,t1` with inverted fractions, each channel a `+0x10 >> 5` round (a faithful port of ParaLLEl-RDP `texture_pipeline_cycle`). Validated byte-for-byte against Angrylion by `tex_tri_bilinear_16` (an 8×8 gradient sampled at 0.5 texel/pixel in **both** axes, so R interpolates `0,2,4,6,8,10` where point sampling would step `0,0,4,4,8,8`, and pixels hit **both** triangle branches), plus mutation-checked `bilinear_3point` (both triangles, hand-computed) and `sample_axis` (fraction capture + zero-on-clamp) unit tests. **MASK-WRAP SEAM RESOLVED (2026-07-25).** The bilinear neighbour is no longer a hardcoded `+1`: `mask_coupled` ports `tcmask_coupled`'s `sdiff`/`tdiff` — `+1` normally, `0` at a wrap seam (the "duplicate the last texel" quirk), `-base` at a mirror-off period end (neighbour wraps to 0), `-1` in a mirrored half; the neighbour is `base + diff`, *not* re-masked (`is_t` uses the T `-(base & 0xff)`). Validated byte-for-byte against Angrylion by `tex_tri_bilinear_wrap_16` (a 2-texel `mask_s = 1` tile whose `S = 1.5` seam column blends green+red — the wrapped texel 0 — not green+black; non-vacuous because the wrapped texel differs from the pre-fix unloaded read) plus a mutation-checked `mask_coupled` unit test. **2-CYCLE `texel1` RESOLVED (2026-07-25).** 2-cycle mode now samples a **second texel from `tile+1`** at the same coordinate (`combined_color`), and `combine` swaps `texel0`/`texel1` before cycle 1 (`combiner_2cycle_cycle1`), so cycle 1's `TEXEL0` reads `tile+1`. Validated byte-for-byte against Angrylion by `tex_tri_2cycle_16` (two 1-texel tiles red/green, both cycles output `TEXEL0` → the pixel is green because the swap brings `tile+1` into cycle 1; red without the swap) plus a mutation-checked `combine_two_cycle_swaps_texels` unit test. **PRIMITIVE BASE TILE RESOLVED (2026-07-26).** The sampler no longer hardwires tiles 0/1: `triangle_fill` decodes the command's base tile (`(hi >> 16) & 7` = bits 50:48, `ewdata[0]` in Angrylion `rasterizer.c:1887`) and threads it through `combined_color`/`depth_span`, which sample `tiles[base_tile]` (and `tiles[(base_tile + 1) & 7]` for the 2-cycle `texel1`). Validated byte-for-byte against Angrylion by `tex_tri_base_tile_16`: the ramp is loaded into **tile 3** at a non-zero TMEM word and the triangle names tile 3, so its golden is identical to `tex_tri_16` (same picture, different tile) while the pre-fix `tiles[0]` read samples the unloaded low TMEM and renders black (mutation-witnessed: got `[00,01]` vs golden `[F8,01]`). The 2-cycle `base + 1` path is guarded by `tex_tri_2cycle_16`, which still passes. **MID-TEXEL RESOLVED (2026-07-26).** `Set Other Modes` bit 44 (`mid_texel`) now feeds `bilinear_3point`: when set and the bilinear sample lands exactly on the texel centre (`sfrac == tfrac == 0x10`), the four neighbours are averaged (`t3 + ((((t1+t2)<<6) − (t3<<7) + ((!t3+t0)<<6) + 0xc0) >> 8)`, Angrylion `tex.c` `center`/`centerrg` case) instead of the 3-point triangle pick — which, unlike the triangle pick, uses all four texels including `t0`. Validated byte-for-byte against Angrylion by `tex_tri_mid_texel_16`: the bilinear setup of `tex_tri_bilinear_16` over a **non-planar checkerboard** (a smooth gradient is planar, so its centre average equals its 3-point pick and bit 44 would be invisible — a vacuous golden), whose golden carries the midpoint value `0x8001` (R=16) that a 3-point pick of the checkerboard extremes never produces. Mutation-witnessed: disabling the centre branch turns pixel (3,3) from the golden `[80,01]` to `[F8,01]` (the 3-point extreme). Plus a mutation-checked `bilinear_3point` unit test (`t0` off the gradient plane so the centre average `98` differs from the 3-point `48`). **n64-systemtest impact: none** — the suite has no RDP texture-path coverage (nothing renders a textured triangle during a systemtest run), so its failing-assertion count is unchanged (90); mid-texel is validated only by the Angrylion conformance vector. **LOD FRACTION RESOLVED (2026-07-26).** The derivative-computed `lod_frac` is now modelled for **2-cycle** mode — a port of Angrylion `tclod_2cycle` + `lodfrac_lodtile_signals` (`tcoord.c`) down to their `lf` output. The texture setup gained the per-**Y** derivative (`DsDy`/`DtDy`/`DwDy`, block words 5/7, previously unparsed — the LOD's second delta pair uses the true vertical gradient, *not* the major-edge `de` the scanline walk steps by); `Set Prim Color`'s `min_level` (bits 12:8) and the triangle command's `level[2:0]` (`max_level`, bits 53:51) are now stored; and `Set Other Modes` `tex_lod_en`/`sharpen_tex_en`/`detail_tex_en` (bits 48/49/50) are decoded. The LOD is the larger of the `+dsdx` and `+dsdy` coordinate deltas (each through the same perspective divide as the pixel's own coordinate, `lod_delta` = `tclod_4x17_to_15`'s magnitude fold), then `lod_frac_of` maps it to the raw 9-bit fraction — sign-extended downstream by `sext9`, matching how `K4`/`K5` are stored. It reaches the combiner through the previously-dead **RGB mul-select 13 / alpha mul-select 0** (closing that half of R-10). Computation is gated on Angrylion's `dolod` (`tex_lod_en` or the combiner actually selecting `LODFrac`), so the common path is unchanged and pays nothing. Validated byte-for-byte against Angrylion by `tex_tri_lodfrac_16`: a 2-cycle textured triangle with `dx.S = 48` but `dy.S = 112` and `de = 0`, so the LOD settles at **112** and — with `level = 2`, which keeps it out of the "distant" saturation — the fraction is the real weight `((112 << 3) >> 1) & 0xff = 0xc0`, emitted as the pixel colour (golden `0xc631`). Three mutation checks: unwiring select 13 renders black; reading `de` instead of `dy` for the Y tap gives LOD 48 → `0x80`; dropping the `l_tile` shift gives `0x80`. (The first draft of this vector left `dy = 0`, which made the `de`-vs-`dy` mutation **pass** — the vector was rebuilt with `dy` dominating so the tap is genuinely pinned.) Plus hand-computed `lod_delta` and `lod_frac_of` unit tests covering the fold, the max, the saturation marker, and each `lf` branch (in-range / distant / lodclamp / magnify / sharpen / `min_level` floor). **n64-systemtest impact: none** (no RDP texture-path coverage; count stays 90). Still open: the **1-cycle** LOD form (`tclod_1cycle_current_simple` compares the `x+1` and `x+2` taps and needs the span-edge signals `endspan`/`longspan`/`midspan`/`validline` the rasteriser does not model — deferred rather than approximated with the 2-cycle formula, and it reads zero meanwhile) **MIP TILE SELECTION RESOLVED (2026-07-28).** With `tex_lod_en` (bit 48) the 2-cycle sampler now reads the mip pair the LOD selects instead of `base`/`base+1` — a port of the tile-selection tail of Angrylion `tclod_2cycle`, via `lod_mip_tiles`: a *distant* LOD pins the level to `max_level`; otherwise it is `l_tile`; the pair straddles the mip boundary (`base+level`, `base+level+1`) and collapses to one tile where there is nothing to blend toward (distant, or magnifying without `sharpen_tex_en`); `detail_tex_en` shifts both one level finer; every index wraps mod 8. `lod_frac_of` was generalised to `lod_signals`, returning all four Angrylion outputs (`frac`/`l_tile`/`magnify`/`distant`) rather than discarding three. Validated byte-for-byte against Angrylion by `tex_tri_mip_tile_16`: the LOD-112/`level = 2` setup gives `l_tile = 1`, so from base tile 0 the pair is (1, 2) and — with three 1-texel tiles holding red/green/blue and a `TEXEL0` passthrough — the pixel is **blue** (tile 2, via the cycle-1 swap); disabling the selection samples the un-selected pair and renders green, mutation-verified. Plus a hand-computed `lod_mip_tiles` unit test covering the straddle, the distant pin-and-collapse, plain-magnify collapse, the `sharpen` exception, the `detail` shift, and the mod-8 wrap. **A vector-authoring trap worth recording:** the first draft set only `bi_lerp0` (bit 11) and not **`bi_lerp1` (bit 10)**, so cycle 1's `texture_pipeline_cycle` took the YUV colour-convert path and Angrylion rendered **white** — the same class of vector bug as the original `tex_tri_16` `bi_lerp0` mistake. A 2-cycle textured vector must set both. **n64-systemtest impact: none** (no RDP texture-path coverage; count stays 90). Still open under R-13: only the **1-cycle** LOD form diff --git a/docs/residuals/R-18.md b/docs/residuals/R-18.md new file mode 100644 index 00000000..341c9152 --- /dev/null +++ b/docs/residuals/R-18.md @@ -0,0 +1,24 @@ +# R-18 + +> Extracted verbatim from the `R-18` row of `../accuracy-ledger.md` on +> 2026-07-29, because the row had grown past 4,000 characters in a single +> Markdown table cell — which had already begun corrupting the table (see +> below). **Nothing was summarised, reworded or dropped**: every cell is +> reproduced as written, and the append-only rule continues to apply here — +> superseded claims stay, marked, rather than being edited away. + +## Symptom + +A **commercial ROM boots and executes real code but does not reach video** (Phase 5 capstone). Through the retail HLE boot (`rom::hle_boot`) the game's own IPL3 runs, the CPU fetches the cartridge's instruction stream, and the PC advances through hundreds of millions of retired instructions across varied routines — but no frame is scanned out: over ~10 s of emulated time `VI_CTRL` stays 0, `VI_ORIGIN` is never set, and **no interrupt of any kind fires** (SM64 witnessed at `retired ≈ 9.4×10⁸`, all MI interrupt lines clear) + +## Suspected mechanism + +The retail OS-boot runtime the game waits on is not yet modelled. A commercial title's boot is interrupt-driven: after its OS initialises, its main loop blocks on the **VI vblank interrupt**, which the emulator only raises once the game programs `VI_CTRL`/`VI_V_INTR` — and the game does not reach that programming, indicating an earlier dependency (the **RI/RDRAM interface** registers used for RDRAM sizing, and/or the OS thread/interrupt setup). This is a cross-subsystem gap spanning the VI vblank loop, the RI registers, and the F3DEX graphics microcode — all **outside the Phase 5 cart/boot/saves boundary** (ADR 0003; the cart phase delivers PI/SI/PIF/CIC + saves, not the OS runtime) + +## Classification + +absolute — a coverage boundary across subsystems, not a fitted constant or a timing interval + +## Status + +**Open — characterised, not a regression.** The committable Phase 5 gate (n64-systemtest cart/PIF/SI, save round-trips, homebrew boot) is met; the commercial capstone is asserted at its honest achievable level — `a_commercial_rom_boots_and_executes` (local, `#[ignore]`d) proves the ROM boots and retires ≥ 10⁶ real instructions without panicking, and *reports* the lit-pixel count (0) rather than asserting it. Reaching a title frame is deferred to the VI/RI/F3DEX work of a later phase and validated then. This gap was surfaced by the capstone exactly as the plan's escalation gate intended: **ship v0.6.0 on the committable gates + an honest "boots and executes" capstone, not a faked pass or an unbounded chase.** n64-systemtest impact: none — the boot/video path has no systemtest driver; the suite-wide count is **90** (see C-32) **SUBSTANTIALLY RESOLVED 2026-07-29 — the root cause was NOT the theory above.** It was `hle_boot` never seeding **`sp`**. IPL1 sets it before handing off (N64brew *IPL2* §IPL1 listing, `0xBFC000D0`: `ORI sp, sp, 0x1FF0 # sp = 0xA4001FF0`), and `hle_boot` skips IPL1/IPL2 without standing in for it. With `sp = 0`, IPL3's opening `ADDIU sp, sp, -24` / `SW s3, 0(sp)` prologue stored to `0xFFFF_FFE8` — KSEG3, TLB-mapped, no entries — taking a TLB-refill exception to `0x8000_0000` in empty RDRAM and executing a **NOP sled to the end of memory**. That is why the game never programmed `VI_CTRL`: it never ran at all. The symptom hid perfectly behind the capstone's own metric, because a sledding machine still retires ~180 million instructions. Found by tracing the instruction stream rather than the state (`docs/engineering-lessons.md`), and by noticing `retired` was *identical across four different games* — the 'same value regardless of input' signature. With `sp` seeded (and the **RI register block** decoded so IPL3's `RI_SELECT` read is coherent), retail titles now boot into their own code: Super Mario 64 reaches `pc=0x80246ddc` with 928 KiB of RDRAM populated, Star Fox 64 submits **122** RDP commands, World Driver Championship **45**, and Super Mario 64, Star Fox 64 and World Driver Championship upload graphics microcode into IMEM. **"Lit pixels" has been RETIRED as evidence — it never meant what earlier revisions of this row implied.** It counts non-black scanned-out pixels, and *uninitialised RDRAM is non-black*. Rogue Squadron and Jet Force Gemini score 68 527 and 69 479 of 75 840 (90-92%) with 4 790 and 6 203 distinct colours, which sounds like a picture; **rendered to PNG and looked at, both are pure noise** — RDRAM garbage scanned out, no rendered content whatever, and neither title even runs microcode. This is the `retired > 1_000_000` failure again: a metric a broken machine satisfies exactly as easily as a working one, cited for weeks because nobody opened the image. Only two things are evidence of video now: **(a)** a byte-comparison against a committed golden frame, and **(b)** a human or oracle actually viewing the output. The capstone still *reports* the count, but as a diagnostic, never as a pass condition. **Where the video gap actually is — localised, and NOT where an earlier revision said.** That revision blamed the "RDP → VI presentation path"; **wrong**. Ocarina of Time's framebuffer at `VI_ORIGIN` is uniformly `0x0001` — RGBA5551 with R=G=B=0 and the coverage bit set — so **the VI is faithfully presenting a genuinely black buffer** and `scanout` is correct. Nor is submission at fault: a DPC-seam opcode census over 300 frames shows the real F3DEX stream arriving — **7,412 `TRIANGLE` (0x0F: shade + texture + Z)**, 1,630 `TEXTURE_RECTANGLE`, 4,374 `LOAD_BLOCK`, 7,705 `SET_TILE`, 1,589 `SET_COMBINE`, of 74,508 commands. So geometry, textures and tile state all reach the RDP, and the frame still ends as the clear colour: **the RDP rasterises real geometry to black**. That is the gap. **Narrowed again, by inspecting the RDP state Ocarina actually leaves.** The **Z path is refuted**: `z_compare_en = false` and `z_update_en = false`, so nothing is depth-rejected (`SET_Z_IMAGE` *is* issued, `z_image = 0x12c700`, but the compare is off). The **combiner is refuted as a *collapse***, and is in fact the key evidence: `cyc1` decodes to `rgb_a=15, rgb_b=15, rgb_c=31, rgb_d=1` = `(0 - 0) x 0 + TEXEL0`, a pure **texture pass-through** — so the pixel colour *is* the texel, and a black frame means **the texel fetch resolves to 0**. The tiles are `fmt=2, size=0` = **CI4**, 4-bit colour-indexed textures resolved through a **TLUT**. Both TMEM halves are populated after 4,374 `LOAD_BLOCK`s — 1,508/2,048 non-zero bytes in the texture half, 760/2,048 in the TLUT half — and TLUT entry 0 reads `0x0000` (black), with entries alternating `0000 ffff 0000 ffff` at the 8-byte stride `tlut_lookup` uses. So the remaining gap is the **CI4 + TLUT texel path**: real indices and a real palette are present, and the resolution yields black. **That probe has been run. Its result is PROVISIONAL and is recorded as such.** A CI4-with-TLUT vector was authored (eight indices 0..7 against eight distinct non-zero palette entries) and replayed: RustyN64 produced `f801 07c1 003f ffff ffc1 07ff` — red, green, blue, white, yellow, cyan, exactly the authored TLUT — while **Angrylion produced mostly `0x0001`** from the same command list. Angrylion is the oracle, so the disagreement means the authored **`Load Tlut` encoding is wrong**, not that our decoder is right: Angrylion loaded a near-empty palette and we were more permissive about where the entries came from. The vector was therefore **deliberately not committed** — committing that golden would pin an authoring error as the spec. **What this does and does not establish:** it shows the CI4 index→palette mapping resolves against whatever TMEM *we* loaded, which makes a totally-dead CI4 decoder unlikely; it does **not** establish that the load path is correct, because the probe never exercised a verified `Load Tlut`. So CI4 is *weakly* de-prioritised as R-18's cause, not eliminated. **A separate, independently real defect surfaced:** `Set Other Modes.tlut_en` — **bit 47**, N64brew *Reality Display Processor/Commands* §0x2F, *"tlut_en: Enables Texture Look-Up Table (TLUT) sampling"*, with `tlut_type` at bit 46 selecting RGBA16 vs IA16 — is **not decoded at all**. Our TLUT lookup was driven purely by the tile's *format* field, so a CI tile with `tlut_en` clear still got a palette lookup and a non-CI tile with it set did not: wrong in both directions. **FIXED 2026-07-29.** `tlut_en` (bit 47) and `tlut_type` (bit 46) are now decoded, and the colour-index lookup is gated on `tlut_en` rather than on the format. The oracle settled the `tlut_en = 0` behaviour rather than it being guessed: `ci4_tlut_disabled_16` is byte-identical to `tex_tri_ci4_tlut_16` apart from that single bit, and the two goldens are **the full palette versus all black** — so an un-TLUT'd CI tile renders black. That is reproduced as the observed result, *not* as a mechanism claim: §0x2F does not document what the hardware does with un-TLUT'd index data, so no reinterpretation of the index bits is invented. `tlut_type`'s **IA16** palettes remain deferred (the lookup assumes RGBA16) — decoded so the flag is no longer silently ignored, but unimplemented until a vector exists. Battery is 53 probes (40 RDP + 13 VI) — the authoritative current count, matching `docs/STATUS.md`; the **38/51 and 39/52 figures earlier in this row are dated measurements, correct when written and kept under the append-only rule**, not competing claims about the present; mutation-checked by removing the gate. **Only one of the two directions is fixed:** a CI tile with `tlut_en` clear is no longer palette-mapped, but 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 prose would be inventing behaviour, so it stays wrong-but-recorded until a vector defines it. **SETTLED 2026-07-29.** The `Load Tlut` encoding was verified against N64brew *…/Commands* §0x30 and the first attempt had **two** errors: `lower_right.s` is command bits **23:12** (`lo >> 12`, so an 8-entry palette is `(8-1) << 2 = 0x1C` shifted by 12 — I had shifted by 14, decoding as 29 entries), and 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 correctly, Angrylion now renders the intended palette — `f801 07c1 003f ffff ffc1 07ff` — and **RustyN64 matches it byte-for-byte**. The vector `tex_tri_ci4_tlut_16` is committed (38 RDP vectors, 51 battery probes) and mutation-checked: changing `tlut_lookup`'s stride from the quadrupled 8 to 4 turns it red at pixel (3,2). So the **CI4 + TLUT path is now genuinely ELIMINATED** as R-18's cause — not weakly de-prioritised — and the colour-indexed path finally has oracle coverage it never had. **`tlut_en` remained undecoded** at the time of writing (above) — a real defect but demonstrably not this one; it was **FIXED 2026-07-29** in #185, recorded later in this row, and this clause is retained as the historical statement under the append-only rule rather than rewritten: the vector passes because our lookup keys off the CI format, which happens to coincide with `tlut_en` being set here. **R-18's video cause is therefore still open**, and 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. **`Load Block` (0x33) has ZERO oracle coverage**, which is the sharpest of those: every committed texture vector loads through `Load Tile` (0x34), while Ocarina issues **4,374 `Load Block`s** of 74,508 commands — it is the dominant texture-load path in a live retail stream and is entirely unpinned. Reading the implementation, `load_block` *does* handle the two details §0x33 calls out — coordinates as **u12.0** (not `Load Tile`'s u10.2) and the **dxt-driven odd-line 32-bit word swap** (`line = (word * dxt) >> 11`, `swap = (line & 1) << 2`) — so a static read finds nothing wrong. **Two attempts to author a `Load Block` vector were made and both DISCARDED (not committed).** Recording the evidence so the next attempt starts from data rather than from my summary of it. Candidate `load_block_odd_line_16`: 16 distinct non-zero RGBA16 texels at `0x3000`; `Set Texture Image` `0x3D100007 0x00003000` (16-bit, width 8); load tile 7 `0x35100400 0x07000000` (16-bit, line 2 words, tmem 0); `Load Block` `0x33000000 0x0700F800` (uls=0, ult=0, lrs=15, dxt=0x800); render tile 0 `0x35100402 0x00000030` (16-bit, line 2 words, tmem word 2, mask_s=3). **Observed:** Angrylion's row 7 came back `003f 0001 0001 0001 f83f 8421 ffc1 07ff` — containing `0001` fill (unwritten TMEM) interleaved with line-0 texels — and at pixel (0,1) Angrylion gave `003f` where RustyN64 gave `0841` (line 1's first texel). **What that establishes:** the golden is unusable as an oracle, because part of it reflects TMEM the load never wrote. **What it does NOT establish:** *why*. The plausible **hypothesis** is that the authored layout is wrong — tile `line`, `tmem_addr`, or the texel count's inclusivity — but that is unverified, and the divergence is therefore **not attributable to RustyN64 either**. **That question is now SETTLED, and `Load Block` finally has coverage.** §0x33's prose says *"`lower_right.s - upper_left.s` determines the number of texels"* (no `+1`) while `Load Tile` is inclusive, so the readings disagreed and prose could not decide it. A **minimal** vector could: `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 layout can confound it (precisely what sank the two discarded attempts). **Angrylion loads two.** The count is therefore **inclusive** and `load_block`'s `shi - slo + 1` is correct; RustyN64 matches the golden byte-for-byte, and the vector is committed (39 RDP vectors, 52 battery probes) and mutation-checked — dropping the `+1` turns it red. So this hypothesis joins the others as **refuted**, and `Load Block` is no longer wholly unpinned, though the coverage it now has is minimal: multi-line loads, the odd-line swap under a real `dxt`, and non-16-bit texel sizes all remain untested. **CORRECTION (same day).** This row briefly claimed "the RSP is never started — no title unhalts it, so those RDP commands come from the CPU driving the DPC directly". **That was wrong**, and wrong for an instructive reason: it was measured off `Rsp::halted` / `Rsp::pc`, two `pub` struct fields that are **never written**. The authoritative state is `SP_STATUS` (`sp.halted()` / `sp.pc()`), which is what `su_step` itself gates on. Sampling the dead fields reports "halted forever at PC 0" for a *running* RSP — the inert-API hazard of `docs/engineering-lessons.md` §3.2, and it produced two confident wrong conclusions in one session. The fields are now private with `Rsp::halted()` / `Rsp::pc()` accessors delegating to `SP_STATUS`; they are kept in the struct only because removing them would change the save-state layout (ADR 0005). Measured correctly, **retail microcode executes**: Castlevania Legacy of Darkness visits 805 distinct RSP PCs, 007 TWINE 459, Beetle Adventure Racing 356, Star Fox 64 331, Super Mario 64 236, Mega Man 64 229, World Driver Championship 148 — see T-71-003's witness, `tests/game_microcode.rs`. **Provenance for `T-71-003`'s witness parameters** (recorded because a threshold without it is a fitted constant). `MIN_DISTINCT_PCS = 32` separates two **measured** populations more than an order of magnitude apart: titles whose RSP never runs measure **0** distinct PCs (Blast Corps, Bomberman 64, Donkey Kong 64, Jet Force Gemini, Rogue Squadron), while titles whose microcode runs measure **148-815** (World Driver Championship 148, Mega Man 64 229, Super Mario 64 236, Star Fox 64 331, Beetle Adventure Racing 356, 007 TWINE 459, Castlevania Legacy of Darkness 805). `SAMPLE_TICKS = 24` is a sampling cadence, not a hardware value: it is 8 RCP steps (the RCP advances every 3 master ticks, ADR 0006), sampling can only **under**-count so every figure is a lower bound, and re-running at the finest possible cadence of 3 (8× finer) leaves the verdict unchanged — 805→815, 459→463, 356→356, 229→258, same four witnesses. `FRAMES = 90` doubled to 180 yields an **identical** witness set with unchanged counts. **Corpus evidence for the capstone's `MIN_RDRAM_NONZERO` floor** (recorded here because a threshold with no provenance is a fitted constant): a booted title leaves **787 KiB - 1.23 MiB** of RDRAM non-zero (Mega Man 64 539 KiB at the low end, World Driver Championship 1.23 MiB at the high), while a machine that faults out of IPL3 leaves **exactly 0**. The capstone's floor is therefore derived as `IPL3_COPY_BYTES / 4` (256 KiB) from the documented 1 MiB IPL3 copy, comfortably between the two populations. **R-18 remainder — diagnosed to a point, and the limits of that diagnosis stated.** Five staged titles still do not run microcode, in three groups. (1) **CIC-6103/6106** — Banjo-Kazooie, 1080 Snowboarding, F-Zero X — *do* boot and execute game code (Banjo-Kazooie reaches `pc=0x80268fcc` with 1.2 MiB of RDRAM populated) and fill IMEM to 4094 bytes, but never leave `SP_STATUS.halt` even over **600 frames / 10 s emulated**, so this is a stall, not a slow init. (2) **Blast Corps, Bomberman 64** boot but never load microcode at all (IMEM stays 0). (3) **Rogue Squadron** loads no microcode; its high lit-pixel count is **noise**, not output (see the lit-pixel retirement above), so it renders nothing at all. **Three hypotheses have now been tested and REFUTED; record them so they are not re-chased.** (a) *KSEG0 under 64-bit addressing* — the segment map returns `Direct` for `0xFFFF_FFFF_8028_4C78` in wide kernel mode, pinned by `r18_kseg0_is_direct_in_wide_kernel_mode`. (b) *"These titles sit in the exception vector"* — a **correlated** capture (armed on first entry to `0x8000_0000..0x8000_0200`, per `docs/engineering-lessons.md`) showed **no exception at all**: `ExcCode=0`, `EPC=0`, `Cause=0`. The instruction stream shows IPL3 executing `LUI t4,0x8000` / `ADDIU t4,t4,0` / `JR t4` — for **CIC-6103/6106 the game's entry point simply IS `0x8000_0000`**, so PCs like `0x8000_018c` are game code, not a vector. An earlier revision of this row read those PCs as an exception loop; that was wrong, and it was wrong because the first reading sampled `Cause`/`EPC` uncorrelated, long after the fact. (c) *R-18's original "the VI vblank interrupt never fires" theory* — F-Zero X programs the VI (`VI_CTRL=0x3102`, `VI_V_INTR=2`), and the interrupt **does** fire and reach the CPU: 1228 `MI_INTR.vi` and 3304 `Cause.IP2` samples over 120 frames with `MI_MASK=0x3f`, comparable to Super Mario 64's 1024/7219 — and SM64 runs microcode fine. The VI interrupt path is working. **What the evidence does still show:** Banjo-Kazooie alternates between game code (`0x80268fxx`) and `0x8000_0184` at *identical* sample counts, which is the signature of a tight fault-and-return loop; 1080 spins at one PC; F-Zero X spins while receiving interrupts. So the remaining cause is per-title and downstream of boot, VI and addressing — not one shared subsystem gap. **Genuinely still open:** several titles (Blast Corps, Bomberman 64, Donkey Kong 64, Jet Force Gemini) never load microcode into IMEM at all, and Rogue Squadron never starts the RSP, so their boots stall earlier than the RSP seam. n64-systemtest is unchanged at 90 (it uses the ELF load path, not IPL3). **FIRST RENDERED COMMERCIAL FRAME — 2026-07-29.** R-18's headline claim is now falsified by a picture: **Paper Mario renders real geometry through the full LLE path** (retail HLE boot -> the game's own code -> its graphics microcode on the LLE RSP -> DPC seam -> LLE RDP -> `Bus::scanout_scaled`), committed at `screenshots/paper-mario-first-commercial-frame.png`. Its colour image holds **87 distinct RGBA5551 values** (dominant `0xE739` = (28,28,28,1) at 72,156 of 76,800), and the frame is fully lit on **both** scan-out paths — **75,840 / 75,840** at 320x237 through the unscaled 1:1 `Bus::scanout` and **148,125 / 148,125** at 625x237 through the presented `Bus::scanout_scaled` (the two denominators are kept apart deliberately; quoting one beside the other path's dimensions is an error this row's first revision made) — held stably from frame 120 through 270 of a 300-frame run — flat-shaded quads with clean edge-walked slopes on a light-grey clear, **viewed, not inferred**. So the earlier conclusion that 'the RDP rasterises real geometry to black' was **title-specific, not a pipeline defect**: the pipeline produces a correct picture end-to-end. **The remaining gap is coverage, and it stratifies by title**, measured over 120 frames: Ocarina of Time 27,651 RDP commands but 98% of its colour image is `0x0001` (the black clear) and only 1,440 px lit; Majora's Mask 2,310 commands; World Driver Championship 45 commands and **exactly one distinct value** (`0x0001`) — it clears and draws nothing; Super Mario 64 and Banjo-Tooie submit **zero** commands. **Two claims in this row's opening column are now WRONG and are retained only as the historical observation** (per the immutable-reference rule, cf. R-5): `VI_CTRL` does **not** stay 0 and `VI_ORIGIN` is **not** never set — both are programmed by every title that boots (SM64 `VI_CTRL = 0x13016`, Mario Kart 64 `0x3116`, both TYPE=2), and interrupts do fire. **The noise trap was re-tested, not merely re-asserted:** on the same run Ocarina scored 62,963 lit pixels at frame 30 and, rendered and viewed, is pure uninitialised-RDRAM noise — indistinguishable from Paper Mario's real frame by pixel count alone, which is why the screenshot policy requires looking. **Measurement defect fixed in the same change:** `commercial_boot` measured through the superseded 1:1 `Bus::scanout` rather than `Bus::scanout_scaled`, the path the frontend actually presents, so every lit-pixel number this row ever quoted was from a buffer no user sees; and `Bus::scanout_scaled`'s own rustdoc still claimed it was 'not yet wired into the frontend', false since #158. **A suspicion raised and withdrawn by measurement:** the 625-wide scan-out looked like `PRESCALE_H` leaking into the width term; it is **correct** — `VI_X_SCALE = 0x200` is 0.5 in 2.10, so 320 upscales to 640, less the 8/7-px `minhpass`/`maxhpass` crop = 625. **THE NON-RENDERING TITLES ARE WAITING, NOT FAULTING — measured 2026-07-29, and it corrected two of my own claims in the same session.** With a picture now proven possible (Paper Mario), the question became why the others do not, and the `scanout_dims` field added to `commercial_boot` in **#186** split them into groups that a lit-pixel count had merged: **VI never enabled** (Banjo-Kazooie, Banjo-Tooie, 1080 Snowboarding), **VI enabled but zero RDP commands** (Conker `VI_CTRL=0x3116` w=292, Super Mario 64 `0x13016`, Jet Force Gemini `0x1311E`), and **real frame scanned but black** (007 `625x237` at `0/148125`, World Driver Championship). **CORRECTION 1 — the 'hard fault loop' was a sticky-register artefact.** Sampling `Cause.ExcCode` showed AdES on 10,575 of 12,000 samples for Banjo-Kazooie, which reads as a permanent fault loop and was reported as one. `Cause` is **sticky** — it survives the handler returning — so counting *transitions* instead of samples gives the real number: **exactly 1** address-error transition over **1,332,906,106** retired instructions (first at 14,432,774), and **exactly 1** for 1080 over **1,995,894,174** retired (first at 7,724,174). One fault, then over a billion instructions of spinning. The fault is **not** what stops these titles, and is very likely handled normally. This is the same failure mode as `retired > 1_000_000` and 'lit pixels': a number that a broken machine and a working one both produce. **CORRECTION 2 — our AdES is CORRECT in both cases; the emulator is not at fault where I first looked.** Banjo-Kazooie faults on `SD t0, 88(k0)` at `epc=0xFFFF_FFFF_8026_8FC0` (`k0`/`k1` = libultra's exception preamble saving thread context) to `0xFFFF_FFFF_8028_4C78` — 8-byte aligned and correctly sign-extended, so neither documented AdES cause applies. `Status = 0x6D016CAA` gives **`KSU = 1` (Supervisor), `SX = 0`**, and **KSEG0 does not exist in Supervisor mode**, so the address-space check *must* reject it. 1080 faults at `epc=0xFFFF_FFFF_A400_02F4` (IPL3 executing from **DMEM**) on `0xFFFF_FFFF_A400_02F3` — `Status = 0x34000002`, `KSU = 0` (Kernel), and the address is simply **odd**, so AdES is again correct. **Three emulator-side hypotheses were refuted by reading the code rather than patching it:** `EXL`/`ERL` forcing Kernel regardless of `KSU` is implemented correctly (`pipeline.rs` `access_mode`); the 64-bit-operation reservation is correct (`sixty_four_bit_is_reserved` — never reserved in Kernel, whatever `KX` says); and `kernel_segment` classifies `0xFFFF_FFFF_8xxx_xxxx` as CKSEG0 through its `0xFFFF_FFFF_8000_0000..=` arm. **`CpU` exceptions are benign — established by a CONTROL, not by argument:** Jet Force Gemini raises 174 and Paper Mario, which renders correctly in the same run, raises 18. It is libultra enabling the FPU per thread on demand. **What is genuinely open:** all four non-rendering titles **spin**, and two of them (Banjo-Tooie, Jet Force Gemini) never raise an address error at all — so the shared cause is something they are *waiting* for that never arrives, not a fault. The open questions are therefore (a) why Banjo-Kazooie's `Status.KSU` is 1 when libultra runs in Kernel mode, (b) what 1080's IPL3 is storing to an odd DMEM address, and (c) **what all four are blocked on** — the interrupt/DMA-completion path is the first place to look, since a title that never gets its completion signal spins exactly like this. Note the grouping above is provisional: Jet Force Gemini *does* enable the VI, so 'blanked VI' was too coarse a bucket for it. **BANJO-TOOIE LOCALISED TO A TWO-INSTRUCTION LOOP — 2026-07-29, and a sampling artefact was caught on the way.** Banjo-Tooie's CPU **is** executing — `retired` advances by a perfectly constant **781,250 per frame** (exactly one instruction per 2 CPU cycles) — while the fetch PC never leaves a two-instruction window. **The first measurement of this was an ALIASING ARTEFACT and is recorded because the correction matters:** sampling every 400 master ticks (= ~100 instructions) reported **one** distinct PC over 60,000 samples, which reads as a hard deadlock. The sample period was commensurate with the loop, so it landed on the same instruction every time. Re-sampled at coprime periods (397, 1009) and at **step = 2 master ticks — one CPU cycle, the finest granularity there is** — the answer is stable and different: **exactly two PCs, 50/50**, `0x8003_29A8` = `JAL 0x8002_E9C0` (`0x0C00BA70`) and `0x8003_29AC` = its delay slot `OR a0, zero, zero` (`0x00002025`). No exception vector ever appears at one-cycle sampling, so **nothing is faulting** — this is not an exception loop. **The evidence, stated without a cause attached:** (1) control never reaches `0x8002_E9C0`, which **does** contain a valid function prologue (`ADDIU sp, sp, -0x38` / `SW ra, 0x24(sp)` / `SW a0, 0x38(sp)` ...), so the target is loaded and the transfer simply does not happen; (2) ~~`ra` holds the JAL's own address rather than the required `PC + 8`~~ — **RETRACTED before merge by the retirement measurement below: the JAL never executes, so `ra` was never its business and the observation carried no information**; (3) the instruction immediately before the loop, at `0x8003_29A4`, is **`ERET`** (`0x42000018`), and code after an `ERET` is normally unreachable — so execution arriving at `0x8003_29A8` at all is itself suspect; (4) the RSP is halted throughout. **Caveat kept deliberately:** `Cpu::pc` is the **fetch** PC of a five-stage pipeline (ADR 0007), not the retiring position, so 'the PC is here' means the fetch position is here. **Next step is to confirm which of the two candidates it is before touching either** — a `JAL` link-value/target defect, or an `ERET` that falls through instead of redirecting to `EPC` — because naming a cause before measuring its boundary has been wrong here four times running. **Both candidates were then WEAKENED by checking before theorising further:** the isolated behaviours are already pinned and green — `jal_links_past_the_delay_slot_and_jr_returns_there` and `eret_resumes_at_epc_and_has_no_delay_slot` (plus `a_jal_in_a_delay_slot_links_past_the_outer_target`) — and the CPU category of n64-systemtest passes at `Failed: 0`. So neither `JAL` nor `ERET` is simply broken; whatever is happening is **conditional on state these unit tests do not set up**, which is a materially different search than 'fix JAL'. **MEASURED — and the answer is that NOTHING retires at `0x8003_29A8`.** Dumping all four inter-stage latches at one-cycle granularity: the `JAL` enters `IC/RF` and is **discarded there, never reaching `RF/EX`**, while the only instruction to reach `DC/WB` in 400 cycles is the **`ERET` at `0x8003_29A4`**, **200 times** — once every two cycles, which is exactly the constant 781,250/frame retirement rate. The loop is `ERET` → fetch `0x8003_29A8` → `ERET`, and the `JAL` is never more than a fetched-and-flushed shadow. **`EPC = 0xFFFF_FFFF_8003_29A4` — the `ERET`'s own address.** With `Status = 0x0000_FF01` (`EXL = 0`, `ERL = 0`, `IE = 1`, `IM = 0xFF`), `ERET` correctly takes the `ERL = 0` path and sets `PC <- EPC`, landing back on itself. **So `ERET` is behaving exactly to specification; the defect is that `EPC` points at the `ERET` instruction.** `Cause.ExcCode` is 0 and `EXL` is clear, so **no exception is being taken in the loop** — the corruption happened once (`EPC` becomes `0x8003_29A4` at frame 12, alongside `Status` changing from `0x3400_0000` to `0x0000_FF01`) and the machine then settled into a deterministic self-loop. **The one remaining question is what wrote that `EPC`**, and there are exactly two sources: libultra's thread dispatcher writes `EPC` explicitly via `MTC0` before its `ERET` (so a thread context holding the dispatcher's own resume address would do it), or an exception was taken **on the `ERET` itself**, which sets `EPC` to the `ERET`'s address. Distinguishing those two is a single probe on the `MTC0 EPC` write path versus the exception-entry path, and it is the next step. The `CACHE`/I-cache hypothesis is **eliminated**: the fetched words match RDRAM exactly. **ROOT CAUSE FOUND AND REPRODUCED — an interrupt is charged to the `ERET`, not to the instruction it returns to (2026-07-29).** Catching the fatal `EPC` write in the act (cycle-stepping with a pipeline-history ring buffer) gives an unambiguous verdict: `EPC 0xFFFF_FFFF_8001_3678 -> 0xFFFF_FFFF_8003_29A4`, **`EXL 0->1`**, `Cause = 0x0000_0400` (`IP2`, the RCP line), **verdict: EXCEPTION ENTRY**, with the history showing `EX/DC 0x8003_29A4:ERET` -> `DC/WB 0x8003_29A4:ERET` at that exact cycle. The preceding instructions are `LUI k0,0xA430` / `ADDIU k0,k0,12` / `SW k1,0(k0)` — a write to **`MI_INTR_MASK` (`0xA430_000C`)**, i.e. an interrupt handler's epilogue unmasking the RCP and returning. **The mechanism:** `ERET` resolves in **EX** (`ex_stage`), where it clears `Status.EXL` and points `next_pc` at `EPC`. `dc_stage` runs its interrupt check one cycle later; `EXL` now reads 0, so `interrupt_pending()` is true and `abort_from(Stage::Dc, ..)` charges the interrupt to whatever sits in `ex_dc` — **the `ERET` itself**. `EPC` is overwritten with the `ERET`'s own address, the return address it was about to consume is destroyed, and the handler returns to the `ERET`, which resumes at itself. This is the reverse-cascade stage hazard the conventions warn about: an instruction's architectural effect becoming visible to an earlier-running stage one cycle before it retires. **Reproduced in a unit test** — `an_interrupt_across_an_eret_is_charged_to_the_target_not_the_eret` — which yields `EPC == KSEG0_PROG`, the `ERET`'s own address, exactly matching the live signature. **It is `#[ignore]`d because it reproduces an OPEN defect; no fix is shipped.** **Two false starts are recorded because each was caught by a rule rather than by luck.** (a) The first version of the test **passed vacuously**: `dc_stage` re-samples `Cause.IP2` from `bus.poll_irq()` every cycle, so seeding `IP2` on the COP0 block is erased on the next cycle and *no interrupt was ever taken* — the assertions passed on a machine that did nothing. A witness assertion (`Status.EXL` must have been set) exposed it; the fix was a test bus that actually holds the line asserted. (b) A first candidate fix additionally required `ex_dc.occupied`, which **broke `interrupt_is_not_accepted_on_the_cycle_after_a_stall`** — a test that pins an interrupt being accepted while the pipeline is still filling. Narrowing the guard to `ERET` alone then left `EPC == 0`, because after the `ERET`'s redirect the pipeline refills and the *next* cycle charges the interrupt to a **bubble** (`ex_dc.pc` is 0 in a default latch). **So a correct fix must defer past both the redirecting `ERET` and the refill bubbles** — which changes *when* interrupts are accepted, is timing-visible, and contradicts a currently-green test that only ever asserted the abort flag and never `EPC`. That is a decision about the exception model, not a patch, and it is deliberately left open here. **FIXED 2026-07-29 — `interrupt_has_an_instruction_to_charge`.** An interrupt is now deferred when `ex_dc` holds no instruction to charge it to: a **bubble** (`pc == 0` in a default latch) or an **`ERET` that has already redirected in EX**. Both exclusions are load-bearing and neither works alone — excluding only the `ERET` moves the corruption onto the refill bubble that follows its redirect (`EPC == 0`), and excluding only bubbles leaves the livelock intact. Deferring loses nothing: `Cause.IP` is a level re-sampled from the bus every cycle, so a still-asserted interrupt is taken on the next cycle that does present a real instruction — after an `ERET`, the instruction it returned to. **Oracle-validated as the decision required:** n64-systemtest Phase 1 categories remain **`Failed: 0`** and the suite-wide count remains **90** — no regression. Mutation-checked: forcing the predicate to `true` turns **both** the new test and `interrupt_is_not_accepted_on_the_cycle_after_a_stall` red. **Measured effect on the stalled titles** (240 frames): **Banjo-Tooie 1 -> 80 distinct PCs** with `VI_CTRL` going **`0x0000_0000` -> `0x0001_3006`** (it now programs the VI at all, having previously never reached that code), and **Banjo-Kazooie now scans out real geometry, `0x0` -> `570x213`**. Paper Mario, the rendering control, is unaffected except for doing more work (4,687 -> 12,218 RDP commands over the longer run) and remains fully lit. **A pinned test was corrected in the same change, and it is the reason this hid for so long:** `interrupt_is_not_accepted_on_the_cycle_after_a_stall` advanced exactly twice from a cold pipeline, so it accepted its interrupt while `ex_dc` was still a fill bubble and `EPC` was charged to address 0 — and it never noticed, because it asserted only the abort **flag** and never `EPC`. That is the *assertion a broken machine also satisfies* pattern; it now waits for a real instruction to reach DC and asserts `EPC != 0`. **OPEN, adjacent, and deliberately NOT changed: the NMI path.** `nmi_pending` reaches `abort_from(Stage::Dc, Exception::Nmi)` **without** the same `interrupt_has_an_instruction_to_charge` guard, so an NMI arriving while `ex_dc` holds a redirecting `ERET` or a fill bubble would corrupt `ErrorEPC` exactly as the interrupt path corrupted `EPC`. Raised in review and **rejected as a code change here**: NMI is documented as *"not maskable; it occurs regardless of the settings of the EXL, ERL, and the IE bits"* (UM §6.4.6), nothing in the vendored oracles exercises NMI-on-`ERET`, and deferring an NMI on the strength of an analogy would be inventing behaviour — the one thing this ledger exists to prevent. Recorded as a real adjacent risk to be closed when an oracle for it exists. **Still stuck for other reasons:** Super Mario 64, 1080 Snowboarding and Jet Force Gemini remain at 1-2 distinct PCs, and 1080 still never programs `VI_CTRL` at all — separate causes, not this one. **R-18 CAPSTONE MET — SUPER MARIO 64 RENDERS ITS TITLE SCREEN (2026-07-29).** `screenshots/super-mario-64-title.png`: Mario's head, textured cap with the M logo, shaded, over the tiled *SUPER MARIO 64* background — **125,278 RDP commands**, 138,474 of 148,125 pixels lit at 625x237, viewed and confirmed rather than counted. **The unblocking defect was in the PIF, not the RDP.** SM64 was halting in its own assert path — a `B -1` self-loop at `0x8024_6DD8`, reached by falling through `BNE t1, zero, +4` where `t1` is a byte at `0x8032_D590` that is **never written** — immediately after an `osCreateThread(..., pri=100)` call. The cause: our PIF answered the `0x00`/`0xFF` info command as a **connected controller on all four joybus channels**, so `osContInit` reported four pads on a one-pad console. `PIF-NUS.md` §*RX byte: special flags* is explicit — bit 7 (`0x80`), *"set if the handshake failed because no device appears to be connected to the joybus channel"* — and we never set it. **A second, pre-existing defect sat next to it:** `mark_no_device` wrote the flag to `resp - 1`, which for a block laid out `TX RX tt[tx_len] rr[rx_len]` with `resp = i + 2 + tx_len` is the **last TX data byte**, not the RX byte at `i + 1`; since `run_channel` returns early on `tx_len == 0` the two offsets could never coincide, so the flag had **never once** landed where software reads it — while a doc comment asserted it did. Both are fixed and separately mutation-checked, and the second test only exists because the first mutation **passed**: the empty-port guard short-circuits before `run_channel`, so nothing exercised `mark_no_device` at all until a test was written for a *connected* channel given an unsupported command. **Effect, measured:** Super Mario 64 **0 -> 125,278** RDP commands and a title screen; Banjo-Kazooie **0 -> 133,625** commands and real 3D geometry (`screenshots/banjo-kazooie-first-3d-scene.png`, kept as a known-imperfect frame — geometry, textures and depth are right, colours carry a heavy blue/yellow cast, an open combiner/texel-format issue). Oracle unchanged: n64-systemtest Phase 1 `Failed: 0`, suite-wide **90**. **A measurement trap worth recording:** a 240-frame sample reported SM64 at `scanout = 0x0` and it looked like a geometry failure — it was a **transient**, one frame in which the game had `H_VIDEO = 0` mid-reprogram of the VI. Sampling at 60/120/180/300 shows a stable 625x237 throughout. A single-point sample of a value the guest rewrites can land on the rewrite. **Still open:** Jet Force Gemini and Banjo-Tooie remain at 0 RDP commands, and 1080 Snowboarding still faults in IPL3 on an odd DMEM address — separate causes. **NEXT LEAD, measured but NOT yet the cause: `osMemSize` is never written.** The OS boot block at `0x8000_0300` comes out of `hle_boot` correctly for `osTvType` (`0x300 = 1`, NTSC) and `osRomBase` (`0x308 = 0xB000_0000`), but **`osMemSize` at `0x8000_0318` is `0x0000_0000`** — on hardware IPL3 writes the detected RDRAM size there (`0x0040_0000`, or `0x0080_0000` with an Expansion Pak). `RDRAM_SIZE` is 8 MiB in this emulator, so the memory exists; what does not happen is IPL3's sizing writing the value software reads. A game that computes a heap from `osMemSize` gets a zero-sized heap, which is a plausible route to the silent titles — several of them (Donkey Kong 64, Banjo-Tooie, Conker, Jet Force Gemini) are exactly the large-heap / Expansion-Pak titles. **It is recorded as a LEAD and not a cause, because it is measurably NOT the discriminator: Super Mario 64 renders its title screen with `osMemSize == 0` too.** Naming it as the cause on the strength of the correlation is the mistake this row has recorded four times; the next step is to find why IPL3's sizing leaves it zero (the RI/RDRAM-register model is the first suspect) rather than to seed the value, which would fabricate a number the boot is supposed to compute. **NEW DEFECT FOUND BY LOOKING AT THE REJECTED FRAMES: text renders MIRRORED.** Capturing the corpus for `screenshots/` and inspecting each frame turned up a signature no counter would have surfaced — **GoldenEye 007's "Nintendo" logo and WCW vs. nWo's "WCW World Championship Wrestling" banner both render left-right FLIPPED**. Two independent titles, different studios, same flip, so it is the **texture S-axis mirror path** and not either game. Both titles issue large command counts (GoldenEye 649,481), so submission is fine and the defect is in tile addressing — the `mirror_s` handling in the clamp/mask/mirror wrap order (R-13). **It is tile-specific, not global** — WCW/nWo Revenge, the *same publisher*, renders its THQ logo correctly oriented (committed), so the defect is in how a particular tile's `mirror_s` resolves rather than in every textured quad. Recorded but not yet fixed; it wants an Angrylion vector with `mirror_s` set rather than a guess. **A second R-13-shaped defect surfaced the same way:** Ocarina of Time renders a recognisable night sky over Hyrule Field, with a **visible rectangular texture-clamp box around the moon** — committed as a known-imperfect frame, and pointing at the clamp/border path rather than the mirror one. **Other rejected frames, also recorded rather than discarded:** Blast Corps renders garbled colour blocks and Turok a glitchy textured plane, both at high command counts, so both fail downstream of submission too. **Screenshot selection is by INSPECTION, and the census justified that rule on its own data** — Rayman 2 and Namco Museum 64 report **zero** RDP commands with **123,540** and **137,681** lit pixels; a lit-pixel-only rule would have committed two frames of pure uninitialised RDRAM as evidence of rendering. **Eleven** titles are committed as rendering correctly (Super Mario 64, Pokemon Snap, Pokemon Stadium, Mario Kart 64, Castlevania: Legacy of Darkness, Bomberman Hero, Bomberman 64, Super Smash Bros., Mario Golf, Resident Evil 2, WCW/nWo Revenge) and **three** as known-imperfect (Banjo-Kazooie's colour cast, Ocarina of Time's texture-clamp box, Paper Mario's first-frame historical record) — 14 in total; `screenshots/README.md` is the authoritative list. **This sentence first said seven and two**, written before the capture pass had finished: seven of its 31 frames landed after the contact sheets were built, and the count was not revised when the extra titles were committed. Caught in review, and worth recording as an instance of the same sampling error this row keeps catching — judging on what had finished rather than on what the run produced. Castlevania is worth singling out: it is a **legible text/UI dialog**, so it exercises a different path from the 3D frames and shows the texture and blend pipeline producing readable glyphs. **MIRRORED-TEXT DEFECT NARROWED (not yet fixed).** Two things were checked before guessing, and both came back clean: (a) the `Set Tile` (0x35) decode of **`mirror_s` is correct** — bit 8 of the low word, `(lo >> 8) & 1`, matching N64brew *…/Commands* §0x35; and (b) `mask_coupled` implements the mirror fold (`s ^ -wrap`) and its neighbour-diff seam handling. So the flip is **not** a mis-decoded tile flag. **`Texture Rectangle Flip` (0x25) is separately unimplemented in COPY mode** — `texture_rectangle_copy` bails on `flip || tile.size != 2 || color_image_size != 2` (R-8) — but that cannot be the explanation on its own, because a bail-out renders *nothing* and the mirrored logos **do** appear. Something is therefore applying a flip on a path that renders. The negative control matters here: WCW/nWo Revenge's THQ logo is correctly oriented while WCW vs. nWo World Tour's banner is flipped, so whatever it is, it is per-draw rather than global. **Next step is an Angrylion vector, not a patch** — a texture rectangle with `mirror_s` set and a second with the 0x25 opcode, compared byte-for-byte, which distinguishes 'we mirror when we should not' from 'we fail to mirror and the game pre-flips'. Guessing between those two without the oracle is how three earlier localisations in this row went wrong. **MIRROR HYPOTHESIS REFUTED BY THE ORACLE — `mirror_s` is CORRECT (2026-07-29).** The vector was authored rather than a patch guessed, and it settled the question against the hypothesis: `tex_tri_mirror_s_16` (V40) renders four distinct 16-bit texels with `mask_s = 2, mirror_s = 1` across **eight** columns, so the fold is unmistakable — Angrylion emits `RED GREEN BLUE WHITE` then `WHITE BLUE GREEN RED`, and **RustyN64 matches it byte-for-byte**. Eight columns are load-bearing: a four-column render samples only the forward half and mirrored and non-mirrored outputs are identical, the same converging-paths trap that let an earlier CI4 vector declare eight palette entries while exercising six. Mutation-checked by replacing the fold (`s ^ -wrap`) with a plain mask, which names the vector directly: *"vectors diverged from the Angrylion oracle: [rdp-conformance/tex_tri_mirror_s_16]"*. **So the mirrored text in GoldenEye 007 and WCW vs. nWo is NOT the S-axis mirror path**, and the earlier note recording that as the likely cause is superseded — the third suspect cleared by measurement in this investigation, after the `Set Tile` decode and `mask_coupled`. What remains: `Texture Rectangle Flip` (0x25) is unimplemented in COPY mode (R-8) but bails out rather than rendering flipped, so it cannot be it either; the live candidates are now a **negative `DsDx`** (texture coordinate stepping backwards) and the triangle edge/winding path. The vector is committed regardless of the refutation — it is permanent coverage for a path that had none, and the battery is now **54 probes (41 RDP + 13 VI)**. **THE SILENT COHORT CHARACTERISED — they HALT, each differently (2026-07-29).** Dumping what actually reaches `WB` at one-cycle granularity, the titles issuing zero RDP commands are not stalled mid-work; they have **deliberately stopped**, the same shape Super Mario 64 had before the PIF fix. **Banjo-Tooie**: `B -1` at `0x8001_36A0` with `Status = 0x0000_FF01` (`IE = 1`, `IM = 0xFF`, `EXL = 0`) and `Cause.IP = 0` — an unconditional self-loop with interrupts enabled and nothing pending, so nothing can ever release it. **Donkey Kong 64**: `BNE v1, v0, -1` at `0x8000_0A04` with **`IE = 0`** (registers given by ABI name, as elsewhere in this row; the raw encoding is `r3`/`r2`, and that they are **`v0`/`v1` — the return-value pair** — is itself a hint: the loop is comparing two returned values, so tracing what last wrote them is the way in) — a spin-poll comparing two registers with interrupts *disabled*, and the delay slot is a `NOP`, so **neither register is reloaded inside the loop**; it is unexitable by construction unless something outside changes them, which with `IE = 0` cannot be an interrupt. That is the most distinctive of the three and the best next target. **Jet Force Gemini**: `B -1` at `0x8003_F5D8` with `IE = 0` **and `Cause.IP = 0x04` (IP2, the RCP line) pending but masked**, while still servicing the occasional exception — its preamble `SD t0, 88(k0)` retires — so it is halted rather than frozen. **`osMemSize` is now doubly weakened as a lead:** seeding `RI_SELECT = 0x14` (the documented IPL3 configuration, `TSEL = 0b0001`, `RSEL = 0b0100`, N64brew *RDRAM Interface* §RI_SELECT) so IPL3 takes the RDRAM-already-up branch changed **nothing** — `osMemSize` stayed `0`, the silent titles stayed silent, and Super Mario 64 was unaffected. Refuted cheaply rather than assumed. **So the remaining work is per-title root-causing, not one shared fix**: each of these reached a halt for its own reason, exactly as SM64 reached its assert because `osContInit` miscounted controllers. The method that worked there is the method to repeat — find the branch into the halt, read what it tested, and trace that value back. diff --git a/docs/residuals/R-19.md b/docs/residuals/R-19.md new file mode 100644 index 00000000..9750d599 --- /dev/null +++ b/docs/residuals/R-19.md @@ -0,0 +1,24 @@ +# R-19 + +> Extracted verbatim from the `R-19` row of `../accuracy-ledger.md` on +> 2026-07-29, because the row had grown past 4,000 characters in a single +> Markdown table cell — which had already begun corrupting the table (see +> below). **Nothing was summarised, reworded or dropped**: every cell is +> reproduced as written, and the append-only rule continues to apply here — +> superseded claims stay, marked, rather than being edited away. + +## Symptom + +**The emulator hangs on the n64-systemtest test `TLB: Execute mapped branch with a non-mapped delay slot`** — a mapped branch whose delay slot lies in a page not currently in the TLB. Both the committed **base** ROM and the `--features timing` ROM stop dead there: `started = 917`, `emux_exited = false`, no test after it ever starts, at an 8×10⁹-tick budget (~2× a normal base completion). It is a genuine loop, not slowness. + +## Suspected mechanism + +**The committed `systemtest` gate masks it**: `tests/systemtest.rs` asserts Phase-1 *category* `Failed: 0` (those results are captured before test 917) and witnesses `started > 0`, but never requires the ROM to run to `xioctl(EXIT)` — so a mid-suite hang is invisible (the failure mode engineering-lessons §2.2 warns about, one level up). **Fully traced 2026-07-24 — every architectural field is CORRECT, so the defect is NOT in the delivered exception state.** The loop oscillates between `pc = 0x1234_5000` (the non-mapped delay-slot fetch) and `pc = 0x8000_0180` (the general vector), sustaining `EXL = 1`, with: `BadVAddr = 0x1234_5000` (✓ the delay slot), `EPC = 0x1234_4FFC` (✓ the branch, `pc − 4`), `Cause.BD = 1` (✓), `Cause.ExcCode = 2` = `TLBL` (✓), `Context`/`XContext` `BadVPN2 = 0x0_91A2` (✓ `= BadVAddr >> 13`), `EntryHi` VPN2 `= 0x1234_4000` + ASID (✓), 32-bit mode (`Status.KX/SX/UX = 0`). The general vector is correct *given* `EXL = 1` (a refill with `EXL` set uses `0x180`, S-3). `ERET` clears `EXL` correctly (tested). **So `EPC`, `BD`, `Cause`, `BadVAddr`, `Context`, `XContext`, `EntryHi`, the vector, and `ERET` are all right** — the earlier "vector/EPC/EXL is off" guess is disproven. The remaining suspects are in the finer *sequencing* the trace hasn't yet caught: (a) the **`EXL = 0` first fault** (does it reach the refill vector `0x8000_0000` and n64-systemtest's *test* handler, or does the refill handler's own page-table load fault nested straight to the general/"unexpected-exception spin" handler?); and (b) whether n64-systemtest's handler **maps the page and ERETs** expecting the fetch to now hit — in which case a stale **micro-ITLB** (not refilled from the JTLB after the map) would keep the fetch missing. + +## Classification + +absolute — a hang is a coverage boundary, not a fitted constant + +## Status + +**Open — fully characterised, not yet root-caused. Blocks the `timing` suite from completing (so it blocks the clean `M` measurement, C-1).** **The test + handler are now understood** (`tlb/exceptions.rs:388` + `exception_handler.rs:247`): the JALR is the last instruction of the mapped page, its delay slot is the first of the next (unmapped) page; the test runs it under `expect_exception(TLBL, -4, …)`, which sets `EXCEPTION_SKIP = -4`, so the handler resumes at `return_to = exceptpc + skip*4 = EPC − 16` — back inside the mapped block, expecting the block's own code there to escape back to the `0x80…` test. n64-systemtest asserts `exceptpc == fault_address − 4` (line 436), and **our `EPC = 0x1234_4FFC` matches that exactly** — a third confirmation the exception state is right. So the loop is not a wrong `EPC`/vector; it is that after the skip-return our CPU re-reaches the JALR and re-faults instead of escaping. **RESOLVED 2026-07-24 (root-caused by a full pipeline-latch trace, not by reasoning).** The defect was NOT in the exception state (all correct, as characterised) but in the **branch-redirect vs. exception-vector race** in `ex_stage`. Sequence: the delay-slot fetch (`0x1234_5000`) faults and the exception is dispatched at the end of the cycle, setting `next_pc = 0x8000_0180` — but the JALR is still sitting **unexecuted** in `rf_ex`. On the next active cycle the JALR reaches EX and unconditionally applied its redirect (`*next_pc = r.target`), and this JALR's target is its **own address** (`v1 = 0x1234_4FFC`), so it clobbered the vector, re-fetched itself, re-faulted its delay slot, and looped forever — exactly the two-state oscillation the latch trace showed (JALR + aborted delay slot circulating, never retiring). Fix (`pipeline.rs::resolve_branch_control`): a branch whose delay slot has aborted (its `ic_rf` latch carries `in_delay_slot` + an abort) **still writes its link** — from the architectural `pc + 8`, since `next_pc` now holds the vector — but its **redirect is suppressed**, so the exception PC wins. This is hardware-accurate: the older branch retires and links (n64-systemtest asserts `RA == fault_address + 4`) while the precise exception on the younger delay slot takes over control flow. With the fix the delay-slot test passes and **the full suite runs to `xioctl(EXIT)` for the first time (950 tests, ~30 s), so `emux_exited` is now `true`.** No regression: golden-log 0-diff, determinism, residue-invariant, and all workspace tests stay green. Completing the run unmasked a distinct pre-existing cluster the hang had hidden — see **R-20**. Discovered + traced + fixed 2026-07-24 during the Stage-C/D timing work diff --git a/docs/residuals/R-5.md b/docs/residuals/R-5.md new file mode 100644 index 00000000..1784c218 --- /dev/null +++ b/docs/residuals/R-5.md @@ -0,0 +1,24 @@ +# R-5 + +> Extracted verbatim from the `R-5` row of `../accuracy-ledger.md` on +> 2026-07-29, because the row had grown past 4,000 characters in a single +> Markdown table cell — which had already begun corrupting the table (see +> below). **Nothing was summarised, reworded or dropped**: every cell is +> reproduced as written, and the append-only rule continues to apply here — +> superseded claims stay, marked, rather than being edited away. + +## Symptom + +**[HISTORICAL BASELINE — SUPERSEDED by the dated RESOLUTIONs in the disposition column, which are the current normative answer. Retained per the immutable-reference rule.]** VI scan-out (T-31-004) *was* a **1:1 copy** — `VI_X_SCALE`/`VI_Y_SCALE` resampling and the AA / divot / de-dither post-filters were not applied, and the height was derived directly from `VI_V_VIDEO`'s active half-lines rather than from the scale-accumulated framebuffer walk (this is the live `Bus::scanout`; the accurate `Bus::scanout_scaled` now supersedes it — see the RESOLUTIONs) + +## Suspected mechanism + +The framebuffer→RGBA8 pixel conversion is exact and cited: the pixel *format* is selected by `VI_CTRL.TYPE[1:0]` (N64brew *Video Interface* §VI_CTRL — 2 = RGBA5551, 3 = RGBA8888), the RGBA5551 bit layout (R[15:11] G[10:6] B[5:1] A[0]) is the N64 16-bit colour format (N64brew *Reality Display Processor/Commands* §Set Color Image, texture/format enum; *Video DAC*), and the 5→8-bit widening by high-bit replication is the standard N64 convention (the value the VI DAC emits). What is **deferred**: the geometric resampling — the VI accumulates a sub-pixel step of `VI_X_SCALE`/`VI_Y_SCALE` per pixel/line (N64brew *Video Interface* §VI_X_SCALE, §VI_Y_SCALE) — and the analog post-filters `AA_MODE`/`DIVOT_ENABLE`/de-dither (§VI_CTRL), which only matter once scaled or anti-aliased content is scanned + +## Classification + +absolute — a resampling/filter geometry choice, not a timing interval + +## Status + +**Open.** Byte-exact for a 1:1, unfiltered scan of a framebuffer whose width matches `VI_WIDTH` — which is what the FILL pipeline produces and the T-31-004 unit tests pin. Scaling and the post-filters **will be** validated bit-for-bit against Angrylion via the ParaLLEl-RDP fuzz suite / VI golden frames (Sprint 3), and superseded here if they diverge — this entry stays open until then. **n64-systemtest impact: not measured** — `Bus::scanout` has no runtime driver (nothing in the run loop calls it), so it is unreachable by the suite and cannot change the count, which stands at 90 (`docs/STATUS.md` is authoritative). **PARTIALLY RESOLVED (2026-07-25) — Slice 1, the scale accumulator + geometry.** A VI oracle now exists (earlier "no VI oracle" assumption is retired): the Angrylion driver's `vdac_write` captures `vi_process_full`'s output after `n64video_update_screen`, emitting `.vivec` goldens (`crates/rustyn64-test-harness/vectors-gen/driver.c`, format = 15-u32 header + logical source + golden RGBA8). A new `Bus::scanout_scaled` reproduces the hardware geometry bit-for-bit: the **2.10 fixed-point X/Y accumulator** (`line_x = x_offs >> 10`, source index `stride*srcY + srcX`), the **NTSC 108-px horizontal overscan** (`h_start -= 108`) with the left/top clamps folding the crop into the accumulator start, the `minhpass=8`/`maxhpass=hres-7` overscan crop, the `PRESCALE 640×625` clamp, and the **truncating RGBA5551→8** conversion the VI actually uses (`(px>>8)&0xF8` — *not* `expand5`'s high-bit replication; corrects the "5→8 by replication" claim above for the VI path specifically). Validated by `vi_scale_1x_16` (1:1, the overscan makes output column 0 sample source column 8) and `vi_scale_down2x_16` (2× downscale, the accumulator steps two source pixels) — both byte-for-byte (RGB) vs Angrylion — plus mutation-checked unit tests (`scanout_scaled_geometry_and_truncating_convert`, and the conformance mutation dropping the overscan offset goes red). This slice is `aa_mode = REPLICATE` (nearest, no lerp). **Slice 2 (2026-07-25) — the 5-bit bilinear lerp.** `aa_mode != REPLICATE` with a non-zero fraction now bilinearly resamples: four texels `(sx,sy)/(sx+1,sy)/(sx,sy+1)/(sx+1,sy+1)`, vertical-lerped per column by `yfrac` then horizontal-lerped by `xfrac`, each channel `a + (((b-a)*frac + 16) >> 5)` (`vi_lerp3`, a port of Angrylion `vi_vl_lerp`; `frac` = fraction bits `[9:5]`). Validated byte-for-byte vs Angrylion by `vi_scale_bilinear_16` (2× upscale, `aa_mode = RESAMP_ONLY`, `xfrac`/`yfrac` alternating 0/0x10 — the clean 50 % blend) and `vi_scale_bilinear_odd_16` (scale `0x240`, fractions ≢ 0 mod 4 so the `+16` rounding actually flips the result — dropping the `+16` fails *this* vector while the 0x200 one stays green, because its products are all multiples of 32), plus a mutation-checked `vi_lerp3` unit test. Bilinear applies to the 16-bit path in this slice; **32-bit source bilinear followed in slice 4b** (`vi_scale_bilinear_32`, oracle-validated), so bilinear now applies to both source formats. **Slice 3 (2026-07-25) — the gamma curve.** `gamma_enable` (VI_CTRL bit 3), with `gamma_dither` (bit 2) clear, applies the sqrt gamma table to the final RGB: `gamma(v) = sqrt(v << 6) << 1` (`vi_gamma` / `vi_integer_sqrt`, a port of Angrylion `vi_gamma_init`). Validated byte-for-byte vs Angrylion by `vi_gamma_1x_16` (`VI_STATUS = 0x030A`, nearest + gamma; `gamma(0x40) = 0x80`, non-vacuous vs the raw sample) plus a mutation-checked `vi_gamma_curve` unit test. The **dithered** gamma variants (bit 2 set, cases 1 and 3) are noise-based and stay deferred. **Slice 4a (2026-07-25) — the PAL active-span geometry.** `v_sync > 550` selects the PAL branch — the horizontal overscan is 128 px not 108 and `vstartoffset` is 44 not 34. **Provenance:** these are Angrylion source constants, not fitted values — `angrylion-rdp-plus/src/core/n64video/vi.c:688` (`h_start -= ispal ? 128 : 108`, `ispal = v_sync > V_SYNC_NTSC + 25 = 550`) and `vi.c:700` (`vstartoffset = ispal ? 44 : 34`); the `vi_pal_geometry_16` golden is generated *by that Angrylion source* through `crates/rustyn64-test-harness/vectors-gen/driver.c` (`make ANGRYLION_CORE=… driver && ./driver …/tests/vectors`), so the values are traceable to the oracle's code, not only to the fixture. That code was present in `scanout_scaled` since slice 1 but unverified; it is now pinned by `vi_pal_geometry_16` (`v_sync = 625`, `h_start = 115` so PAL's −128 clamps to sample source column 13 while a mis-applied NTSC −108 would sample column 8) byte-for-byte vs Angrylion, and a mutation forcing the PAL overscan to 108 fails it. **Slice 4b (2026-07-25) — 32-bit RGBA8888 source with bilinear.** The `.vivec` harness now carries a 32-bit source framebuffer (driver `bpp` field + `rdram_put_fb32`), and `scanout_scaled` dispatches the per-pixel fetch on the format (`vi_fetch16`/`vi_fetch32`) so the bilinear + gamma path runs for both — the 16- and 32-bit paths are now one unified loop. Validated by `vi_scale_bilinear_32` (`VI_STATUS = 0x0203`, 2× upscale) byte-for-byte vs Angrylion; a mutation breaking the 32-bit stride multiply fails it. **Slice 4c (2026-07-25) — the de-dither restore filter.** `aa_mode` 0/1 now reads the real per-pixel coverage (32-bit: alpha bits 7:5 = `(px>>5)&7`), and a fully-covered pixel (`cvg == 7`) with `dither_filter_enable` (VI_CTRL bit 16) takes the **de-dither** `restore_filter32`: over the 3×3-minus-centre 8 taps, each channel is nudged ±1 toward the neighbour's top-5-bit value (a port of Angrylion `restore.c`; the `vi_restore_table` reduces to `+1` if centre5 < neighbour5, `-1` if greater). Validated by `vi_dedither_32` (`VI_STATUS = 0x00010003`, all `cvg == 7`, 1:1 scale so no lerp) byte-for-byte vs Angrylion — non-vacuous (output col 0 = `0x1b` vs the raw `0x20`, the row-0 top taps reading 0) — with a mutation flipping the correction sign failing it. **Slice 4d (2026-07-25) — the AA edge filter.** A partial-coverage pixel (`cvg < 7`) now takes `video_filter32` (Angrylion `video.c`): it gathers the fully-covered pixels among its 6 taps (the up/down diagonals and the two-away left/right), takes the per-channel penultimate min/max (`vi_video_max`, the exact single-pass runner-up algorithm, ported verbatim for its tie-handling), and pulls the centre toward their midpoint — `centre + (((penmin + penmax − 2·centre)·(7 − cvg)) + 4 >> 3)` masked to 8 bits (unsigned two's-complement wrap). Validated by `vi_aa_edge_32` (`VI_STATUS = 0x00000003`, 32-bit with every 4th column partial so each partial pixel's taps are fully covered; the partial pixels are a fixed dark colour, *not* the smooth gradient's local midpoint, so the filter changes them at INTERIOR pixels too rather than only where the top-boundary taps break the gradient symmetry — a CodeRabbit catch) byte-for-byte vs Angrylion — with both a blend-coefficient mutation and a skip-the-filter (return-raw) mutation failing it. **Slice 4e (2026-07-25) — the divot median filter.** With `divot_enable` (VI_CTRL bit 4), a pixel whose 3 horizontal neighbours are not all fully covered takes the per-channel **median** of the post-de-dither/AA-edge values of itself and its left/right source-column neighbours (Angrylion `divot_filter`, the branch-expanded median-of-3 ported for its tie-handling); it is skipped (`(c.a & l.a & r.a) == 7`) where all three are fully covered, so it only touches partial-coverage edges. The coverage-exposing `vi_fetch32_cov` feeds it. Validated by `vi_divot_32` (`VI_STATUS = 0x00000013`, the every-4th-column-partial source) byte-for-byte vs Angrylion — non-vacuous (the partial columns and their neighbours differ from the non-divot AA-edge output, the median ≠ the AA blend), and a mutation forcing the median (removing the median-of-3) fails it. The all-fully-covered **early-return** is separately made observable (CodeRabbit #155): a **non-monotonic** fully-covered probe triplet (source columns 17/18/19 row 10, values low/high/mid per channel so the median ≠ the centre) lands on output pixel (10,10) with zero scale fraction; deleting the early-return computes the median there and fails the vector (`got [80,90,A0]` vs golden centre `[F0,E0,D0]`), where a monotonic gradient would have hidden it (median = centre). **Slice 4f (2026-07-25) — the 16-bit RGBA5551 coverage path.** The coverage machinery is now format-generic. A single primitive `Bus::vi_read_cov(x, y, bpp)` returns raw RGB8 + 3-bit coverage — 32-bit from alpha bits 7:5 (`(px>>5)&7`), **16-bit** from the 9th-bit **hidden plane** (`((px & 1) << 2) | rdram_hidden`, the pixel's bit 0 as the coverage MSB and the two hidden bits as the low bits, so `cvg == 7` needs bit 0 set **and** hidden `0b11`); every downstream filter (de-dither / AA-edge / divot) then operates on 8-bit channels, so `vi_fetch_cov`, `vi_video_filter`, and `vi_divot` gained a `bpp` argument and the 32-bit path is byte-identical (all ten prior vectors stay green). The hidden read reuses the pre-existing `Bus::rdram_hidden` plane (2 bits/halfword) that until now only served the Z-buffer `dz` path; RustyN64's default-0 borders match the oracle because the harness memsets Angrylion's default-3 plane to 0 and sets the source region explicitly. The `.vivec` format gained **version 2** (a trailing hidden-bits plane, one byte per source pixel) — the driver populates Angrylion's `rdram_hidden` via a new `extern` and emits the plane, the loader packs it into `Bus.rdram_hidden`. Validated byte-for-byte vs Angrylion by three vectors, all with the same geometry as their 32-bit twins but `type = 2`: `vi_dedither_16` (`0x00010002`, all fully covered → the 5-bit de-dither unpack), `vi_aa_edge_16` (`0x00000002`, every 4th column partial → the AA-edge over hidden-plane coverage), and `vi_divot_16` (`0x00000012`, the partial pattern + a non-monotonic fully-covered probe triplet so the divot early-return is observable). Mutation-checked: forcing coverage to 7 (ignoring the hidden read) fails `vi_aa_edge_16`+`vi_divot_16`; neutralising the de-dither nudge fails `vi_dedither_16`+`vi_dedither_32`; removing the divot bypass fails `vi_divot_16`+`vi_divot_32`. **Still deferred within `scanout_scaled` (later slices):** the **gamma-dither** variants (`aa_mode` 0/1 coverage-gated / noise-based), and the **coverage filters under `aa_mode == 2`** (RESAMP_ONLY forces `cvg = 7` on hardware so de-dither can still apply; currently gated to `aa_mode ≤ 1`), and the **field-rate half of R-6** (the 50 Hz PAL field cadence + interlace/serrate + exact H_TOTAL, which live in the `Vi::tick` scan timing, not this scan-out geometry). `scanout_scaled` is **now wired into the frontend** (2026-07-25): `emu::EmuCore::produce_frame` calls `scanout_scaled` instead of the 1:1 `Bus::scanout`, so the presented picture uses the accurate scale-resample + filters — the R-12-style land-ahead-of-caller is now complete for the live path. `Bus::scanout` is retained for the harness frame tests (`composite_frame`/`real_rom_frame`/`golden_frame`/`commercial_boot`), which migrate as R-18 needs them. **`commercial_boot` has since MIGRATED (2026-07-29)** — it now measures through `Bus::scanout_scaled`, so it is a caller of the accurate path, not the 1:1 one; the preceding clause is retained as the historical statement. The frontend's `FB_MAX 640×480` backing store bounds the presented output via two guards: `scanout_scaled` returns `(0, 0)` (→ black) when `w * h * 4` overflows `frame.rgba`, and `presentable_geometry` rejects any dimension past `FB_MAX` (in practice a tall height — `scanout_scaled`'s width is prescale-bounded to `FB_MAX_W`, but its height can reach the 625-line prescale). **n64-systemtest impact: not measured** — `scanout_scaled` now runs in the frontend's `EmuCore::produce_frame` (wired 2026-07-25), but n64-systemtest is a headless CPU/RSP oracle with no VI presentation, so it does not drive the frontend and the scan-out stays unreachable by the suite; the failing-assertion count (90) is unchanged for that reason, not measured against it diff --git a/docs/residuals/README.md b/docs/residuals/README.md new file mode 100644 index 00000000..12267b52 --- /dev/null +++ b/docs/residuals/README.md @@ -0,0 +1,44 @@ +# Residual records + +Long-form records for the accuracy residuals whose entries outgrew a single +Markdown table cell in [`../accuracy-ledger.md`](../accuracy-ledger.md). + +The ledger table remains the index: every residual **R-1 … R-23** still has a +row there with its classification and current status. Residuals whose record +passed **4,000 characters** have their full text here, linked from that row. + +## Why they were moved + +Two reasons, one of them demonstrated rather than theoretical. + +1. **The rows had begun corrupting the table.** A Markdown table row cannot + contain a hard line break, so these records grew as single lines of tens of + thousands of characters — R-18 reached **47,811**. Unescaped `|` inside + inline code spans then split the row, and `markdownlint` reported the + consequence exactly: *"MD056/table-column-count [Expected: 5; Actual: 9; Too + many cells, **extra data will be missing**]"*. Content at the **end** of the + row — which is where the newest appends land — was being dropped from the + rendered table. +2. Three reviewers raised the readability cost independently across four PRs. + +## What did NOT change + +These files are **append-only**, exactly as the ledger row was. Superseded +claims stay in place and marked (`[HISTORICAL BASELINE — SUPERSEDED …]`, +`RETRACTED`, `at the time of writing`) rather than being edited away — the +provenance is the point, and a record that only shows the conclusion cannot +show which measurement overturned which guess. + +The extraction was verified mechanically, not by eye: every one of the 20 cells +across the five moved residuals was checked to appear verbatim in its new file +before the ledger rows were replaced. + +## Index + +| Residual | Subject | +| --- | --- | +| [R-5](R-5.md) | VI scan-out — scale/resample and the AA / divot / de-dither post-filters | +| [R-10](R-10.md) | Colour-combiner exotic inputs — noise, LOD fraction, chroma key, YUV convert | +| [R-13](R-13.md) | Triangle texturing — perspective divide, bilinear sampling, LOD/mip | +| [R-18](R-18.md) | Commercial video — boot, microcode, and the road to a rendered title screen | +| [R-19](R-19.md) | n64-systemtest hang — the branch/vector race |