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>
26 KiB
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 shiftlane*PIX_BITS; strobelane*PIX_BYTESwide. The&nsbeat-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 lastpx_emitto 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 (gatedflush_psm_o==6'h00) drivesgs_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
flushpulse + 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:
- 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.) - Drain gate must be STABLE-idle: a single
idlesample RACES the async-FIFO CDC (flush-pushed beat's gray pointer needs ~2 axi cycles to cross;fifo_remptyreads 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(fullvsreal):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 staysh3_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_demo96.2% PASS (Ch352 build intact). - proof TB:
\ifdef SH3_FULL_FRAMEselectssh3_full_params.vh; Makefile targettb_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_masterPASS (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_demostill 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_masterPASS (+ 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-cycleframe_startpulse 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_FRAMEis 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:
- HPS preclears the framebuffer.
- HPS uploads the texture.
- Texture cache fill completes successfully.
- CLUT boot upload/load completes.
- Arm the PSMCT32 writer.
- Start the feeder render.
- Send EOF; await EMIF-domain
frame_drained. - Enable the static LPDDR scanout.
Codex round 5 — Brick 3 build rulings (board path)
- 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 — pollingwrite_pendingand checking BRESP. A new AXI engine + arbiter are unjustified before measuring this path. - Dedicated
GS_SH3_LPDDR_FBprofile, separate fromGS_TILE_SPILL, static PSMCT32 line-buffer scanout, profile-specific geometry (256×334). frame_drainedas 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.- Feeder auto-start-once correction (CRITICAL).
top_psmct32_raster_demo_bram.sv:807C_SETUP assertsfdr_startwhen the boot upload finishes → the disarmed boot render leaves a pending EOF that would assertframe_drainedthe instant the host later arms the writer. Fix:FEEDER_AUTOSTARTparam (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. - Expose
frame_drainedas 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_AUTOSTARTparam + FSM intop_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_FBinner-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 / routeframe_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_drainedas a status bit (mind the.*-port-add trap: update every bridge instantiation/TB). - (host)
ps2_sh3_tex_upload.cFB-preclear loop (zero 0x000000–0x0537FF via write-probe, poll write_pending + BRESP). - (build) add the
GS_SH3_LPDDR_FBprofile toselect_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_drainedfires 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_drainedreaches 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_inflightset 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_emiton 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_effforced 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_wforward-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): newinput 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_drainednow 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 tiedlogic 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 reportsframe_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.