Harden the LPDDR scanout underflow diagnostic (Codex direction). Keeps the
Ch443e 4-buffer + lead-2 fix; adds no 5th buffer.
RTL (gs_lpddr_scanout_lb):
- Replace the raw-binary next_fetch readiness sync with a reset-aware GRAY
code. next_fetch is monotonic between frames, so one Gray bit changes per
increment; the 2-FF-synced + decoded nf_v is always a real prior frontier
(monotone, burst-safe), never a torn multi-bit combination. (A plain
toggle-per-change handshake dropped bursts when two increments landed in one
sync window; that is why the earlier attempt under-read nf_v and false-tripped.)
fs_edge_v overrides the lone multi-bit reset transient.
- Qualify the underflow: only a miss persisting >= QUAL_CYCLES (4) sets sticky
underflow. uf_pmax_q records the longest streak and uf_qual_q whether any
qualified, so a host can distinguish a 1-cycle CDC lag from a real late row.
- Atomic snapshot: scan_y/nf_v/pmax/causes/vphase/line_valid all latched the
same video cycle on the first qualified miss.
ABI: 0x120 adds [15:12]=live pmax (2-FF synced via scan_diag_pmax_i). 0x124
[29:20] now carries pmax-at-capture (was nf_s0). Bridge dst reg kept 10-bit.
SDC: scanout diag bundle source count 37 -> 31 (nf_s0[10] -> pmax[4]); new
async-in cut + max_skew/net_delay for the next_fetch Gray CDC; stage-0 cut for
the live pmax sync.
Tests: new tb_gs_scanout_cdc_qual (async-clock focused: no false event from the
readiness transition or ordinary sync latency; sub-QUAL transient does not
qualify; genuine late row qualifies with a self-consistent atomic snapshot;
frame reset + mod-4 reuse re-arm the detector). tb_gs_scanout_diag updated for
the pmax field. Tie off scan_diag_pmax_i (+ pre-existing clut_* gap from the
fog baseline) in the four .* bridge/integration TBs.
Regressions green: scanout (cdc_qual, binomial_lookahead, diag, restart,
lpddr_scanout_lb x3), regbuf (r/aw/w), bridge + 3 pad integration TBs, and the
f52 top-level golden FB (Z 0/307200, COLOR 0/245760 mismatch, drops=0).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Ch443d board per-frame diagnostic identified the real displayed-frame
failure as a BINOMIAL vertical-lookahead starvation: displaying source
row r while interpolating r-1/r/r+1, the prefetch led by only one row
(next_fetch <= disp_row+1), so the lookahead row r+1 was still in flight
when the 3x3 filter read it (board: scan_y=33, nf_v=34, cause_lookahead=1,
read-error=0, deterministic every frame).
Fix (BINOMIAL_3X3_FILTER only; legacy 2/3-buffer, lead-1 paths unchanged):
- Add a 4th rotating line buffer (lb3) with its own RAM-local write/read/
cache registers. The 3x3 filter needs r-1/r/r+1 resident (3 buffers), so
leading by 2 (fetch r+2 while displaying r) without overwriting r-1
requires a 4th buffer.
- Prefetch lead-2 for binomial: disp_row_limit_e = disp_row+2. The in-flight
r+2 lands in the 4th buffer (b+2 mod 4), always distinct from prev/cur/next
(b-1/b/b+1 mod 4), so it never clobbers a row being read.
- Modulo-4 rotation everywhere: V_SOURCE_BUF%4, stretch_buf_q, next_fetch_buf,
reset alignment at V_SOURCE_START, and the read-cache prev/cur/next case
extended to 4 branches with (b-1)/b/(b+1) mod 4 selection + first/last-row
clamps preserved.
New tb_gs_scanout_binomial_lookahead reproduces the board condition under
realistic EMIF latency (LAT=7) + backpressure and proves: NO binomial
lookahead underflow, correct 3x3 output across modulo-4 wrap + clamps (full
oracle, 1280 px), and coverage that mid-frame rows past V_SOURCE_START+1
with vphase!=0 were exercised under prefetch pressure.
All green: binomial (4-buffer, identical output), lookahead (new),
scanout_lb {,_hstretch,_psm32_256,_fb}, scanout_restart, scanout_diag,
ps2_hps_bridge, and the complete f52 replay BYTE-IDENTICAL (Z 0/307200,
COLOR 0/245760). Also commits the Ch443d board evidence that identified
this defect. No Quartus/board/push from here.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the last currently-visible EMIF-handshake -> FSM setup family the
Ch443c fit exposed (-0.043/-0.022/-0.005 ns), the read-response analogue
of the AW/W buffers.
- gs_axi_r_regbuf: one-entry FULLY-registered AXI R buffer (the R twin of
gs_axi_w_regbuf). Buffers the complete {rdata,rresp,rlast}; u_rready =
!full only (NO combinational dependence on the texture FSM's d_rready);
captures on u_rvalid && u_rready; d_rvalid = full with the payload held
stable until d_rvalid && d_rready; resets only . Inserted between
read-arbiter s2 and gs_texture_cache (u_texf_rbuf). The arbiter is
unchanged -- it completes its R transaction into the buffer, which then
owns delivery to the fill FSM. Cuts EMIF rvalid/rdata -> fst.F_R.
- gs_texture_cache: drop the unobservable fill_data_q reset. F_DRAIN (its
only reader) is reachable only after F_R loads it, so the reset value is
never observed; removing it kills the separate lock_sync|dreg[1] ->
fill_data_q[80] setup path (-0.005 ns).
- New tb_gs_axi_r_regbuf: exactly-once/in-order, randomized responses +
stalls, full backpressure, the full && d_rready no-fall-through case,
{rdata,rresp,rlast} stability, reset-while-empty AND reset-while-full.
All green: r-buffer TB, texture_cache, texture_psmt8_clut, scanout_lb,
scanout_restart, scanout_diag, ps2_hps_bridge, rd_arb, and the complete
f52 replay BYTE-IDENTICAL (Z 0/307200, COLOR 0/245760). No Quartus/board/
push from here.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Ch443 board A+B diagnostic captured a WARM-UP miss, not a displayed-
frame miss: the line-buffer reader is enabled by video_src_emif
immediately, but the HDMI mux only switches to it at the next vsync, and
the diagnostic (cleared only on !enable) froze that pre-display capture.
scan_y=32/nf=32 was the expected pre-display warm-up, and line_valid is
sticky so it only meant SOME row had loaded, not that row 32 was valid.
Two fixes (all accepted Ch443 timing repairs kept: AW buffer, F_SETTLE +
drain multicycle, tile max-skew, monolithic tex_mem):
1. Per-frame diagnostic: clear diag_valid_q on fs_edge_v as well as
!enable (mirrors underflow_v). Discards the warm-up capture and
records the first miss, if any, AFTER the real frame boundary.
2. Frame-restart latency: in L_R, after the single-beat response is
accepted, if fs_pending || fs_edge_e, abandon the remainder of the
obsolete row -- no beat commit, no next old-row AR, no publish/
increment -- and return to L_IDLE, which restarts at V_SOURCE_START.
Protocol-safe (the accepted AXI transaction is complete); removes up
to a full row of restart latency during vertical blanking, so the
restarted prefetch leads the first displayed row.
New tb_gs_scanout_restart proves: mid-fetch frame-start accepts the
in-flight response, issues NO further old-row AR, restarts at row 32,
loads rows 32/33 before active consumption, and no post-restart
underflow. Regressions green: scanout_lb {,_binomial,_hstretch,
_psm32_256,_fb}, scanout_diag (per-frame), ps2_hps_bridge, and the
complete f52 replay BYTE-IDENTICAL (Z 0/307200, COLOR 0/245760).
Also commits the previously-untracked Ch443 board A+B evidence
(docs/hardware/ch443_board_validation/: verdict, 3-session raw, board
FB, RBF sha). No Quartus/board/push from here.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The four-bank tex_mem split (27dfd0b) closed the -0.370 EMIF drain
write-address fanout but the owner GUI fit showed the fitter SCATTERED
the banks, pushing the DESIGN-clock sampler read cone
(ras_v0_x -> perspective-UV -> texel addr -> tex_mem portbaddr, the
design's fundamental ~40ns critical path) to -2.208 ns. Net worse.
Codex's call (Option 1 + honest write-side multicycle), implemented:
- Restore the MONOLITHIC 65536x32 tex_mem, recovering the clean 25 MHz
read-cone placement. Sampler/read-address path stays fully timed
(Ch439g); nothing about it is relaxed.
- Make the EMIF drain write genuinely two-cycle: new F_SETTLE state
between F_DRAIN and F_WRITE. drain_idx_q/drain_word_q are loaded in
F_DRAIN, HELD unchanged through F_SETTLE (the load block gates on
F_DRAIN), and the RAM write + CRC happen at the later F_WRITE edge.
- SDC: fail-closed 2-cycle-setup / 1-cycle-hold multicycle from ONLY
u_texcache|drain_idx_q[*] to tex_mem (a 6.45 ns EMIF window for the
drain write-address). Scoped -from the drain regs, so the sampler
read path (different launch regs) is untouched. HALTs if tex_mem is
present but drain_idx_q renamed.
The AW buffer (gs_axi_aw_regbuf) and the tile-CDC max-skew 2.5 relax
from 27dfd0b are KEPT unchanged (both closed their families in the fit).
Verified: tb_gs_texture_cache (monolithic + F_SETTLE, distinct-per-byte-
lane + full-word, 0 errors), aw/w regbuf, texture_psmt8_clut,
scanout_diag, ps2_hps_bridge, and the complete f52 replay BYTE-IDENTICAL
(Z 0/307200, COLOR 0/245760). No Quartus/board/push from here.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ch442's fit exposed two failing setup families (plus a stale max-skew).
Attack both structurally; no reseeding.
Family 1 - AWREADY -> Z-FSM (-0.410): gen_p2c_ff[23] (EMIF AWREADY)
reached u_zc_emit|u_z's S_SFLUSH_AW/S_FILL_R next-state combinationally.
Ch441 registered only the W channel; add the AW twin:
- new gs_axi_aw_regbuf (one-entry fully-registered AW buffer), inserted
in zc_emit between u_z's AW output and the arbiter s2 AW port. The FSM
now sees registered occupancy, never EMIF's combinational AWREADY.
Family 2 - texcache drain_idx_q -> tex_mem (-0.370, x7): a single index
fanned across the whole 65536x32, 128-M20K macro. Split by WIDTH into
four 65536x8 banks, each with its own (* preserve, dont_merge *) write-
address launch register; write the four byte lanes together in F_WRITE;
reconstruct the sample word by concatenating four registered read bytes.
Selector structure and 1-cycle read latency unchanged; total M20Ks
unchanged (4x32 == 128); fill_crc still sums the full 32-bit word.
Max-skew: relax ONLY the Ch357 tile-write CDC set_max_skew 2.0 -> 2.5
(quasi-static bundle, >=2 dclk stability window); retain set_net_delay
2.0 (the real arrival bound). SDC comment updated.
Tests: new tb_gs_axi_aw_regbuf (AW scoreboard: exactly-once/order/no-
combinational-AWREADY-bypass/stable-while-stalled); tb_gs_texture_cache
strengthened to distinct-per-byte-bank data + per-bank + full-word
checks. All pass: aw/w regbuf, texture_cache, texture_psmt8_clut,
scanout_diag, ps2_hps_bridge, and the complete f52 replay BYTE-IDENTICAL
(Z 0/307200, COLOR 0/245760).
No Quartus, board, or push from here. Ready for one owner GUI fit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codex review containment/observability fixes (no behavior change, no redesign):
- scanout_lb: add !fs_edge_v to the diagnostic predicate so capture
matches the underflow latch's frame-start CLEAR priority exactly
(no capture on an fs_edge cycle the real latch suppresses).
- bridge: expose a destination snap_valid_q as 0x120[0] — set ON the
payload-capture edge, cleared on synced source-valid deassert — so
valid never leads the bundle by a cycle (was the middle sync stage).
- bridge: forced-synchronizer (SYNCHRONIZER_IDENTIFICATION FORCED) +
dont_merge/preserve on the underflow/read-error/valid chains;
preserve on the bundle capture regs (both domains).
- SDC: stage-0 async cuts on the three sync[0] inputs + the 37-bit
stable bundle hold-false-path + 2ns max_skew + 2ns net_delay, with
fail-closed src==37 / dst!=0 count checks (tile_ram_cdc idiom).
- tb_gs_scanout_diag: +fs_edge-suppression monitor (with coverage that
the coincidence is exercised), +valid-ordering monitor, +snapshot
stability after later misses, +production DUT (V_SOURCE_START=32,
stretch, linear) proving cold-start scan_y=32, +cause/phase packing.
32/32 checks pass.
- doc: production cold start is source row 32 (not 0), base+lookahead
may both assert, and one snapshot narrows but does not prove
starvation vs next_fetch CDC-lag.
Sim set all PASS: focused TB, tb_ps2_hps_bridge, scanout_lb
{binomial,hstretch,psm32_256}, complete f52 replay (FB byte-identical
Z 0/307200, COLOR 0/245760). No Quartus, board, push, or scanout
behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Read-only diagnostic to disambiguate the three causes folded into
LPDDR_STATUS[5]. bit5 (0x02C) semantics are UNCHANGED.
- gs_lpddr_scanout_lb: register a live (rd_errs!=0) flag in the emif
domain (the raw counter never crosses), and capture the FIRST raw
underflow of each video-source-enabled session as a bundled-data
snapshot (scan_y/nf_v/nf_s0 + base-vs-lookahead cause + line_valid +
vphase), held stable until !enable. The existing sticky underflow_v
latch, fetch FSM, pixel path and arbitration are untouched.
- ps2_hps_bridge: independently 2-FF sync the two split live flags;
latch the snapshot on the rising synced-valid edge (coherent bundled
data). New read-only regs 0x120 SCAN_DIAG_STATUS / 0x124
SCAN_DIAG_FIRST (window addr[37:5]==9); 0x118/0x11C reserved slots
untouched.
- top: drive 5 diag nets per scanout arm like scan_err_w; bit5 assign
unchanged.
- tb_gs_scanout_diag (new): provokes AXI RRESP error, cold-start row-0
starvation, first-failure capture + coherent bridge readback, and
clear-via-video-source-disable (17/17 checks).
- Tie off the new bridge inputs in the four .*-instantiating TBs.
Sim set all PASS: focused TB, tb_ps2_hps_bridge, scanout_lb
{binomial,hstretch,psm32_256}, and the complete f52 replay (FB
byte-identical: Z 0/307200, COLOR 0/245760; sum32=0xaad0b94d).
No fix / gray-code / persistence-filter / scanout checksum. No Quartus,
no board, no push.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cuts the lone remaining EMIF setup fail (-0.016 ns, -0.259 skew): the combinational
EMIF gen_p2c_ff -> wr_arb s2_wready -> gs_lpddr_z_rmw next-state (st, endpoint
labelled S_FILL_R via the shared encoded state register).
New gs_axi_w_regbuf: fully-registered one-entry W buffer (Option B per Codex).
- u_wready = !full ONLY (registered occupancy) -> EMIF WREADY never reaches the Z
FSM combinationally. NOT a fall-through skid (no !full-OR-d_wready term).
- Buffers WDATA/WSTRB/WLAST; downstream held stable until accepted; exactly-once.
- AW/B untouched; arbiter bready_q unchanged (still arms on real EMIF W handshake).
- z_rmw may enter B-wait once the beat is buffered -- safe: EMIF cannot return B
until the buffered beat reaches it. Single-beat writes -> the 1-beat/2-cycle
buffer rate is far above the Z write rate (no new FIFO pressure).
Wired in zc_emit between u_z W output (zi_*) and the z_w* ports. New file in sim
Makefile RTL_SRCS + synth QSF (both). Focused tb_gs_axi_w_regbuf: exactly-once/order/
payload scoreboard + no-combinational-bypass check (u_wready===!full incl. full &&
d_wready) + downstream-stable check; standalone target + in make run. Texture-cache
+0.016 paths NOT touched. No simulations or Quartus run.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per Codex review of 471c1af:
1. tb_gs_async_fifo QUAD_WIDTH4 variant now runs at the PRODUCTION 93-bit width
(exercising the odd 23/23/23/24 remainder split) and drives a DISTINCT NONZERO
pattern into every width bank (seq XOR per-bank constants, bank2 inverted), with
the scoreboard checking the FULL reconstructed word. A swapped/broken/zeroed
upper bank now changes the word and trips the scoreboard. Depth-half crossing
(DEPTH=8) + wrap/full coverage retained. mk() is generate-guarded so the 32-bit
variants never elaborate the 93-bit selects.
2. Softened gs_async_fifo comments: 'same total M20K' -> EXPECTED-similar, pending
synthesis (fact -> expectation).
No simulations or Quartus run. Awaiting review before any sim.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Candidate 2 from the accepted structural proposal. Adds a QUAD_WIDTH4_READ
generate branch to gs_async_fifo: like QUADRANT_READ (2 depth x 2 width) but
splits the payload into FOUR width banks, so each preserved read-address launch
register drives ~half the M20K load (targets the 310MHz raddr_hi1_q->mem_hi1
setup family, WNS -0.103). Keeps QUADRANT_READ's proven 2:1 depth OUTPUT selector
unchanged (no new/deeper mux, per the doc's warning). Depth (8192), one-cycle
read latency, ordering, CDC, capacity, and interface are identical.
zc_emit u_req switched QUADRANT_READ->QUAD_WIDTH4_READ. New default-off param
leaves the other two gs_async_fifo instances byte-identical. TB gains a
TEST_QUAD_WIDTH4 variant + standalone Makefile target tb_gs_async_fifo_quad_width4.
NO simulations or Quartus run (per authorization). Awaiting review before any sim.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
My REQ_DEPTH=1024 reduction was wrong: I sized it off a 478 peak from a SMALL
scene. The documented full-f52 replay peak is 6,115 (adjacent frame 3,610; even
2,048 clips) — ch439_timing_convergence_prefit.md. The raster cannot backpressure,
so an undersized FIFO SILENTLY DROPS Z-requests -> corrupt frame. The -0.074 fit
was on an invalid (drop-prone) config. Restore 8,192 as the correctness baseline.
The real timing fix is a registered/skid AR-handshake boundary (arbiter s2_arready
combinational -> texcache F_AR), NOT FIFO shrinking. Packaging that for Codex review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 8K request FIFO was 8x oversized (measured peak ~478; Codex's TB uses 1024).
Its read address routed to 8 spread M20K blocks -> 310MHz EMIF setup miss (-0.103,
MAX_FANOUT can't help: Quartus refuses to duplicate RAM-address regs, Warning 22471).
Shrinking to 1024 (2 blocks): 26.1 fit shows that path GONE, design clk +0.631->+5.693,
RAM 309/358 (86%) -> 277/358 (77%). New worst is a DIFFERENT, skew-dominated EMIF->
texcache-FSM path at -0.074 (TNS -2.032 -> -0.164).
SAFETY GATE before board load: confirm the full zsrt139f52 scene never bursts >1024
Z-requests in flight (peak I have is 478 from a smaller scene). Bump to 2048 if unsure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The fog blend's tex_color*F multiply landed on the texture->color critical path
(u_texcache RAM -> u_tex|tex_color -> mult_126 -> raster_pixel_color_q, 46ns,
-6.165ns setup). Fog is a proven no-op for the current board scene (96.5% F=255),
so pay zero for it here.
FOG_ENABLE (default 1, byte-identical fog) added to gs_stub; each fog mux gated
(FOG_ENABLE && ras_fge) so FOG_ENABLE=0 constant-folds the entire fog cone away
(multiply, FOGCOL adders, s2_fog_f/persp_fog_f5 interp). Threaded through
top_psmct32_raster_demo_bram; set FOG_ENABLE(1'b0) on the active GS_SH3_LPDDR_FB
board arm (elsif at :1086, the one the QSF profile compiles). Fog stays default-on
everywhere else.
Verified: FOG_ENABLE=1 fog TBs unchanged PASS; FOG_ENABLE=0 (new tb_gs_fog_disabled
+ board scene TB) emits raw color = pre-fog datapath; board elaborates clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two fixes for the failing STA (Codex-reviewed):
1. gs_texture_unit lerp8: remove the dead 0/255 clamp. For 8-bit taps and f in
0..15, a+floor((b-a)*f/16) is always in [0,255], so the clamp never fired -
but its comparators formed a 161-node combinational loop that failed
design-clock setup by 42 ps (tap[0][11] -> tex_cache_sel_q). Functionally
identical (tb_gs_texture_bilinear + texture TBs bit-identical PASS).
2. SDC: relax the u_zc_emit gray-code CDC max_skew 2.0 -> 2.5 ns. Post-fog
placement pushed actual skew to 2.010 ns (fails 2.0 by ~10 ps). Real ceiling
is the 3.225 ns EMIF period (one gray bit in flight); 2.5 clears the miss and
stays well under it. Documented relax of a conservative round number.
Acceptance (owner fit): no comb-loop warning, no tap->tex_cache_sel_q path,
all setup + max_skew slacks >= 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
RTL (GS rasterizer, EE core stub, platform bridge, LPDDR4B path), sim regression
(272 TBs), docs, and tooling. Copyrighted PS2 content (BIOS, game code, GS dumps,
and all dump-derived textures/traces) is excluded via .gitignore and stays local.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>