# LPDDR_STATUS[5] scanout underflow — structural audit + diagnostic proposal

Read-only audit (no RTL edits, no compile). Purpose: define the smallest instrumentation
that separates the three causes folded into `LPDDR_STATUS[5]`, before any RTL is written.
Instrumentation implementation is NOT yet authorized — this is the proposal for review.

## What bit5 actually is

`de25_nano_psmct32_raster_demo_top.sv:2694`:

```systemverilog
assign scan_err_w = scanout_lb_eff ? (lb_underflow_w | (lb_rd_errs_w != 32'd0))
                                   : (fc_rd_errs_w != 32'd0);
```

Bit5 is a **logical OR of two independent signals**, and one of them is itself ambiguous:

- `lb_rd_errs_w` — cumulative non-OKAY AXI read responses (real bus error). A 32-bit
  counter that already exists but is never surfaced (folded into bit5).
- `lb_underflow_w` — a **sticky** latch (`gs_lpddr_scanout_lb.sv:767-778`) set when
  `scan_y >= nf_v`, where `nf_v` is `next_fetch` 2-FF-synchronized into the video domain.
  `next_fetch` is a **multi-bit binary counter with NO gray coding** (`:765-768`).

So bit5 conflates **three** roots:

1. **Real prefetch starvation** — the AXI row FSM (`L_IDLE→L_AR→L_R→L_C`, single-beat,
   throttled to `disp_row+1`) falls behind the raster. Prime suspect: cold-start row 0
   — at `frame_start`, `next_fetch` resets and the video side begins scanning almost
   immediately (this is the frame-origin location; note it coincides with where the now-
   resolved memory transient appeared, but that was arid=1, a different master).
2. **AXI read error** — `lb_rd_errs_w != 0`, a genuine non-OKAY response.
3. **CDC false positive** — during a multi-bit `next_fetch` increment (e.g. 127→128 flips
   8 bits) the ungray-coded 2-FF sync can momentarily present a *low* `nf_v`; the sticky
   latch then sets `underflow` permanently off one transient cycle. The RTL comment even
   assumes "a 1-off transient is harmless" — but a sticky latch makes it not harmless.

## What the current evidence can and cannot say

- The FB dump / PNG is a **probe (arid=1)** read of LPDDR memory. It proves memory content,
  and says NOTHING about the **scanout/HDMI path (arid=3)** where bit5 lives.
- We have therefore never measured the actual scanout output. bit5 could be a real HDMI
  starvation, a real AXI read error, or a pure CDC status artifact — currently indistinguishable.

## Proposed instrumentation (A–D) — proposal only, not authorized

**(A) Split the OR — near-zero cost.** Surface `lb_rd_errs_w` in its own status register
(precedent: tex_rd_errs@0x068, reload_rd_errs@0x090) and give `lb_underflow` its own bit.
Immediately separates cause #2 (nonzero rd_errs ⇒ real bus error) from #1/#3
(underflow-set with rd_errs==0 ⇒ starvation-or-CDC).

**(B) First-failure latch (video domain, ~3 small regs).** On the 0→1 edge of `underflow_v`,
latch `fail_scan_y`, `fail_nf_v`, a free-running `frame_counter`, and 1 bit for which
sub-term tripped (`scan_y>=nf_v` vs the filter-lookahead term). Captures where/when/which.
`fail_scan_y==0 & fail_frame==0` ⇒ cold-start row-0 starvation; large `scan_y` with `nf_v`
momentarily small ⇒ CDC glitch.

**(C) Disambiguate #1 vs #3 without touching the CDC.** Also latch `nf_s0` and the next
cycle's `nf_v` at first failure: a one-cycle dip that self-heals = CDC transient; a persistent
`scan_y>=nf_v` = real starvation. (Definitive later fix, not now: gray-code `next_fetch`
across the sync, or require the compare to hold 2 consecutive video cycles before setting
sticky.)

**(D) HDMI-vs-memory — the key measurement.** Add a per-frame **scanout CRC**: rolling
sum32/xor32 over the in-window `r/g/b` stream, latched per frame, exposed via status
(~2×32-bit accumulators + frame latch). Compare against a golden scanout-CRC computed from
the same FB in sim. This exercises the actual HDMI-producing path independent of the probe,
and is the one measurement that separates "memory right, HDMI wrong" (real scanout/underflow)
from "memory wrong" (write path — already ruled out for this scene by the re-dump triage).

## Recommended smallest first step

(A) + (B) together are minimal, add no wide arithmetic, and already partition the three
causes for the observed persistent 0xF1. (D) is the follow-on if (A)/(B) point at the
scanout path rather than a status artifact. Selection is deferred to owner/Codex.

## Interpreting 0x120 / 0x124 on the production f52 scene (Ch442, Codex review)

The A+B diagnostic is IMPLEMENTED (Ch442, RTL + SDC + focused TB, sim-only). Read the
registers with these caveats:

- **Cold start is source row 32, NOT 0.** The SH3 DISPLAY2 profile runs `V_SOURCE_START=32`
  (DISPFB2.DBY=32) with the 15:14 vertical map, so `scan_y` begins at 32. A row-zero-miss
  therefore reads `0x124` `scan_y≈32` / `nf_v≈32` — **not** zero. (Zero only appears in
  synthetic `V_SOURCE_START=0` configs; the focused TB checks both.)
- **Both cause bits may assert.** With the vertical linear/binomial filter active, a miss can
  satisfy the base term (`scan_y>=nf_v`) AND the lookahead term (`scan_y+1>=nf_v`) at the same
  vphase, so `0x120[3]` and `0x120[4]` can both be 1. Treat them as "which rows were short,"
  not mutually exclusive.
- **One snapshot NARROWS, it does not PROVE.** A single first-failure `nf_v`/`nf_s0` pair
  distinguishes a real AXI error (`0x120[2]=1`) from an underflow (`[1]=1,[2]=0`), and locates
  the first short row — but by itself it does **not** definitively separate a sustained
  starvation from a transient `next_fetch` CDC-lag false positive. That separation needs the
  later gray-code/2-cycle-agreement work (deferred), or repeated captures across frames.

`bit5` at `0x02C` is unchanged; `0x118/0x11C` remain reserved.
