docs: Ch441 board validation + Ch437 A/B scanout evidence

Preserve the Ch441 timing/renderer board-validation evidence and the
Codex-directed follow-up:
- FB reconciliation: the Ch437 A/B dump's 8-word first-beat delta was a
  non-reproducible one-time transient; 3x render+dump and 8x pure-probe
  re-reads all return golden d0047677 (words 0-7 correct). Memory integrity
  confirmed deterministic. (fb_reconciliation.md)
- Structural audit + A-D diagnostic proposal for LPDDR_STATUS[5]: bit5 ORs
  sticky CDC-synced underflow with AXI read-errors; dump path (arid=1) is
  separate from scanout/HDMI (arid=3). Instrumentation proposal only, not
  implemented. (scanout_status_bit5_proposal.md)
- Raw board logs, status samples, RBF hashes, A/B FB dump, board PNG.

No RTL edits, no compile, no builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-21 20:34:20 -04:00
parent 96d23ea8ee
commit 2123e646c8
14 changed files with 316826 additions and 0 deletions
@@ -0,0 +1,8 @@
55b23b94f51f1aab271d91aa4fa766561c044b9633146f25f4242b431f6ce902 retroDE_ps2.ch435.core.rbf
3dd624cd34e56cf553fc92c47c3a0d47dc6a9c0513f37cbf72f2c4f6f9c71559 retroDE_ps2.ch437.candidate.rbf
9463602dd40f224330de125a68f96d377c28210d3f7dcce3e9adf775e548db5f retroDE_ps2.ch441.core.rbf
56e45346338907eb6c307320757774ac65efa56dabb644906a5600f5ea73a982 retroDE_ps2.core.rbf
6fd81615af95cf51e1a893e04ca34fa1bad7a69c933efcb7fca5c9c091e18a65 retroDE_ps2.pre_ch417b.core.rbf
17d1c1b49aac4d893ed56285cf0703bf375db2ee9f472ea36862cca376ae63fe retroDE_ps2.pre_ch435.core.rbf
55b23b94f51f1aab271d91aa4fa766561c044b9633146f25f4242b431f6ce902 retroDE_ps2.pre_ch436.core.rbf
1c8945e3e8ad85d91a682aa7a068cee487a1459a7f2b899600b24ff7d9751294 retroDE_ps2.pre_ch437.core.rbf
@@ -0,0 +1,7 @@
Ch441 board A/B scanout comparison (2026-07-21)
Candidate Ch441 = 9463602d... : replay DONE rc=0, 311 drains, zero drops, FB SHA d0047677 (byte-identical golden). LPDDR_STATUS[5] SET ~15/16 (0xF1).
Accepted Ch437 = 3dd624cd... : replay DONE rc=0, 311 drains. LPDDR_STATUS[5] SET 16/16 (0xF1) — SAME underflow.
Canonical retroDE_ps2.core.rbf = 56e45346... = Ch439b (previously REJECTED for this exact 0xF1).
VERDICT: both Ch441 and the ACCEPTED Ch437 underflow identically -> reproduced BASELINE scanout defect (LPDDR_STATUS[5]), NOT a Ch441 regression. Ch441 remains the timing-clean / renderer-correct candidate; scanout defect stays OPEN. Ch441 NOT promoted to canonical.
@@ -0,0 +1,59 @@
# Ch441 board validation — framebuffer reconciliation (zsrt139f52)
Scope: reconcile the one framebuffer-content discrepancy seen during the Ch441↔Ch437
A/B, and record the Codex-authorized zero-RTL re-dump triage that classifies it.
## The A/B FB discrepancy (as first observed)
During the Ch441-vs-Ch437 A/B, the Ch437 `--dump-fb` readback logged
`sum32=0xaf7b63f3 xor32=0x33178b8f`, differing from the accepted golden
`sum32=0xaad0b94d xor32=0x33138181`. Local word-level diff localized the entire
difference to **exactly 8 words — the first 256-bit LPDDR beat, row 0, x=0..7, y=0**
(307192/307200 = 99.997% byte-identical). In all 8 words the **alpha byte matched**;
only RGB differed (golden ≈ 0x0b dark, dump ≈ 0xb6 bright, a uniform ~+0xaa lift).
Those 8 words fully account for the logged sum32/xor32 deltas and nothing else.
Artifacts:
- `sh3_zsrt139f52_ch437_ab_fb.mem` SHA-256 `8fd30cdb926ab7a73c39827a2e3352b72c92af78ebaa7b4d69fd9715bf693aec`
- golden / prior-Ch437 / Ch441 A/B all: SHA-256 `d0047677371a0f6e4e319458926f604a8599c92baaf34d09b8e1fd452e31662b`
Key architectural fact: `--dump-fb` reads LPDDR via `gs_lpddr_rd_probe` (**arid=1**),
a different AXI master from the scanout line-buffer (`gs_lpddr_scanout_lb`, **arid=3**).
So this discrepancy lives on the probe-readback / write path, NOT the scanout/HDMI path.
## Zero-RTL re-dump triage (Codex-authorized; no RTL / Quartus / builds)
Image under test: `retroDE_ps2.ch437.candidate.rbf` (the anomaly image), CORE_ID 0x50533200.
Two independent axes, on the same f52 replay (`sh3_zsrt139f52_epochs.txt`):
1. **Write-side determinism** — three full render+dumps (A/B/C).
2. **Readback determinism** — repeated pure-probe re-reads of words 0..15 via the
`OFF_LPDDR_RDADDR` (0x03C) latch, with NO re-render, on the frame left by dump C.
### Result — anomaly did NOT reproduce
| dump | SHA-256 | sum32 | words 0..7 |
|------|---------|-------|------------|
| A (render+dump) | `d0047677…` | `0xaad0b94d` | golden (dark) |
| B (render+dump) | `d0047677…` | `0xaad0b94d` | golden (dark) |
| C (render+dump) | `d0047677…` | `0xaad0b94d` | golden (dark) |
First 16 words, identical across A/B/C:
`040c0c0b 030c0b0b 030a0909 030b0b0b 030d0b0b 040e0c0c 050f0e0e 050f0e0e 050b0a0a 050a0909 05090808 050a0a0a 050e0d0d 050d0d0c 050d0c0b 050d0b0b`
Pure-probe re-reads (8 rounds, no re-render) returned those same values every round —
words 0..7 stable and golden, no bright first beat.
### Classification
The earlier 8-word bright first-beat was a **non-reproducible one-time transient** in that
single A/B dump — not a stable property of the stored framebuffer and not a deterministic
write or readback defect. Three fresh renders + repeated probe re-reads are byte-identical
to golden across the full frame, including words 0..7. **Framebuffer-memory integrity for
this scene is confirmed deterministic (`d0047677…`).** Evidence integrity is resolved.
This triage does NOT diagnose `LPDDR_STATUS[5]`; that is the scanout/HDMI (arid=3) path,
addressed separately (see `scanout_status_bit5_proposal.md`).
### Raw logs
`sh3_zsrt139f52_ch437_redump_[ABC].log`, `redump_triage_console.log`.
@@ -0,0 +1,34 @@
=== render+dump A ===
[sched] epoch 309: zero fragment drops (snap=0, sticky=0)
[sched] epoch 310: zero fragment drops (snap=0, sticky=0)
[sched] dump-fb: wrote 307200 words -> ch437_redump_A.mem sum32=0xaad0b94d xor32=0x33138181 nonzero=245155 bounds=(0,0)..(511,479) FB=640x480
[sched] DONE rc=0 (all gates passed)
d0047677371a0f6e4e319458926f604a8599c92baaf34d09b8e1fd452e31662b ch437_redump_A.mem
first16(A):
040c0c0b 030c0b0b 030a0909 030b0b0b 030d0b0b 040e0c0c 050f0e0e 050f0e0e 050b0a0a 050a0909 05090808 050a0a0a 050e0d0d 050d0d0c 050d0c0b 050d0b0b
=== render+dump B ===
[sched] epoch 309: zero fragment drops (snap=0, sticky=0)
[sched] epoch 310: zero fragment drops (snap=0, sticky=0)
[sched] dump-fb: wrote 307200 words -> ch437_redump_B.mem sum32=0xaad0b94d xor32=0x33138181 nonzero=245155 bounds=(0,0)..(511,479) FB=640x480
[sched] DONE rc=0 (all gates passed)
d0047677371a0f6e4e319458926f604a8599c92baaf34d09b8e1fd452e31662b ch437_redump_B.mem
first16(B):
040c0c0b 030c0b0b 030a0909 030b0b0b 030d0b0b 040e0c0c 050f0e0e 050f0e0e 050b0a0a 050a0909 05090808 050a0a0a 050e0d0d 050d0d0c 050d0c0b 050d0b0b
=== render+dump C ===
[sched] epoch 309: zero fragment drops (snap=0, sticky=0)
[sched] epoch 310: zero fragment drops (snap=0, sticky=0)
[sched] dump-fb: wrote 307200 words -> ch437_redump_C.mem sum32=0xaad0b94d xor32=0x33138181 nonzero=245155 bounds=(0,0)..(511,479) FB=640x480
[sched] DONE rc=0 (all gates passed)
d0047677371a0f6e4e319458926f604a8599c92baaf34d09b8e1fd452e31662b ch437_redump_C.mem
first16(C):
040c0c0b 030c0b0b 030a0909 030b0b0b 030d0b0b 040e0c0c 050f0e0e 050f0e0e 050b0a0a 050a0909 05090808 050a0a0a 050e0d0d 050d0d0c 050d0c0b 050d0b0b
=== pure-probe re-reads of words 0-15, 8 rounds, NO re-render (frame left by dump C) ===
round0: 0x040C0C0B 0x030C0B0B 0x030A0909 0x030B0B0B 0x030D0B0B 0x040E0C0C 0x050F0E0E 0x050F0E0E 0x050B0A0A 0x050A0909 0x05090808 0x050A0A0A 0x050E0D0D 0x050D0D0C 0x050D0C0B 0x050D0B0B
round1: 0x040C0C0B 0x030C0B0B 0x030A0909 0x030B0B0B 0x030D0B0B 0x040E0C0C 0x050F0E0E 0x050F0E0E 0x050B0A0A 0x050A0909 0x05090808 0x050A0A0A 0x050E0D0D 0x050D0D0C 0x050D0C0B 0x050D0B0B
round2: 0x040C0C0B 0x030C0B0B 0x030A0909 0x030B0B0B 0x030D0B0B 0x040E0C0C 0x050F0E0E 0x050F0E0E 0x050B0A0A 0x050A0909 0x05090808 0x050A0A0A 0x050E0D0D 0x050D0D0C 0x050D0C0B 0x050D0B0B
round3: 0x040C0C0B 0x030C0B0B 0x030A0909 0x030B0B0B 0x030D0B0B 0x040E0C0C 0x050F0E0E 0x050F0E0E 0x050B0A0A 0x050A0909 0x05090808 0x050A0A0A 0x050E0D0D 0x050D0D0C 0x050D0C0B 0x050D0B0B
round4: 0x040C0C0B 0x030C0B0B 0x030A0909 0x030B0B0B 0x030D0B0B 0x040E0C0C 0x050F0E0E 0x050F0E0E 0x050B0A0A 0x050A0909 0x05090808 0x050A0A0A 0x050E0D0D 0x050D0D0C 0x050D0C0B 0x050D0B0B
round5: 0x040C0C0B 0x030C0B0B 0x030A0909 0x030B0B0B 0x030D0B0B 0x040E0C0C 0x050F0E0E 0x050F0E0E 0x050B0A0A 0x050A0909 0x05090808 0x050A0A0A 0x050E0D0D 0x050D0D0C 0x050D0C0B 0x050D0B0B
round6: 0x040C0C0B 0x030C0B0B 0x030A0909 0x030B0B0B 0x030D0B0B 0x040E0C0C 0x050F0E0E 0x050F0E0E 0x050B0A0A 0x050A0909 0x05090808 0x050A0A0A 0x050E0D0D 0x050D0D0C 0x050D0C0B 0x050D0B0B
round7: 0x040C0C0B 0x030C0B0B 0x030A0909 0x030B0B0B 0x030D0B0B 0x040E0C0C 0x050F0E0E 0x050F0E0E 0x050B0A0A 0x050A0909 0x05090808 0x050A0A0A 0x050E0D0D 0x050D0D0C 0x050D0C0B 0x050D0B0B
=== triage done ===
@@ -0,0 +1,74 @@
# 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 (AD) — 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.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,17 @@
# Ch437 LPDDR_STATUS 0x02C active-scanout sampling (video_src=1)
sample 1: 0x000000F1 bit5=32
sample 2: 0x000000F1 bit5=32
sample 3: 0x000000F1 bit5=32
sample 4: 0x000000F1 bit5=32
sample 5: 0x000000F1 bit5=32
sample 6: 0x000000F1 bit5=32
sample 7: 0x000000F1 bit5=32
sample 8: 0x000000F1 bit5=32
sample 9: 0x000000F1 bit5=32
sample 10: 0x000000F1 bit5=32
sample 11: 0x000000F1 bit5=32
sample 12: 0x000000F1 bit5=32
sample 13: 0x000000F1 bit5=32
sample 14: 0x000000F1 bit5=32
sample 15: 0x000000F1 bit5=32
sample 16: 0x000000F1 bit5=32
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 441 KiB

File diff suppressed because it is too large Load Diff
@@ -0,0 +1,17 @@
# Ch441 LPDDR_STATUS 0x02C active-scanout sampling (video_src=1, CTRL 0x018=0x5)
sample 1: 0x000000F1 bit5=32
sample 2: 0x000000F1 bit5=32
sample 3: 0x000000F1 bit5=32
sample 4: 0x000000F1 bit5=32
sample 5: 0x000000F1 bit5=32
sample 6: 0x000000F1 bit5=32
sample 7: 0x000000F1 bit5=32
sample 8: 0x000000D1 bit5=0
sample 9: 0x000000F1 bit5=32
sample 10: 0x000000F1 bit5=32
sample 11: 0x000000F1 bit5=32
sample 12: 0x000000F1 bit5=32
sample 13: 0x000000F1 bit5=32
sample 14: 0x000000F1 bit5=32
sample 15: 0x000000F1 bit5=32
sample 16: 0x000000F1 bit5=32