Files
retroDE_ps2/docs/ch353_audit_log.md
T
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

26 KiB
Raw Blame History

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=5572PASS (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-framesh3_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_FRAMEselectssh3_full_params.vh; Makefile target tb_top_psmct32_sh3_lpddr_fb_fullpoints 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_drainedgs_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_fb10688/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=0PASS.

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.