Files
retroDE_ps2/docs/ch353_audit_log.md
thejayman77 ba74bbd5aa Snapshot: fog implementation + fidelity tooling baseline (pre bilinear-clamp fix)
Per-vertex GS fog end-to-end (gs_stub emit incl. persp_emit5, gs_prim_list_feeder
XYZ2->XYZF2 on PRIM.FGE, gs_make_sh3_scheduler_fixture.py F/FGE packing), new fog
TBs, fidelity attribution tooling. Functional baseline before removing the dead
bilinear lerp8 clamps (Codex: 161-node comb loop -> -0.042ns setup fail).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 19:56:46 -04:00

353 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ch353 audit log — full SH3 draw on an LPDDR-only direct PSMCT32 framebuffer
Purpose: complete, auditable trail of every change + verification for Codex review.
Scope (Codex-approved): **Path A — LPDDR-only direct PSMCT32 framebuffer**, replacing the 256×120 BRAM crop with
the full 256×334 draw bounding box rendered to LPDDR and scanned out. Path B (tile-spill) deferred.
Labeling note (Codex): this is the **uncropped draw bounding box at origin**, NOT the full SH3 game framebuffer.
## Numbers (pre-edit, reported to Codex)
- dump 224139, `full_h = 334`; full frame **256 × 334 PSMCT32** = 342,016 B = **334 KiB** (0x53800) = **10,688** 256-bit beats.
- row stride 1024 B. LPDDR ranges (disjoint): **FB 0x000000..0x053800 (334 KiB)**; **TEX 0x200000..0x240000 (256 KiB)**.
- 334 KiB > 128 KiB BRAM and > 256 KiB read2 cap → LPDDR required.
---
## Brick 1a — widen `gs_lpddr_axi_master` to PSMCT32 + end-of-scene flush [DONE, verified]
RTL: `rtl/gif_gs/gs_lpddr_axi_master.sv`
- Added `parameter int PIX_BYTES = 2` (2 = PSMCT16 default, byte-identical to Ch318; 4 = PSMCT32).
- Derived: `PIX_BITS = PIX_BYTES*8`, `LANE_LO = (PIX_BYTES==4)?2:1`, `STRB1 = '1` (per-pixel byte-strobe mask).
- Generalized the packer: `lane = px_addr[4:0]>>LANE_LO`; data shift `lane*PIX_BITS`; strobe `lane*PIX_BYTES` wide.
The `&ns` beat-complete test is UNCHANGED (a full 32-byte beat is always 32 strobe bits either way).
- Port `px_pix16[15:0]``px_pix32[31:0]` (PSMCT16 callers drive `{16'd0, pix16}`).
- New input `flush`: end-of-scene partial-beat flush — sparse triangle coverage won't fill every beat; pulse it
after the last `px_emit` to push the dangling partial beat. PSMCT16 tile path ties it to 0 (tile-rows always
complete beats). Branch: `else if (enable && arm_gs && flush && has_data)` pushes `{cur_addr,cur_data,cur_strb}`.
Callers updated: `rtl/top/de25_nano_psmct32_raster_demo_top.sv` (2 instantiations) → `.px_pix32({16'd0, demo_flush_pix16}), .flush(1'b0)`.
TB: `sim/tb/gif_gs/tb_gs_lpddr_axi_master.sv` — PSMCT16 instantiations updated; added a PSMCT32 DUT (`PIX_BYTES=4`)
with an always-ready capturing slave + tests: 8 px → 1 full beat; 5 px + flush → 1 partial beat; unstrobed lanes 57
untouched. Result: `[axi32] PSMCT32 ok ...`, `[axi] DISARMED/CANARY/STABILITY ok` (PSMCT16 byte-identical), **PASS**.
**Verification:** full regression **272 PASS / 0 FAIL** (writer touches the de25 top, so regression confirms nothing broke).
---
## Brick 1c — LPDDR-only direct-FB proof [DONE, verified]
Recon finding (big de-risk): the bram-top ALREADY has the pieces — no new bram-top logic:
- `FB_LPDDR_ONLY` (Ch326, `top_psmct32_raster_demo_bram.sv:1170`): `vram_we_pre = xfer_busy ? xfer_we : (raster_pixel_emit && !FB_LPDDR_ONLY)` — suppresses the BRAM raster mirror, keeps xfer/CLUT writes. = Codex's "suppress raster, keep CLUT".
- `flush_color32_o`/`flush_addr_o`/`flush_emit_o`/`flush_psm_o` (Ch323): the full PSMCT32 raster stream, already exposed.
- `feeder_ready_o` (C_READY): render-done, for the flush pulse.
TB: `sim/tb/top/tb_top_psmct32_sh3_lpddr_fb.sv` (NEW; Makefile target added; dump-derived, NOT in regression):
- bram-top with `FB_LPDDR_ONLY=1`; the exposed PSMCT32 stream (gated `flush_psm_o==6'h00`) drives
`gs_lpddr_axi_master#(.FIFO_DEPTH(64),.PIX_BYTES(4))` → a behavioral, strobe-honouring LPDDR FB.
- Separate faster `axi_clk2` (#2 vs design #5) models the board's emif_clk and exercises the real async-FIFO CDC.
- "ideal" frame captured straight off the raster stream; PROOF asserts LPDDR == ideal for every emitted pixel.
- End-of-scene `flush` pulse + a STABLE-idle drain gate before asserting.
**Verification:** `[fbproof] covered=14730 words, mismatches=0, writer beats=5572`**PASS** (cropped 256×120 data;
full-frame is a fixture swap, Brick 1b).
Two findings worth auditing:
1. Same-clock drain can't keep up with line-change-flush bursts → use a faster AXI clock (board emif_clk is ~12×
faster, drains fine) + `FIFO_DEPTH=64`. (No RTL backpressure added; the board clock ratio makes it unnecessary.)
2. **Drain gate must be STABLE-idle**: a single `idle` sample RACES the async-FIFO CDC (flush-pushed beat's gray
pointer needs ~2 axi cycles to cross; `fifo_rempty` reads empty meanwhile) → asserted before the last overdraw
pixel drained (1 stranded pixel). Fix: settle + require idle for 8 consecutive cycles. **This validates Codex's
"gate scanout on render-done AND writer-drained/B-response" — the board gate must be stable-idle, not transient.**
---
## Brick 1b — full-frame fixture (256×334) + re-run proof [DONE, verified]
Tooling: `tools/gs_make_sh3_real_draw_fixture.py` — added `--full-frame`:
- `global CH`; after the crop search, `if full_frame: CH=full_h; CY0=0; CX0=0` (render the whole 256×334 bounding box).
- Crop-dependent outputs written with a `tag` (`full` vs `real`): `feeder_sh3_{tag}.mem`, `sh3_{tag}_params.vh`,
`sh3_{tag}_refmap*.mem`, `bios_sh3_{tag}.mem`, `payload_sh3_{tag}.mem`. Shared texture/CLUT/idx/pal stay `sh3_real_*`.
→ the Ch352 cropped build (`sh3_real_*`) is byte-untouched.
- CBP stays 480 (CLUT in BRAM; with FB_LPDDR_ONLY the FB is in LPDDR so it doesn't collide). CLUT→0 relocation is
deferred to Brick 3 (a fit-side BRAM-shrink optimization, not needed for the sim proof).
Verification:
- default run → `sh3_real_*` FBH=120/CY0=188 (unchanged); `--full-frame``sh3_full_*` FBH=334/CY0=0.
- **Cropped oracle still bit-identical: `tb_top_psmct32_sh3_real_draw_demo` 96.2% PASS** (Ch352 build intact).
- proof TB: `\`ifdef SH3_FULL_FRAME` selects `sh3_full_params.vh`; Makefile target `tb_top_psmct32_sh3_lpddr_fb_full`
points BIOS/PAYLOAD/FEEDER at the `_full` files.
- **Full-frame proof: `[fbproof] covered=16571 words, mismatches=0, writer beats=6502` → PASS.**
## Status: Brick 1 (render to LPDDR) COMPLETE + verified at full frame (16571 px, 0 mismatches).
---
## Codex audit corrections (all 5 resolved before Brick 2)
**#1 (High) — full-frame oracle distinction.** The transport proof (LPDDR == raster-captured ideal) proves *transport*,
not *render correctness*. Added the full-frame equivalent of the bounded oracle to the proof TB: loads the reference
texel map (`sh3_{tag}_refmap.mem`) + palette/indices, scores the rendered frame `<=1 texel` (same acceptance as
`tb_top_psmct32_sh3_real_draw_demo`). Cropped reads **96.2% / INT 97.5% / clut_bad=0 — EXACTLY matching the standalone
oracle**; full-frame **96.1% / INT 97.6% / clut_bad=0**. (Also init `ideal[]`=0 to model the Brick-3 FB preclear, so
refmap-covered-but-unrendered edge pixels read as the cleared color, not X.)
**#2 (High) — production-safe drain ack (replaces the stable-idle heuristic).** `gs_lpddr_axi_master`: FIFO word
widened to 321 (marker bit). A `flush` now pushes any partial beat **then an ordered EOF marker**; the AXI FSM sets a
new `frame_drained` output when it POPS the marker — which, by the in-order FIFO + per-beat B handshake, happens only
AFTER the last data beat's BRESP. Coherent in the axi domain (no GS-domain `idle` sampling, no pointer-in-flight race).
The proof TB now waits on `frame_drained` (a stable level), not the heuristic. PSMCT16 path byte-identical (flush tied 0).
**#3 (Medium) — PSMCT32 AXI backpressure.** `tb_gs_lpddr_axi_master`: the PSMCT32 DUT now has LFSR AW/W backpressure +
a delayed B response; asserts full beat + partial-flush survive stalls, **ovf=0, bresp_err=0**, and the ordered drain
ack fires. PASS.
**#4 (Medium) — tagged generated PNGs.** `gs_make_sh3_real_draw_fixture.py` now writes `sh3_{tag}_ref.png`; the stale
idx8 recip render is written as `STALE_idx8_sh3_{tag}_ref_recip.png` (clearly marked). A `--full-frame` run no longer
clobbers crop references; restored the crop `sh3_real_ref.png` (256×120) and created `sh3_full_ref.png` (256×334).
**#5 (Hygiene) — gitignore.** Added `/synth/**/tmp-clearbox/`, `/synth/**/sta_*.txt`, `*.tdf` so STA scratch +
clearbox temp are excluded from the next commit (the committed tmp-clearbox is already pending-deleted on disk).
## Verification (post-corrections)
- `tb_gs_lpddr_axi_master` PASS (PSMCT16 byte-identical, PSMCT32 + backpressure, ordered ack).
- `tb_top_psmct32_sh3_lpddr_fb` (cropped) PASS: transport 0 mism + oracle 96.2%/97.5%/clut_bad=0.
- `tb_top_psmct32_sh3_lpddr_fb_full` (256×334) PASS: transport 0 mism + oracle 96.1%/97.6%/clut_bad=0.
- Cropped oracle `tb_top_psmct32_sh3_real_draw_demo` still 96.2% PASS (Ch352 build intact).
- Full regression: re-running to confirm the writer-width/marker change stays green.
## Codex audit round 2 (two real items + integration note)
**(High) EOF/partial dropped when FIFO full.** The flush branches cleared `has_data`/`flush_pending` on scheduling
`fifo_wr`, but the FIFO write is `fifo_wr && !fifo_wfull` — if full, the partial/marker vanished. Fix in
`gs_lpddr_axi_master`: (a) gate the flush branches on `!fifo_wfull` so state is RETAINED (retried) until accepted;
(b) `fifo_wfull` is REGISTERED (asserts 1 cycle after the FIFO fills), so the partial push could make the FIFO full
while the marker branch still saw stale `!wfull` and fired early → its write then dropped. Added a 1-cycle `flush_gap`
between the partial and marker pushes so `wfull` reflects the partial's write first. **Saturation test added**
(`tb_gs_lpddr_axi_master`): hold AXI fully off until the FIFO is full, flush, release → asserts the partial beat
survives, `frame_drained` asserts only after release/BRESP, EOF never popped while stalled. PASS.
**(Hygiene) tracked tmp-clearbox deletions.** `git rm --cached` on the 51 tracked `tmp-clearbox/*.tdf` files (gitignore
only affects untracked) → now 0 tracked, 51 staged deletions; the next commit removes them. No other STA/tdf scratch
tracked.
**Integration note (Codex, for Brick 2).** `frame_drained` is coherent in the AXI/EMIF domain — Brick 2's scanout
gate must consume it THERE (gs_lpddr_scanout* run on emif_clk). Any design-clock use needs a synchronizer. The proof
TB's direct cross-domain poll of `frame_drained`/`fbw_idle` is sim convenience, not a hardware CDC.
## Codex audit round 3 (enqueue edge case)
**(High) a prior NORMAL `fifo_wr` leaves `fifo_wfull` stale at a flush branch.** Cases: a scene ending on a full
beat (no partial), or a partial right after a line-change push — the previous registered `fifo_wr` is consuming the
final slot, so the flush branch's `!fifo_wfull` check is stale. The round-2 `flush_gap` only covered partial→marker.
Fix: gate BOTH flush enqueue branches on **`!fifo_wr && !fifo_wfull`** — no push in flight AND not full, so the
`!wfull` check is accurate; state is retained/retried until accepted. This also subsumes the partial→marker gap
(the marker waits for the partial's `fifo_wr` to clear and `wfull` to settle), so `flush_gap` was removed.
**NEAR-FULL test added** (`tb_gs_lpddr_axi_master`): scene ends on a FULL beat, no partial, flush immediately after the
final normal beat under a saturated FIFO → the EOF marker survives and `frame_drained` asserts only after release/BRESP.
PASS (alongside the saturation test).
## Verification (round 2)
- `tb_gs_lpddr_axi_master` PASS (+ saturation: FIFO-full flush, partial+EOF retained, frame_drained after BRESP).
- both proofs still PASS (cropped 96.2%/clut_bad=0; full-frame 96.1%/clut_bad=0; transport 0 mism).
- full regression re-running (writer gap fix).
Remaining (Brick 2 / 3): PSMCT32 line-buffer scanout proof (`gs_lpddr_scanout_lb` already PSMCT32, reads the LPDDR FB
back out, gated on `frame_drained` IN THE EMIF DOMAIN); board (HPS preclear, static line-buffer scanout, CLUT→CBP 0 to
shrink BRAM, fit).
## Brick 2 — PSMCT32 LPDDR-FB line-buffer scanout proof (`tb_gs_lpddr_scanout_fb`)
Full write→read loop: the Ch353 writer (`gs_lpddr_axi_master #(.PIX_BYTES(4))`) fills a behavioral 256×334 PSMCT32 FB
(STRIDE=1024B, ROW_BEATS=32, BEATS_PER_FRAME=10688) → `frame_drained``gs_lpddr_scanout_lb #(.STRIDE_BYTES(1024),
.ROW_BEATS(32),.N_ROWS(334),.PSMCT32(1))` with `enable=frame_drained` reads the FB back → video raster serves r/g/b →
compared against `exp_word(x,y)`. Three independent clocks (gs #5 / emif #2 / video #7). Real cadence: H_BP=32/H_FP=8,
V_BP=16/V_FP=8; active-relative px/py; `in_win`/`vsync` gating. FB precleared to 0 (models HPS preclear). Read model
has variable AR/R latency (LFSR). All 256×334 pixels compared **including the x≥128 black half**.
Codex acceptance gates — all met:
- **All pixels correct incl. black**: `errors=0` (checked=105968 ≥ 256×334 active).
- **underflow=0** (row always ready before its pixel — required the vertical/horizontal back porch so the prefetch
leads the display; the first run without a back porch raced and asserted underflow).
- **rd_errs=0** (no read-response errors).
- **valid through the final row**: `checked ≥ H_ACT*V_ACT`.
- **frame_drained gate consumed in the EMIF domain** (scanout runs on emif_clk).
Read-beats/frame: measured in the **EMIF domain** per prefetch frame (counter reset on the emif-synced vsync rising
edge) = **10720 = 334 rows + 1 row re-fetched at the vblank boundary**. Root cause (a real scanout behavior, not a TB
bug): the scanout's `fs_edge_e` is a *both-edge* detector, so a 1-cycle vsync pulse fires two prefetch resets
(rising+falling), re-fetching row 0 once. Harmless — every row is read correctly (pixels match). The TB accepts 10688
(ideal) OR 10720 (+1 boundary row) and hard-fails anything else. **FLAG FOR CODEX:** is the exact-10688 gate strict, or
is the ~1-row/frame vblank-boundary re-fetch acceptable (it wastes 32 beats/frame ≈ 0.3% of read bandwidth)? If it must
be exactly 10688, the fix is on the scanout/top side (drive frame_start so the both-edge doesn't double-reset), not the
proof.
Result: `tb_gs_lpddr_scanout_fb` **PASS**; added to both `sim/Makefile` master run lists.
Remaining (Brick 3 / board): HPS preclear FB, static line-buffer scanout wired at the de25 top, CLUT→CBP 0 to shrink
BRAM, fit.
## Codex round 4 — beat-count fix (production RTL) + Brick 3 start-ordering gate
**(Resolved) both-edge → rising-edge in `gs_lpddr_scanout_lb`.** The 10720-vs-10688 was a real production-RTL issue,
not test noise. Both frame-start edge detectors were both-edge:
- `fs_edge_v` (video domain, line 88): `fs_sync_v[2] != fs_sync_v[1]``fs_sync_v[1] && !fs_sync_v[2]`
- `fs_edge_e` (emif domain, line 132): `fs_sync_e[2] != fs_sync_e[1]``fs_sync_e[1] && !fs_sync_e[2]`
A 1-cycle `frame_start` pulse fired rising+falling → two prefetch resets → row 0 re-fetched once. Rising-edge only =
exactly one reset per frame. TB assertion tightened back to **exactly 10688** (`!== BEATS_PER_FRAME` is a hard fail).
Verification: `tb_gs_lpddr_scanout_fb`**10688/frame, underflow=0, rd_errs=0, errors=0, PASS**. Focused scanout
suite all PASS (scanout_lb, scanout_lb_psm32, scanout_lb_psm32_256, scanout_concurrency, scanout, scanout_psm32).
Full regression re-run for the RTL change.
## Brick 3 — start-ordering gate (Codex, mandatory sequence)
The de25 top must enforce this host-start order; the boot CLUT payload must NOT auto-start rendering before 14:
1. HPS preclears the framebuffer.
2. HPS uploads the texture.
3. Texture cache fill completes successfully.
4. CLUT boot upload/load completes.
5. Arm the PSMCT32 writer.
6. Start the feeder render.
7. Send EOF; await EMIF-domain `frame_drained`.
8. Enable the static LPDDR scanout.
## Codex round 5 — Brick 3 build rulings (board path)
1. **No hardware preclear engine for v1.** Extend the HPS write-probe (`ps2_sh3_tex_upload.c`) to zero
0x0000000x0537FF (85,504 words) — only ~30% more traffic than the proven 65,536-word texture upload — polling
`write_pending` and checking BRESP. A new AXI engine + arbiter are unjustified before measuring this path.
2. **Dedicated `GS_SH3_LPDDR_FB` profile**, separate from `GS_TILE_SPILL`, static PSMCT32 line-buffer scanout,
profile-specific geometry (256×334).
3. **`frame_drained` as a HARD gate, no timeout.** A timeout can display a partial FB — recreating the bug the
handshake fixes. Failure stays BLACK with diagnostics; a manual debug override is acceptable only if explicit.
4. **Feeder auto-start-once correction (CRITICAL).** `top_psmct32_raster_demo_bram.sv:807` C_SETUP asserts `fdr_start`
when the boot upload finishes → the disarmed boot render leaves a pending EOF that would assert `frame_drained` the
instant the host later arms the writer. Fix: **`FEEDER_AUTOSTART` param (default 1'b1), 0 only for this profile** →
C_SETUP goes straight to C_READY (no boot render); every render then waits for an explicit 0x0E8 GO.
5. **Expose `frame_drained` as a host-readable diagnostic** if a spare status bit exists.
Required host sequence (GS_SH3_LPDDR_FB): (1) wait setup/CLUT complete + feeder ready → (2) preclear LPDDR FB →
(3) upload texture → (4) fill + verify texture cache → (5) configure base 0, full mode, arm writer → (6) issue 0x0E8 GO
→ (7) await ordered `frame_drained` → (8) enable scanout automatically from that ack.
### Landed this round
- **`FEEDER_AUTOSTART` param + FSM** in `top_psmct32_raster_demo_bram.sv` (param after FEEDER_STG_WORDS; C_SETUP
branches on it → auto boot render when 1, straight to C_READY when 0). Additive, default 1'b1. Verified
byte-identical: tb_top_psmct32_sh3_lpddr_fb / feeder_runtime_demo / feeder_scene_retrigger_demo all PASS.
### Design clarification (derived)
`GS_SH3_LPDDR_FB` is a **feeder-based** profile: the host streams the SH3 draw list into the feeder (0x0DC/0x0E4) and
GOes (0x0E8) — matching Codex's host sequence. So its inner-instance config mirrors `GS_SH3_REAL_DRAW_DEMO`
(FEEDER_ENABLE=1, CLUT_CSM1_ENABLE=1, PERSPECTIVE_CORRECT=1, PERSP_RECIP_IDX_BITS=11, GS_LPDDR_TEX cache) but with:
FEEDER_AUTOSTART=0, FB_LPDDR_ONLY=1 (Ch326, suppress BRAM FB mirror), the full 256×334 PSMCT32 FB in LPDDR (not the
128 KiB crop), and the line-buffer scanout at 256×334.
### Remaining Brick 3 build (precise)
- **(RTL, board top)** add `elsif GS_SH3_LPDDR_FB` inner-instance param block (SH3 config + FEEDER_AUTOSTART=0 +
FB_LPDDR_ONLY=1); activate the GS_LPDDR4B_FB writer/scanout blocks for this profile; reconfigure the writer instance
to PIX_BYTES=4 / px_pix32=demo_flush_color32 / px_emit on psm==6'h00 / flush=end-of-scene / route `frame_drained`;
set scanout geometry (STRIDE=1024, ROW_BEATS=32, N_ROWS=334, PSMCT32); `frame_ready_emif = frame_drained` (hard gate,
no timeout).
- **(RTL, bridge)** expose `frame_drained` as a status bit (mind the `.*`-port-add trap: update every bridge
instantiation/TB).
- **(host)** `ps2_sh3_tex_upload.c` FB-preclear loop (zero 0x0000000x0537FF via write-probe, poll write_pending + BRESP).
- **(build)** add the `GS_SH3_LPDDR_FB` profile to `select_de25_profile.sh` + QSF VERILOG_MACRO (also defines
GS_LPDDR4B_FB + GS_LPDDR_TEX + the SH3 fixture macros).
- **(sim, MANDATORY before fit)** integration sim proving the sequence: FEEDER_AUTOSTART=0 → no boot render → arm →
GO → EOF → `frame_drained` fires exactly once (NOT prematurely from a boot render) → scanout serves the correct
full-frame. This is the correctness proof for Codex's round-5 item 4.
## Brick 3 — host-start SEQUENCE proof (integration sim) — PASS
`sim/tb/top/tb_top_psmct32_sh3_lpddr_fb_seq.sv` (full-frame fixtures; the mandatory pre-fit gate). Instantiates the
bram-top with **FEEDER_AUTOSTART=0** + FB_LPDDR_ONLY + the SH3 feeder config, the PSMCT32 writer, the behavioral
(precleared) LPDDR FB, and `gs_lpddr_scanout_lb` reading it back to a real video raster — with the **render-epoch**
flush logic (Codex round 5) driving the writer's EOF. Clocks: design/gs #5, emif #2, video #7, bridge #11.
Render epoch (mirrors what wires into the board top): `render_inflight` set by an accepted host GO (feeder_go while
ready); `flush = render_inflight && feeder_ready_rise`; cleared on flush. So the FEEDER_AUTOSTART=0 setup→C_READY edge
(render_inflight=0) can NOT enqueue a false EOF.
Codex acceptance — ALL met on the first run:
- Boot setup reaches C_READY. **Before GO: raster_emits=0, writer_beats=0, eof=0, frame_drained=0** (no boot render).
- Texture fill verifies (fill_crc=fbdeaa32, tex_rd_errs=0). FB precleared to 0; idle BRESP errors=0.
- Host sets base 0 / canary off / arms writer (latched via ctrl_commit).
- **GO → feeder_ready drops then re-rises** after render+drain.
- **Exactly ONE EOF → exactly ONE frame_drained.** Writer beats=6502, FIFO overflow=0, BRESP errors=0.
- Synchronized `frame_drained` reaches the bridge domain.
- Scanout auto-enabled from the ack: **exactly 10688 read beats/frame**, all 256×334 pixels match (incl. black),
underflow=0, rd_errs=0.
- Bounded oracle: 96.1% ≤1-texel ALL / 97.6% interior / clut_bad=0 (correct SH3 render).
- `errors=0`**PASS**.
The SH3 fixture TBs are dump-derived/LOCAL, so this is run manually (like its siblings) — NOT added to the master
`run:` list. Full regression re-run for the widely-used inner-module FEEDER_AUTOSTART change (additive, default 1'b1).
REMAINING Brick 3 (board plumbing, then owner fit): board-top GS_SH3_LPDDR_FB profile (param block + writer reconfig +
scanout geom + frame_ready_emif=frame_drained + the render-epoch logic wired in design_clk); bridge frame_drained
status bit; host preclear loop in ps2_sh3_tex_upload.c; select_de25_profile.sh + QSF profile. No fit until the board
build compiles clean under the profile and this sequence proof stays green.
## Brick 3 board plumbing — COMPLETE (awaiting owner fit)
All board-side wiring for GS_SH3_LPDDR_FB is landed and verified in sim; the fit itself is owner-gated.
**Board top (`de25_nano_psmct32_raster_demo_top.sv`), all gated on `GS_SH3_LPDDR_FB` (inert for every other profile):**
- Inner-instance param block `elsif GS_SH3_LPDDR_FB` — mirrors GS_SH3_REAL_DRAW_DEMO (FEEDER_ENABLE, CLUT_CSM1,
PERSPECTIVE_CORRECT, PERSP_RECIP=11, VRAM 128 KiB, GRAD_SEQ_DIVIDER=0) + **FEEDER_AUTOSTART=0** + **FB_LPDDR_ONLY=1**.
- TEXC block: `elsif GS_SH3_LPDDR_FB` → the same 512×512 PSMT8 LPDDR texture geometry as the crop.
- Render epoch (design_clk): `sh3_render_inflight` set by an accepted feeder GO; `sh3_fb_flush = inflight && feeder_ready
rise`; cleared on flush → exactly one EOF per render (Codex round 5 — the setup→C_READY edge can't leak an EOF).
- frame_ready gate: `frame_ready_emif = sh3_frame_drained` (EMIF prefetch enable) + `frame_ready_r` = design-synced
sh3_frame_drained (HDMI mux blanks black until drained). HARD gate, no timeout.
- Writer (u_lpddr_axi, the active GS_LPDDR4B_FB instance): `PIX_BYTES(4)`, `px_emit` on psm==6'h00,
`px_pix32=demo_flush_color32`, `flush=sh3_fb_flush`, `.frame_drained(sh3_frame_drained)`.
- Scanout: `SCANOUT_LB_NROWS=334` (STRIDE 1024 / ROW_BEATS 32), `SCANOUT_FB_BASE=0`, `SCANOUT_PSMCT32=1`,
`scanout_lb_eff` forced 1 (line-buffer; a 256×334 frame-cache would be 334 KiB).
- Verified: patched-copy iverilog elaboration under GS_SH3_LPDDR_FB+GS_LPDDR4B_FB+GS_LPDDR_TEX = exit 0 (the only
standalone-sim gap is the Quartus-only EMIF_Qsys IP + a pre-existing `lpddr_ctrl_commit_w` forward-ref, both
fit-proven). Default board-top TB still builds+passes (edits inert for non-SH3).
**Drain diagnostic:** used existing `LPDDR_STATUS 0x02C[0]` (writer idle, stable post-render) + `LPDDR_BYTES 0x030`
(beats!=0) — the narrowest path (Codex), avoiding a bridge port-add that would break 4 `.*` regression TBs.
`frame_drained` still gates the scanout internally.
**Build profile:** `scripts/select_de25_profile.sh sh3_lpddr_fb` case added (SCENE = GS_SH3_LPDDR_FB + GS_LPDDR_TEX +
feeder_sh3_full; FIXTURE = bios/payload_sh3_full). Ran it myself + verified the QSF: SCENE/FIXTURE blocks correct,
markers balanced (2×), static GS_LPDDR4B_FB=1 / GS_LPDDR_FB=1 / USE_QSYS_TOP=1 / EMIF IP intact.
**Host app:** `tools/ps2_sh3_tex_upload.c` extended with `--lpddr-fb` mode (Codex's no-hw-clear path): wait feeder
ready → PRECLEAR FB 0x000000..0x0537FF (85504 words) via the write-probe (poll write_pending, check BRESP) → upload
texture → fill+verify cache → set base 0 / canary off / arm writer → feeder GO → await drain (idle + bytes!=0) →
raise video_src. Compiles clean (-Wall); dry-run OK (tex sum32=fbdeaa32). Crop path byte-unchanged.
**Verification:** seq proof `tb_top_psmct32_sh3_lpddr_fb_seq` still PASS (10688 beats, 1 EOF→1 frame_drained, oracle
96.1%). Regression re-running after the board-top edits.
**Owner fit order:** (1) `python3 tools/gs_make_sh3_real_draw_fixture.py --full-frame` (regenerate LOCAL fixtures if
absent); (2) `./scripts/select_de25_profile.sh sh3_lpddr_fb` (DONE — QSF already set); (3) fit in Quartus; (4) on the
board: `gcc -O2 -o ps2_sh3_tex_upload tools/ps2_sh3_tex_upload.c && sudo ./ps2_sh3_tex_upload sh3_real_tex_lpddr.mem
--lpddr-fb`.
## Ch353 follow-up — host-visible frame_drained diagnostic bit (Codex-approved after silicon)
Silicon showed the host's `idle`-based drain confirmation reads a mid-render count (idle pulses between the feeder's
render batches → the board printed "beats=44"). The DISPLAY was correct (scanout gated on the ordered frame_drained),
but the host couldn't READ a stable drain signal. Added it (the `.*`-port-add done carefully):
- **Bridge (`ps2_hps_bridge.sv`):** new `input frame_drained_i` + `frame_drained_sync` (2-FF into the bridge clock) +
**LPDDR_STATUS 0x02C[6] = frame_drained_sync[1]** (STABLE — asserts only after the EOF marker's last BRESP).
- **Board top:** `sh3_frame_drained` now defined in both ifdef branches (driven by the writer under GS_SH3_LPDDR_FB,
tied 0 otherwise); bridge `.frame_drained_i(sh3_frame_drained)`.
- **`.*`-trap handled:** the 4 `.*` bridge TBs (tb_ps2_hps_bridge, tb_bridge_iop_pad_input, tb_pad_state_via_sif_to_ee,
tb_ee_pad_buffer_branch) each got a tied `logic frame_drained_i = 1'b0;` (matched by name). tb_lpddr_tex_staging is
explicit/partial — the new input floats like its other unconnected inputs (no change). null bridge unaffected
(its port list is a strict subset). All 5 bridge TBs PASS.
- **Host (`ps2_sh3_tex_upload.c --lpddr-fb`):** step 7 now polls **0x02C[6] frame_drained** (not [0]idle) and reports
`frame_drained=1, FB beats written=~6500`. Compiles clean (-Wall).
Verified: board top elaborates clean under GS_SH3_LPDDR_FB+GS_LPDDR4B_FB+GS_LPDDR_TEX (exit 0); seq proof still PASS
(10688, 1 EOF→1 frame_drained); full regression re-running. This is a diagnostic-only change — the functional path
(silicon-verified) is unchanged; it re-fits into the same profile.