Ch443e: binomial 4th line buffer + prefetch lead-2 (fix lookahead underflow)

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>
This commit is contained in:
2026-07-23 15:24:37 -04:00
parent 0c9b2bf5cd
commit 2e2c1e9ca6
7 changed files with 307470 additions and 20 deletions
+64 -20
View File
@@ -116,10 +116,16 @@ module gs_lpddr_scanout_lb #(
assign arsize = 3'b101; // 32 bytes
// Two line buffers for legacy/linear scanout; Ch438 enables a third so the
// previous, current, and next source rows are resident simultaneously.
// previous, current, and next source rows are resident simultaneously. Ch443e
// adds a FOURTH buffer for BINOMIAL only: the 3x3 filter needs r-1/r/r+1 resident
// (3 buffers), so to lead the prefetch by 2 (fetch r+2 while displaying r) without
// overwriting r-1 (still needed) a 4th rotating buffer is required. Non-binomial
// configs leave lb3 unused (pruned) and keep the legacy 2/3-buffer, lead-1 behavior.
localparam int NBUF = BINOMIAL_3X3_FILTER ? 4 : 3; // buffers actually rotated
logic [255:0] lb0 [0:ROW_BEATS-1];
logic [255:0] lb1 [0:ROW_BEATS-1];
logic [255:0] lb2 [0:ROW_BEATS-1];
logic [255:0] lb3 [0:ROW_BEATS-1];
// ================= video side (video_clk) =================
// No miss-prone request toggle. The video side just exposes the current
@@ -133,7 +139,7 @@ module gs_lpddr_scanout_lb #(
// "disp_buf" lags by one cycle and corrupts col 0 of each line.
logic [$clog2(N_ROWS):0] stretch_src_y_q;
logic [3:0] stretch_vphase_q;
localparam int V_SOURCE_BUF = V_SOURCE_START % 3;
localparam int V_SOURCE_BUF = V_SOURCE_START % NBUF;
logic [1:0] stretch_buf_q;
logic in_window_v_q;
always_ff @(posedge video_clk) begin
@@ -157,8 +163,8 @@ module gs_lpddr_scanout_lb #(
else begin
stretch_src_y_q <= stretch_src_y_q + 1'b1;
stretch_vphase_q <= stretch_vphase_q - 1'b1;
stretch_buf_q <= (stretch_buf_q == 2'd2) ? 2'd0
: stretch_buf_q + 1'b1;
stretch_buf_q <= (stretch_buf_q == 2'(NBUF-1)) ? 2'd0
: stretch_buf_q + 1'b1;
end
end
end
@@ -236,11 +242,12 @@ module gs_lpddr_scanout_lb #(
logic [255:0] lb0_cache0_q, lb0_cache1_q;
logic [255:0] lb1_cache0_q, lb1_cache1_q;
logic [255:0] lb2_cache0_q, lb2_cache1_q;
logic [255:0] lb3_cache0_q, lb3_cache1_q; // Ch443e — binomial 4th buffer
logic blank_prime_q;
logic [RB_BITS-1:0] active_beat_q;
logic [RB_BITS-1:0] video_rd_addr_q, video_rd_tag_q;
logic video_rd_req_q, video_rd_valid_q;
logic [255:0] lb0_video_rd_q, lb1_video_rd_q, lb2_video_rd_q;
logic [255:0] lb0_video_rd_q, lb1_video_rd_q, lb2_video_rd_q, lb3_video_rd_q;
// Keep the inferred line-buffer read ports canonical: exactly one
// unconditional registered address and one registered data output
@@ -250,6 +257,7 @@ module gs_lpddr_scanout_lb #(
lb0_video_rd_q <= lb0[video_rd_addr_q];
lb1_video_rd_q <= lb1[video_rd_addr_q];
lb2_video_rd_q <= lb2[video_rd_addr_q];
lb3_video_rd_q <= lb3[video_rd_addr_q]; // Ch443e — binomial 4th buffer read port
video_rd_tag_q <= video_rd_addr_q;
video_rd_valid_q <= video_rd_req_q;
end
@@ -263,10 +271,12 @@ module gs_lpddr_scanout_lb #(
lb0_cache1_q <= lb0_video_rd_q;
lb1_cache1_q <= lb1_video_rd_q;
lb2_cache1_q <= lb2_video_rd_q;
lb3_cache1_q <= lb3_video_rd_q;
end else begin
lb0_cache0_q <= lb0_video_rd_q;
lb1_cache0_q <= lb1_video_rd_q;
lb2_cache0_q <= lb2_video_rd_q;
lb3_cache0_q <= lb3_video_rd_q;
end
end
@@ -297,31 +307,35 @@ module gs_lpddr_scanout_lb #(
// opposite cache slot still contains the preceding
// beat; by the time lane seven needs x+1 it contains
// the newly fetched successor beat.
// Ch443e — modulo-4 rotation across FOUR buffers. Current row is
// buffer scan_buf3; prev=(b-1)%4, next=(b+1)%4; the 4th buffer
// (b+2)%4 is being prefetched (row r+2, lead-2) and is not read here.
// First row clamps prev->cur; last row clamps next->cur.
case (scan_buf3)
2'd0: begin
2'd0: begin // cur=lb0 prev=lb3 next=lb1
prv_word_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb0_cache1_q : lb0_cache0_q)
: (col_beat[0] ? lb2_cache1_q : lb2_cache0_q);
: (col_beat[0] ? lb3_cache1_q : lb3_cache0_q);
cur_word_q <= col_beat[0] ? lb0_cache1_q : lb0_cache0_q;
nxt_word_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb0_cache1_q : lb0_cache0_q)
: (col_beat[0] ? lb1_cache1_q : lb1_cache0_q);
prv_left_px_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb0_cache0_q[255:224] : lb0_cache1_q[255:224])
: (col_beat[0] ? lb2_cache0_q[255:224] : lb2_cache1_q[255:224]);
: (col_beat[0] ? lb3_cache0_q[255:224] : lb3_cache1_q[255:224]);
cur_left_px_q <= col_beat[0] ? lb0_cache0_q[255:224] : lb0_cache1_q[255:224];
nxt_left_px_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb0_cache0_q[255:224] : lb0_cache1_q[255:224])
: (col_beat[0] ? lb1_cache0_q[255:224] : lb1_cache1_q[255:224]);
prv_look_px_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb0_cache0_q[31:0] : lb0_cache1_q[31:0])
: (col_beat[0] ? lb2_cache0_q[31:0] : lb2_cache1_q[31:0]);
: (col_beat[0] ? lb3_cache0_q[31:0] : lb3_cache1_q[31:0]);
cur_look_px_q <= col_beat[0] ? lb0_cache0_q[31:0] : lb0_cache1_q[31:0];
nxt_look_px_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb0_cache0_q[31:0] : lb0_cache1_q[31:0])
: (col_beat[0] ? lb1_cache0_q[31:0] : lb1_cache1_q[31:0]);
end
2'd1: begin
2'd1: begin // cur=lb1 prev=lb0 next=lb2
prv_word_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb1_cache1_q : lb1_cache0_q)
: (col_beat[0] ? lb0_cache1_q : lb0_cache0_q);
@@ -344,27 +358,50 @@ module gs_lpddr_scanout_lb #(
? (col_beat[0] ? lb1_cache0_q[31:0] : lb1_cache1_q[31:0])
: (col_beat[0] ? lb2_cache0_q[31:0] : lb2_cache1_q[31:0]);
end
default: begin
2'd2: begin // cur=lb2 prev=lb1 next=lb3
prv_word_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb2_cache1_q : lb2_cache0_q)
: (col_beat[0] ? lb1_cache1_q : lb1_cache0_q);
cur_word_q <= col_beat[0] ? lb2_cache1_q : lb2_cache0_q;
nxt_word_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb2_cache1_q : lb2_cache0_q)
: (col_beat[0] ? lb0_cache1_q : lb0_cache0_q);
: (col_beat[0] ? lb3_cache1_q : lb3_cache0_q);
prv_left_px_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb2_cache0_q[255:224] : lb2_cache1_q[255:224])
: (col_beat[0] ? lb1_cache0_q[255:224] : lb1_cache1_q[255:224]);
cur_left_px_q <= col_beat[0] ? lb2_cache0_q[255:224] : lb2_cache1_q[255:224];
nxt_left_px_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb2_cache0_q[255:224] : lb2_cache1_q[255:224])
: (col_beat[0] ? lb0_cache0_q[255:224] : lb0_cache1_q[255:224]);
: (col_beat[0] ? lb3_cache0_q[255:224] : lb3_cache1_q[255:224]);
prv_look_px_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb2_cache0_q[31:0] : lb2_cache1_q[31:0])
: (col_beat[0] ? lb1_cache0_q[31:0] : lb1_cache1_q[31:0]);
cur_look_px_q <= col_beat[0] ? lb2_cache0_q[31:0] : lb2_cache1_q[31:0];
nxt_look_px_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb2_cache0_q[31:0] : lb2_cache1_q[31:0])
: (col_beat[0] ? lb3_cache0_q[31:0] : lb3_cache1_q[31:0]);
end
default: begin // cur=lb3 prev=lb2 next=lb0
prv_word_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb3_cache1_q : lb3_cache0_q)
: (col_beat[0] ? lb2_cache1_q : lb2_cache0_q);
cur_word_q <= col_beat[0] ? lb3_cache1_q : lb3_cache0_q;
nxt_word_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb3_cache1_q : lb3_cache0_q)
: (col_beat[0] ? lb0_cache1_q : lb0_cache0_q);
prv_left_px_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb3_cache0_q[255:224] : lb3_cache1_q[255:224])
: (col_beat[0] ? lb2_cache0_q[255:224] : lb2_cache1_q[255:224]);
cur_left_px_q <= col_beat[0] ? lb3_cache0_q[255:224] : lb3_cache1_q[255:224];
nxt_left_px_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb3_cache0_q[255:224] : lb3_cache1_q[255:224])
: (col_beat[0] ? lb0_cache0_q[255:224] : lb0_cache1_q[255:224]);
prv_look_px_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb3_cache0_q[31:0] : lb3_cache1_q[31:0])
: (col_beat[0] ? lb2_cache0_q[31:0] : lb2_cache1_q[31:0]);
cur_look_px_q <= col_beat[0] ? lb3_cache0_q[31:0] : lb3_cache1_q[31:0];
nxt_look_px_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb3_cache0_q[31:0] : lb3_cache1_q[31:0])
: (col_beat[0] ? lb0_cache0_q[31:0] : lb0_cache1_q[31:0]);
end
endcase
@@ -656,9 +693,9 @@ module gs_lpddr_scanout_lb #(
// cycle per beat and produced sustained line-buffer underflow on hardware.
// Data/address registers intentionally have no reset; the reset write-
// enables qualify them.
logic [255:0] lb0_wdata_q, lb1_wdata_q, lb2_wdata_q;
logic [RB_BITS-1:0] lb0_waddr_q, lb1_waddr_q, lb2_waddr_q;
logic lb0_we_q, lb1_we_q, lb2_we_q;
logic [255:0] lb0_wdata_q, lb1_wdata_q, lb2_wdata_q, lb3_wdata_q;
logic [RB_BITS-1:0] lb0_waddr_q, lb1_waddr_q, lb2_waddr_q, lb3_waddr_q;
logic lb0_we_q, lb1_we_q, lb2_we_q, lb3_we_q; // Ch443e — binomial 4th buffer
// Ch442 diag: registered single-bit reduction of the read-error counter (emif domain).
// Diagnostic-only; nothing downstream reads it, so it cannot perturb the fetch FSM.
logic rd_err_nz_q;
@@ -668,14 +705,14 @@ module gs_lpddr_scanout_lb #(
if (!axi_rst_n) begin
fs_sync_e <= 3'd0;
disp_row_s0 <= ($clog2(N_ROWS)+1)'(V_SOURCE_START);
disp_row_limit_e <= ($clog2(N_ROWS)+1)'(V_SOURCE_START + 1);
disp_row_limit_e <= ($clog2(N_ROWS)+1)'(V_SOURCE_START + (BINOMIAL_3X3_FILTER ? 2 : 1));
next_fetch <= ($clog2(N_ROWS)+1)'(V_SOURCE_START);
next_fetch_buf <= BINOMIAL_3X3_FILTER ? 2'(V_SOURCE_BUF)
: {1'b0, 1'(V_SOURCE_START)};
lst <= L_IDLE; araddr <= '0; arvalid <= 1'b0; rready <= 1'b0;
cur_row <= '0; cur_buf <= 2'd0; beat <= '0;
line_valid <= 1'b0; rd_errs <= 32'd0; fs_pending <= 1'b0;
lb0_we_q <= 1'b0; lb1_we_q <= 1'b0; lb2_we_q <= 1'b0;
lb0_we_q <= 1'b0; lb1_we_q <= 1'b0; lb2_we_q <= 1'b0; lb3_we_q <= 1'b0;
rd_err_nz_q <= 1'b0;
end else begin
fs_sync_e <= {fs_sync_e[1:0], frame_start};
@@ -684,7 +721,10 @@ module gs_lpddr_scanout_lb #(
// second CDC stage, but removes disp_row -> (+1) -> compare -> araddr
// enable from one 310 MHz cycle (the post-alpha fit's -0.125 ns family).
// The extra bit represents N_ROWS exactly on the final display row.
disp_row_limit_e <= disp_row_s0 + 1'b1;
// Ch443e — BINOMIAL leads by 2 (fetch disp_row+2 while displaying disp_row) so
// the 3x3 filter's lookahead row (disp_row+1) is FULLY resident before it is read;
// the 4th rotating buffer holds the in-flight disp_row+2. Legacy stays lead-1.
disp_row_limit_e <= disp_row_s0 + (BINOMIAL_3X3_FILTER ? 2'd2 : 2'd1);
// Ch439c — RAM-local response pipeline. Commit the response
// captured on the preceding cycle while the AXI FSM advances to
// (or waits for) the next single-beat read. This keeps the
@@ -694,9 +734,11 @@ module gs_lpddr_scanout_lb #(
if (lb0_we_q) lb0[lb0_waddr_q] <= lb0_wdata_q;
if (lb1_we_q) lb1[lb1_waddr_q] <= lb1_wdata_q;
if (lb2_we_q) lb2[lb2_waddr_q] <= lb2_wdata_q;
if (lb3_we_q) lb3[lb3_waddr_q] <= lb3_wdata_q;
lb0_we_q <= 1'b0;
lb1_we_q <= 1'b0;
lb2_we_q <= 1'b0;
lb3_we_q <= 1'b0;
// Ch442 diag: register the read-error-nonzero flag (emif domain). One cycle of lag
// vs rd_errs is irrelevant for a stuck-nonzero indicator; keeps a clean launch flop.
rd_err_nz_q <= (rd_errs != 32'd0);
@@ -734,6 +776,7 @@ module gs_lpddr_scanout_lb #(
lb0_we_q <= 1'b0;
lb1_we_q <= 1'b0;
lb2_we_q <= 1'b0;
lb3_we_q <= 1'b0;
if (fs_pending || fs_edge_e) begin
// Ch443c (Codex): a vsync restart is pending. This single-beat AXI
// transaction is COMPLETE (rvalid accepted), so it is protocol-safe to
@@ -748,6 +791,7 @@ module gs_lpddr_scanout_lb #(
case (cur_buf)
2'd1: begin lb1_wdata_q <= rdata; lb1_waddr_q <= beat[RB_BITS-1:0]; lb1_we_q <= 1'b1; end
2'd2: begin lb2_wdata_q <= rdata; lb2_waddr_q <= beat[RB_BITS-1:0]; lb2_we_q <= 1'b1; end
2'd3: begin lb3_wdata_q <= rdata; lb3_waddr_q <= beat[RB_BITS-1:0]; lb3_we_q <= 1'b1; end
default: begin lb0_wdata_q <= rdata; lb0_waddr_q <= beat[RB_BITS-1:0]; lb0_we_q <= 1'b1; end
endcase
if (beat == ROW_BEATS-1) begin
@@ -771,7 +815,7 @@ module gs_lpddr_scanout_lb #(
line_valid <= 1'b1;
next_fetch <= next_fetch + 1'b1; // rows 0..next_fetch are now loaded
if (BINOMIAL_3X3_FILTER)
next_fetch_buf <= (next_fetch_buf == 2'd2) ? 2'd0
next_fetch_buf <= (next_fetch_buf == 2'd3) ? 2'd0 // Ch443e — mod-4 rotation
: next_fetch_buf + 1'b1;
else
next_fetch_buf <= {1'b0, ~next_fetch_buf[0]};