# 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 5–7 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 1–4: 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 0x000000–0x0537FF (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 0x000000–0x0537FF 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.