Files
retroDE_ps2/rtl/gif_gs/gs_lpddr_scanout_lb.sv
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

780 lines
45 KiB
Systemverilog

// ============================================================================
// gs_lpddr_scanout_lb.sv (Ch321 Brick 2)
//
// LINE-BUFFER LPDDR4B scanout — the architectural successor to the whole-frame
// cache (gs_lpddr_scanout). Instead of mirroring the entire framebuffer in
// on-chip RAM (which defeats the point of putting the FB in LPDDR), this holds
// just two scanlines (three with the optional Ch438 low-pass): it displays row
// L while prefetching row L+1. On-chip cost is O(width), not O(width*height).
//
// NARROW SCOPE (Ch321): the 128x128 PSMCT16 demo. The frame is LINEAR (the GS
// writer mirrors the rasterizer's linear flush addresses), display window at
// origin, 1:1 (MAG off) — so the reader serves pixel (col=pixel_x, line=pixel_y)
// directly when inside the window. No general MAG/window handling beyond that.
//
// Two clock domains:
// axi_clk (emif_clk) — AXI4 burst-read one row (ROW_BEATS beats) into a buffer
// video_clk (design) — pixel_x/pixel_y index the active line buffer -> r/g/b
//
// Prefetch handshake: on each new display line (and at frame start) the video
// side requests the next FB row via a toggle; the axi side fills the OTHER
// buffer. `underflow` flags any pixel read before its row finished loading.
// ============================================================================
`timescale 1ns/1ps
module gs_lpddr_scanout_lb #(
parameter [29:0] FB_BASE = 30'd0,
parameter int STRIDE_BYTES = 256, // PSMCT16 128px*2B=256; PSMCT32 128px*4B=512
parameter int ROW_BEATS = 8, // STRIDE_BYTES / 32 (PSMCT16 128px=8; PSMCT32 128px=16)
parameter int N_ROWS = 128,
// Ch327a — PSMCT32 (ABGR8888, 8 px/256-bit beat) vs the original PSMCT16 (RGBA5551,
// 16 px/beat). The Ch326 LPDDR-only spill framebuffer is PSMCT32 @ COLOR_SPILL_BASE, so the
// line-buffer must decode it — NOT a config flip of the Ch321 PSMCT16/FB-at-0 path.
parameter bit PSMCT32 = 1'b0,
// Ch418 — the captured SH3 DISPLAY2 is 512 source pixels wide
// (FBW=8) and MAGH=4, i.e. five VCKs per source sample. The board
// emits a 640-pixel active line, so reducing that authentic 2560-VCK
// display domain to VGA requires the exact nearest-neighbour map
// source_x = floor(display_x * 4 / 5).
// Keep this opt-in: legacy demos and directed scanout tests remain 1:1.
// The implementation below is a five-state phase accumulator, not an
// inferred divider, and therefore adds no wide arithmetic timing cone.
parameter bit H_STRETCH_5_TO_4 = 1'b0,
// Captured SH3 interlace presentation: DISPLAY2.DH=895 represents
// 448 source lines, beginning at DISPFB2.DBY=32. Mapping those lines
// onto the board's 480-line active raster is source_y =
// V_SOURCE_START + floor(display_y*14/15). As with horizontal scale,
// this remains opt-in and uses a tiny phase accumulator.
parameter int V_SOURCE_START = 0,
parameter bit V_STRETCH_15_TO_14 = 1'b0,
// Ch436 — optional linear reconstruction between the two resident source
// rows. The 15:14 mapper already keeps row L and L+1 in the alternating
// line buffers, so this adds no framebuffer traffic or line storage.
parameter bit V_LINEAR_FILTER = 1'b0,
// Ch437 — horizontal linear reconstruction for the 5:4 presentation map.
// A two-beat register cache provides x and x+1 from each resident row while
// retaining ONE read port per physical line buffer. H_SOURCE_PIXELS is the
// active source width (512 for SH3 DISPLAY2); zero means the physical stride.
parameter bit H_LINEAR_FILTER = 1'b0,
parameter int H_SOURCE_PIXELS = 0,
// Ch438 — separable [1 2 1]/4 low-pass reconstruction in source space.
// Three rotating line buffers retain rows y-1/y/y+1; the horizontal taps
// reuse the Ch437 two-beat cache, so every physical RAM still has exactly
// one registered read port. This is mutually exclusive with the Ch436/437
// linear filters in the SH3 profile and uses only adds plus shifts.
parameter bit BINOMIAL_3X3_FILTER = 1'b0
)(
// ---- AXI read clock domain (emif_clk) ----
input logic axi_clk,
input logic axi_rst_n,
input logic enable, // 1 = active (prefetch + serve)
// ---- video clock domain (design_clk) ----
input logic video_clk,
input logic frame_start, // vsync pulse/level (synced internally)
input logic [11:0] pixel_x, // raster column (display)
input logic [11:0] pixel_y, // raster line (display)
input logic in_window, // PCRTC displayed-frame window gate
output logic [7:0] r,
output logic [7:0] g,
output logic [7:0] b,
// ---- status (axi_clk domain; bridge syncs) ----
output logic line_valid, // at least one row has been loaded
output logic underflow, // a pixel was read before its row was ready (sticky)
output logic [31:0] rd_errs, // non-OKAY read responses (cumulative)
// ---- AXI4 read channel to the EMIF user port (axi_clk, 256-bit) ----
output logic [29:0] araddr,
output logic [1:0] arburst,
output logic [6:0] arid,
output logic [7:0] arlen,
output logic [2:0] arsize,
output logic arvalid,
input logic arready,
input logic [255:0] rdata,
input logic [1:0] rresp,
input logic rlast,
input logic rvalid,
output logic rready
);
localparam int RB_BITS = $clog2(ROW_BEATS); // 3 for 8
assign arburst = 2'b01; // INCR
assign arid = 7'd3; // distinct: writer=0, probe=1, frame-cache=2, line-buf=3
assign arlen = 8'd0; // SINGLE-BEAT per read — the only AXI read pattern proven on this
// EMIF (writer/probe/frame-cache all use arlen=0). A multi-beat
// burst (arlen=ROW_BEATS-1) was untested and garbled on hardware.
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.
logic [255:0] lb0 [0:ROW_BEATS-1];
logic [255:0] lb1 [0:ROW_BEATS-1];
logic [255:0] lb2 [0:ROW_BEATS-1];
// ================= video side (video_clk) =================
// No miss-prone request toggle. The video side just exposes the current
// in-window display row; the axi side free-runs, fetching rows sequentially
// and staying one row ahead (see below). disp_row_v resets on vsync.
logic [$clog2(N_ROWS):0] disp_row_v;
logic [2:0] fs_sync_v;
wire fs_edge_v = fs_sync_v[1] && !fs_sync_v[2]; // RISING edge only: one reset per frame_start pulse
// The buffer holding display line L is L&1 (row L is fetched into L&1). Select
// it DIRECTLY from pixel_y[0] (tracks the current pixel) — a separately-registered
// "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;
logic [1:0] stretch_buf_q;
logic in_window_v_q;
always_ff @(posedge video_clk) begin
if (!enable) begin
stretch_src_y_q <= ($clog2(N_ROWS)+1)'(V_SOURCE_START);
stretch_vphase_q <= 4'd0;
stretch_buf_q <= 2'(V_SOURCE_BUF);
in_window_v_q <= 1'b0;
end else begin
in_window_v_q <= in_window;
if (fs_edge_v) begin
stretch_src_y_q <= ($clog2(N_ROWS)+1)'(V_SOURCE_START);
stretch_vphase_q <= 4'd0;
stretch_buf_q <= 2'(V_SOURCE_BUF);
end else if (V_STRETCH_15_TO_14 && in_window_v_q && !in_window) begin
// End of one output line. phase 0 repeats the current
// source line once; phases 14..1 advance while counting
// down, giving 15 output lines per 14 source lines.
if (stretch_vphase_q == 4'd0)
stretch_vphase_q <= 4'd14;
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;
end
end
end
end
wire [$clog2(N_ROWS):0] scan_y = V_STRETCH_15_TO_14
? stretch_src_y_q
: ($clog2(N_ROWS)+1)'(pixel_y);
wire disp_buf = scan_y[0];
wire [1:0] scan_buf3 = stretch_buf_q;
always_ff @(posedge video_clk) begin
if (!enable) begin
disp_row_v <= ($clog2(N_ROWS)+1)'(V_SOURCE_START); fs_sync_v <= 3'd0;
end else begin
fs_sync_v <= {fs_sync_v[1:0], frame_start};
if (fs_edge_v) disp_row_v <= ($clog2(N_ROWS)+1)'(V_SOURCE_START);
// Publish a stretch-row advance during horizontal blanking, not
// only after the first active pixel. The AXI side then has the
// full blank interval to replace the retired parity buffer with
// row L+1 before linear scanout needs it.
else if (scan_y < ($clog2(N_ROWS)+1)'(N_ROWS))
disp_row_v <= scan_y;
end
end
// Ch418 horizontal presentation mapper. At output x=0 the state is
// {src=0,phase=0}; successive active clocks produce source columns
// 0,0,1,2,3,4,4,5,6,7,...,511
// for output columns 0..639. Blanking resets the state before each line.
// phase==0 is the sole repeat; all other phases advance source_x. This is
// algebraically identical to floor(x*4/5), using only a 3-bit decrement
// and a 12-bit increment.
logic [11:0] stretch_src_x_q;
logic [2:0] stretch_phase_q;
always_ff @(posedge video_clk) begin
if (!enable || !in_window) begin
stretch_src_x_q <= 12'd0;
stretch_phase_q <= 3'd0;
end else if (H_STRETCH_5_TO_4) begin
if (stretch_phase_q == 3'd0) begin
stretch_phase_q <= 3'd4;
end else begin
stretch_src_x_q <= stretch_src_x_q + 12'd1;
stretch_phase_q <= stretch_phase_q - 3'd1;
end
end
end
wire [11:0] scan_x = H_STRETCH_5_TO_4 ? stretch_src_x_q : pixel_x;
// Registered (sync-read) pixel: pick buffer + beat + within-beat lane from scan_x.
// PSMCT32: 8 px/256-bit beat -> beat = scan_x>>3, lane = scan_x[2:0] (32-bit).
// PSMCT16: 16 px/beat -> beat = scan_x>>4, lane = scan_x[3:0] (16-bit).
localparam int PXSH = PSMCT32 ? 3 : 4; // px-per-beat shift
localparam int PX_PER_ROW = PSMCT32 ? (STRIDE_BYTES/4) : (STRIDE_BYTES/2);
localparam int H_SOURCE_PX = (H_SOURCE_PIXELS != 0) ? H_SOURCE_PIXELS : PX_PER_ROW;
localparam int H_LAST_BEAT = (H_SOURCE_PX-1) >> PXSH;
wire [RB_BITS-1:0] col_beat = scan_x[RB_BITS+PXSH-1 -: RB_BITS];
wire [3:0] col_lane = PSMCT32 ? {1'b0, scan_x[2:0]} : scan_x[3:0];
logic [255:0] prv_word_q, cur_word_q, nxt_word_q;
// Only lane zero of the lookahead beat can be selected: x+1 crosses a
// beat exactly when x is its final lane. Keep 32 bits, not another pair
// of 256-bit payload registers.
logic [31:0] prv_left_px_q, cur_left_px_q, nxt_left_px_q;
logic [31:0] prv_look_px_q, cur_look_px_q, nxt_look_px_q;
logic [3:0] lane_q, vphase_q;
logic [2:0] hphase_q;
logic first_source_x_q, last_source_x_q;
logic in_q;
generate
if (H_LINEAR_FILTER || BINOMIAL_3X3_FILTER) begin : g_hlinear_cache
// Slot parity equals beat parity. Horizontal blanking continually
// primes beats 0 and 1 after the vertical row selector settles.
// On entry to each subsequent beat, the retired slot is refilled
// with beat+1. No second line-buffer read port is required.
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 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;
// Keep the inferred line-buffer read ports canonical: exactly one
// unconditional registered address and one registered data output
// per physical array. The prior conditional multi-address reads
// made Quartus expand both arrays into 30,720 flip-flops.
always_ff @(posedge video_clk) begin
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];
video_rd_tag_q <= video_rd_addr_q;
video_rd_valid_q <= video_rd_req_q;
end
always_ff @(posedge video_clk) begin
// The registered RAM response arrives with its beat tag two
// control edges after the request. Store physical lb0/lb1
// independently; row parity is selected only at pixel output.
if (video_rd_valid_q) begin
if (video_rd_tag_q[0]) begin
lb0_cache1_q <= lb0_video_rd_q;
lb1_cache1_q <= lb1_video_rd_q;
lb2_cache1_q <= lb2_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;
end
end
if (!enable) begin
blank_prime_q <= 1'b0;
active_beat_q <= '0;
video_rd_addr_q <= '0;
video_rd_req_q <= 1'b0;
in_q <= 1'b0;
end else if (!in_window) begin
// Alternate requests for beats 0 and 1 throughout blank.
// VGA supplies far more than the four clocks needed for
// both registered responses to settle into the cache.
video_rd_addr_q <= blank_prime_q ? RB_BITS'(1) : '0;
video_rd_req_q <= 1'b1;
blank_prime_q <= ~blank_prime_q;
active_beat_q <= '0;
in_q <= 1'b0;
end else begin
video_rd_req_q <= 1'b0;
// Source x and x+1 come from the two cached beats. The
// current beat is selected by parity; the opposite slot
// is its already-fetched successor.
if (BINOMIAL_3X3_FILTER) begin
// Select the three rotating physical rows. The first
// displayed source row clamps y-1 to y; the final row
// similarly clamps y+1. For a lane-zero sample the
// opposite cache slot still contains the preceding
// beat; by the time lane seven needs x+1 it contains
// the newly fetched successor beat.
case (scan_buf3)
2'd0: begin
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);
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]);
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]);
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
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);
cur_word_q <= col_beat[0] ? lb1_cache1_q : lb1_cache0_q;
nxt_word_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb1_cache1_q : lb1_cache0_q)
: (col_beat[0] ? lb2_cache1_q : lb2_cache0_q);
prv_left_px_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb1_cache0_q[255:224] : lb1_cache1_q[255:224])
: (col_beat[0] ? lb0_cache0_q[255:224] : lb0_cache1_q[255:224]);
cur_left_px_q <= col_beat[0] ? lb1_cache0_q[255:224] : lb1_cache1_q[255:224];
nxt_left_px_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb1_cache0_q[255:224] : lb1_cache1_q[255:224])
: (col_beat[0] ? lb2_cache0_q[255:224] : lb2_cache1_q[255:224]);
prv_look_px_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb1_cache0_q[31:0] : lb1_cache1_q[31:0])
: (col_beat[0] ? lb0_cache0_q[31:0] : lb0_cache1_q[31:0]);
cur_look_px_q <= col_beat[0] ? lb1_cache0_q[31:0] : lb1_cache1_q[31:0];
nxt_look_px_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (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
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);
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]);
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] ? lb0_cache0_q[31:0] : lb0_cache1_q[31:0]);
end
endcase
end else if (disp_buf) begin
prv_word_q <= '0;
cur_word_q <= col_beat[0] ? lb1_cache1_q : lb1_cache0_q;
nxt_word_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb1_cache1_q : lb1_cache0_q)
: (col_beat[0] ? lb0_cache1_q : lb0_cache0_q);
cur_look_px_q <= col_beat[0] ? lb1_cache0_q[31:0] : lb1_cache1_q[31:0];
nxt_look_px_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb1_cache0_q[31:0] : lb1_cache1_q[31:0])
: (col_beat[0] ? lb0_cache0_q[31:0] : lb0_cache1_q[31:0]);
end else begin
prv_word_q <= '0;
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);
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
if (!BINOMIAL_3X3_FILTER) begin
prv_left_px_q <= '0;
cur_left_px_q <= '0;
nxt_left_px_q <= '0;
prv_look_px_q <= '0;
end
lane_q <= col_lane;
vphase_q <= stretch_vphase_q;
hphase_q <= stretch_phase_q;
first_source_x_q <= (scan_x == 12'd0);
last_source_x_q <= (scan_x >= 12'(H_SOURCE_PX-1));
in_q <= (pixel_x < PX_PER_ROW) && (pixel_y < N_ROWS);
if ((col_beat != active_beat_q) &&
(col_beat < RB_BITS'(H_LAST_BEAT))) begin
active_beat_q <= col_beat;
video_rd_addr_q <= col_beat + 1'b1;
video_rd_req_q <= 1'b1;
end
end
end
end else begin : g_direct_read
always_ff @(posedge video_clk) begin
// One video read from each physical buffer supplies the current row
// and its already-prefetched successor in parallel. Select them in
// this same registered stage so parity cannot lag at a line boundary.
if (disp_buf) begin
prv_word_q <= '0;
cur_word_q <= lb1[col_beat];
nxt_word_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? lb1[col_beat] : lb0[col_beat];
end else begin
prv_word_q <= '0;
cur_word_q <= lb0[col_beat];
nxt_word_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? lb0[col_beat] : lb1[col_beat];
end
cur_look_px_q <= '0;
nxt_look_px_q <= '0;
prv_left_px_q <= '0;
cur_left_px_q <= '0;
nxt_left_px_q <= '0;
prv_look_px_q <= '0;
lane_q <= col_lane;
vphase_q <= stretch_vphase_q;
hphase_q <= stretch_phase_q;
first_source_x_q <= 1'b1;
last_source_x_q <= 1'b1;
in_q <= in_window && (pixel_x < PX_PER_ROW) && (pixel_y < N_ROWS);
end
end
endgenerate
// PSMCT32 ABGR8888 (r=[7:0],g=[15:8],b=[23:16]) — matches gs_lpddr_scanout (frame-cache).
wire [31:0] px32_prv = prv_word_q[lane_q[2:0]*32 +: 32];
wire [31:0] px32_cur = cur_word_q[lane_q[2:0]*32 +: 32];
wire [31:0] px32_nxt = nxt_word_q[lane_q[2:0]*32 +: 32];
wire [31:0] px32_prv_left = first_source_x_q ? px32_prv :
((lane_q[2:0] == 3'd0)
? prv_left_px_q
: prv_word_q[(lane_q[2:0]-1'b1)*32 +: 32]);
wire [31:0] px32_cur_left = first_source_x_q ? px32_cur :
((lane_q[2:0] == 3'd0)
? cur_left_px_q
: cur_word_q[(lane_q[2:0]-1'b1)*32 +: 32]);
wire [31:0] px32_nxt_left = first_source_x_q ? px32_nxt :
((lane_q[2:0] == 3'd0)
? nxt_left_px_q
: nxt_word_q[(lane_q[2:0]-1'b1)*32 +: 32]);
wire [31:0] px32_prv_right = last_source_x_q ? px32_prv :
((lane_q[2:0] == 3'd7)
? prv_look_px_q
: prv_word_q[(lane_q[2:0]+1'b1)*32 +: 32]);
wire [31:0] px32_cur_right = last_source_x_q ? px32_cur :
((lane_q[2:0] == 3'd7)
? cur_look_px_q
: cur_word_q[(lane_q[2:0]+1'b1)*32 +: 32]);
wire [31:0] px32_nxt_right = last_source_x_q ? px32_nxt :
((lane_q[2:0] == 3'd7)
? nxt_look_px_q
: nxt_word_q[(lane_q[2:0]+1'b1)*32 +: 32]);
// PSMCT16 RGBA5551 5-bit lanes expanded to 8-bit.
wire [15:0] px16_prv = prv_word_q[lane_q*16 +: 16];
wire [15:0] px16_cur = cur_word_q[lane_q*16 +: 16];
wire [15:0] px16_nxt = nxt_word_q[lane_q*16 +: 16];
wire [15:0] px16_prv_left = first_source_x_q ? px16_prv :
((lane_q == 4'd0)
? prv_left_px_q[15:0]
: prv_word_q[(lane_q-1'b1)*16 +: 16]);
wire [15:0] px16_cur_left = first_source_x_q ? px16_cur :
((lane_q == 4'd0)
? cur_left_px_q[15:0]
: cur_word_q[(lane_q-1'b1)*16 +: 16]);
wire [15:0] px16_nxt_left = first_source_x_q ? px16_nxt :
((lane_q == 4'd0)
? nxt_left_px_q[15:0]
: nxt_word_q[(lane_q-1'b1)*16 +: 16]);
wire [15:0] px16_prv_right = last_source_x_q ? px16_prv :
((lane_q == 4'd15)
? prv_look_px_q[15:0]
: prv_word_q[(lane_q+1'b1)*16 +: 16]);
wire [15:0] px16_cur_right = last_source_x_q ? px16_cur :
((lane_q == 4'd15)
? cur_look_px_q[15:0]
: cur_word_q[(lane_q+1'b1)*16 +: 16]);
wire [15:0] px16_nxt_right = last_source_x_q ? px16_nxt :
((lane_q == 4'd15)
? nxt_look_px_q[15:0]
: nxt_word_q[(lane_q+1'b1)*16 +: 16]);
wire [7:0] r16_cur = {px16_cur[4:0], px16_cur[4:2]};
wire [7:0] g16_cur = {px16_cur[9:5], px16_cur[9:7]};
wire [7:0] b16_cur = {px16_cur[14:10], px16_cur[14:12]};
wire [7:0] r16_nxt = {px16_nxt[4:0], px16_nxt[4:2]};
wire [7:0] g16_nxt = {px16_nxt[9:5], px16_nxt[9:7]};
wire [7:0] b16_nxt = {px16_nxt[14:10], px16_nxt[14:12]};
wire [7:0] r16_prv = {px16_prv[4:0], px16_prv[4:2]};
wire [7:0] g16_prv = {px16_prv[9:5], px16_prv[9:7]};
wire [7:0] b16_prv = {px16_prv[14:10], px16_prv[14:12]};
wire [7:0] r16_prv_left = {px16_prv_left[4:0], px16_prv_left[4:2]};
wire [7:0] g16_prv_left = {px16_prv_left[9:5], px16_prv_left[9:7]};
wire [7:0] b16_prv_left = {px16_prv_left[14:10], px16_prv_left[14:12]};
wire [7:0] r16_cur_left = {px16_cur_left[4:0], px16_cur_left[4:2]};
wire [7:0] g16_cur_left = {px16_cur_left[9:5], px16_cur_left[9:7]};
wire [7:0] b16_cur_left = {px16_cur_left[14:10], px16_cur_left[14:12]};
wire [7:0] r16_nxt_left = {px16_nxt_left[4:0], px16_nxt_left[4:2]};
wire [7:0] g16_nxt_left = {px16_nxt_left[9:5], px16_nxt_left[9:7]};
wire [7:0] b16_nxt_left = {px16_nxt_left[14:10], px16_nxt_left[14:12]};
wire [7:0] r16_prv_right = {px16_prv_right[4:0], px16_prv_right[4:2]};
wire [7:0] g16_prv_right = {px16_prv_right[9:5], px16_prv_right[9:7]};
wire [7:0] b16_prv_right = {px16_prv_right[14:10], px16_prv_right[14:12]};
wire [7:0] r16_cur_right = {px16_cur_right[4:0], px16_cur_right[4:2]};
wire [7:0] g16_cur_right = {px16_cur_right[9:5], px16_cur_right[9:7]};
wire [7:0] b16_cur_right = {px16_cur_right[14:10], px16_cur_right[14:12]};
wire [7:0] r16_nxt_right = {px16_nxt_right[4:0], px16_nxt_right[4:2]};
wire [7:0] g16_nxt_right = {px16_nxt_right[9:5], px16_nxt_right[9:7]};
wire [7:0] b16_nxt_right = {px16_nxt_right[14:10], px16_nxt_right[14:12]};
function automatic logic [7:0] blend15(
input logic [7:0] cur,
input logic [7:0] nxt,
input logic [3:0] frac
);
logic [12:0] weighted;
begin
if (frac == 4'd0)
blend15 = cur;
else begin
weighted = ((4'd15-frac) * cur) + (frac * nxt) + 13'd7;
blend15 = weighted / 13'd15;
end
end
endfunction
function automatic logic [7:0] blend5(
input logic [7:0] left,
input logic [7:0] right,
input logic [2:0] frac
);
logic [10:0] weighted;
begin
if (frac == 3'd0)
blend5 = left;
else begin
weighted = ((3'd5-frac) * left) + (frac * right) + 11'd2;
blend5 = weighted / 11'd5;
end
end
endfunction
function automatic logic [7:0] binom3(
input logic [7:0] left,
input logic [7:0] center,
input logic [7:0] right
);
logic [9:0] weighted;
begin
weighted = {2'b0,left} + {1'b0,center,1'b0} + {2'b0,right} + 10'd2;
binom3 = weighted[9:2];
end
endfunction
wire [7:0] prv_r = PSMCT32 ? px32_prv[7:0] : r16_prv;
wire [7:0] prv_g = PSMCT32 ? px32_prv[15:8] : g16_prv;
wire [7:0] prv_b = PSMCT32 ? px32_prv[23:16] : b16_prv;
wire [7:0] cur_r = PSMCT32 ? px32_cur[7:0] : r16_cur;
wire [7:0] cur_g = PSMCT32 ? px32_cur[15:8] : g16_cur;
wire [7:0] cur_b = PSMCT32 ? px32_cur[23:16] : b16_cur;
wire [7:0] nxt_r = PSMCT32 ? px32_nxt[7:0] : r16_nxt;
wire [7:0] nxt_g = PSMCT32 ? px32_nxt[15:8] : g16_nxt;
wire [7:0] nxt_b = PSMCT32 ? px32_nxt[23:16] : b16_nxt;
wire [7:0] prv_left_r = PSMCT32 ? px32_prv_left[7:0] : r16_prv_left;
wire [7:0] prv_left_g = PSMCT32 ? px32_prv_left[15:8] : g16_prv_left;
wire [7:0] prv_left_b = PSMCT32 ? px32_prv_left[23:16] : b16_prv_left;
wire [7:0] cur_left_r = PSMCT32 ? px32_cur_left[7:0] : r16_cur_left;
wire [7:0] cur_left_g = PSMCT32 ? px32_cur_left[15:8] : g16_cur_left;
wire [7:0] cur_left_b = PSMCT32 ? px32_cur_left[23:16] : b16_cur_left;
wire [7:0] nxt_left_r = PSMCT32 ? px32_nxt_left[7:0] : r16_nxt_left;
wire [7:0] nxt_left_g = PSMCT32 ? px32_nxt_left[15:8] : g16_nxt_left;
wire [7:0] nxt_left_b = PSMCT32 ? px32_nxt_left[23:16] : b16_nxt_left;
wire [7:0] prv_right_r = PSMCT32 ? px32_prv_right[7:0] : r16_prv_right;
wire [7:0] prv_right_g = PSMCT32 ? px32_prv_right[15:8] : g16_prv_right;
wire [7:0] prv_right_b = PSMCT32 ? px32_prv_right[23:16] : b16_prv_right;
wire [7:0] cur_right_r = PSMCT32 ? px32_cur_right[7:0] : r16_cur_right;
wire [7:0] cur_right_g = PSMCT32 ? px32_cur_right[15:8] : g16_cur_right;
wire [7:0] cur_right_b = PSMCT32 ? px32_cur_right[23:16] : b16_cur_right;
wire [7:0] nxt_right_r = PSMCT32 ? px32_nxt_right[7:0] : r16_nxt_right;
wire [7:0] nxt_right_g = PSMCT32 ? px32_nxt_right[15:8] : g16_nxt_right;
wire [7:0] nxt_right_b = PSMCT32 ? px32_nxt_right[23:16] : b16_nxt_right;
wire [3:0] filter_frac = V_LINEAR_FILTER ? vphase_q : 4'd0;
wire [2:0] hfilter_frac = H_LINEAR_FILTER ? hphase_q : 3'd0;
wire [7:0] left_r = blend15(cur_r, nxt_r, filter_frac);
wire [7:0] left_g = blend15(cur_g, nxt_g, filter_frac);
wire [7:0] left_b = blend15(cur_b, nxt_b, filter_frac);
wire [7:0] right_r = blend15(cur_right_r, nxt_right_r, filter_frac);
wire [7:0] right_g = blend15(cur_right_g, nxt_right_g, filter_frac);
wire [7:0] right_b = blend15(cur_right_b, nxt_right_b, filter_frac);
wire [7:0] linear_out_r = blend5(left_r, right_r, hfilter_frac);
wire [7:0] linear_out_g = blend5(left_g, right_g, hfilter_frac);
wire [7:0] linear_out_b = blend5(left_b, right_b, hfilter_frac);
wire [7:0] bin_prv_r = binom3(prv_left_r, prv_r, prv_right_r);
wire [7:0] bin_prv_g = binom3(prv_left_g, prv_g, prv_right_g);
wire [7:0] bin_prv_b = binom3(prv_left_b, prv_b, prv_right_b);
wire [7:0] bin_cur_r = binom3(cur_left_r, cur_r, cur_right_r);
wire [7:0] bin_cur_g = binom3(cur_left_g, cur_g, cur_right_g);
wire [7:0] bin_cur_b = binom3(cur_left_b, cur_b, cur_right_b);
wire [7:0] bin_nxt_r = binom3(nxt_left_r, nxt_r, nxt_right_r);
wire [7:0] bin_nxt_g = binom3(nxt_left_g, nxt_g, nxt_right_g);
wire [7:0] bin_nxt_b = binom3(nxt_left_b, nxt_b, nxt_right_b);
wire [7:0] out_r = BINOMIAL_3X3_FILTER ? binom3(bin_prv_r, bin_cur_r, bin_nxt_r)
: linear_out_r;
wire [7:0] out_g = BINOMIAL_3X3_FILTER ? binom3(bin_prv_g, bin_cur_g, bin_nxt_g)
: linear_out_g;
wire [7:0] out_b = BINOMIAL_3X3_FILTER ? binom3(bin_prv_b, bin_cur_b, bin_nxt_b)
: linear_out_b;
assign r = !in_q ? 8'd0 : out_r;
assign g = !in_q ? 8'd0 : out_g;
assign b = !in_q ? 8'd0 : out_b;
// ================= axi side (axi_clk) — row fill FSM =================
// free-running prefetcher: fetch rows sequentially, staying <= disp_row+1 ahead.
// disp_row crosses video->axi (slowly-changing; the +1 throttle tolerates a 1-off
// transient). frame_start is edge-detected here to reset next_fetch every frame.
logic [2:0] fs_sync_e;
wire fs_edge_e = fs_sync_e[1] && !fs_sync_e[2]; // RISING edge only: one prefetch restart per frame_start pulse
logic [$clog2(N_ROWS):0] disp_row_s0, disp_row_limit_e;
logic [$clog2(N_ROWS):0] next_fetch; // next row to load (0..N_ROWS)
logic [1:0] next_fetch_buf;
typedef enum logic [1:0] { L_IDLE, L_AR, L_R, L_C } lstate_t;
lstate_t lst;
logic [$clog2(N_ROWS):0] cur_row;
logic [1:0] cur_buf;
logic [RB_BITS:0] beat;
logic fs_pending; // a vsync restart is pending; applied in L_IDLE (never mid-read)
// Ch358 (Codex) — one-entry RESPONSE STAGE: the 26.1 STA leader (WNS -0.884 at 640) was the EMIF read-FIFO
// RAM output driving lb0/lb1's write port in the SAME rvalid cycle (RAM->bus->RAM in one 310MHz period).
// L_R now only CAPTURES {rdata, beat, buf, last} into per-buffer physical write registers. L_C commits the
// selected LB RAM; next_fetch/line_valid advance only after that commit. One outstanding read.
// Ch439b — physical write-port stage. The Ch438 fit exposed a route-only
// r_data_q -> lb1 RAM path at 310 MHz after adding the third line buffer.
// Give each inferred RAM its own data/address/enable launch registers so
// the fitter can place them beside that RAM instead of routing one shared
// 256-bit register bank across all three memories. Capture directly from
// AXI in L_R and commit in L_C: the first Ch439 L_R->L_C->L_W form added a
// 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;
always_ff @(posedge axi_clk) begin
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);
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;
end else begin
fs_sync_e <= {fs_sync_e[1:0], frame_start};
disp_row_s0 <= disp_row_v; // 2-FF sync of the display row
// Register the already-incremented throttle limit. This remains the
// 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;
// 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
// rdata->local-register->RAM timing cut without paying an L_C
// bubble after every beat. The final beat uses L_C only as a
// one-cycle row-end flush before next_fetch becomes visible.
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;
lb0_we_q <= 1'b0;
lb1_we_q <= 1'b0;
lb2_we_q <= 1'b0;
// vsync: mark a prefetch restart. DEFER it to L_IDLE so an in-flight AXI
// read is never aborted mid-handshake (which would deadlock the slave).
if (fs_edge_e) fs_pending <= 1'b1;
case (lst)
L_IDLE: begin
if (fs_pending) begin
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)};
// restart at the captured display source row
fs_pending <= 1'b0;
end else if (enable && (next_fetch < N_ROWS) && (next_fetch <= disp_row_limit_e)) begin
cur_row <= next_fetch;
cur_buf <= next_fetch_buf;
araddr <= FB_BASE + (next_fetch * STRIDE_BYTES);
beat <= '0;
arvalid <= 1'b1;
lst <= L_AR;
end
end
L_AR: begin
if (arready) begin
arvalid <= 1'b0;
rready <= 1'b1;
lst <= L_R;
end
end
L_R: begin
if (rvalid) begin
// Capture directly into the selected RAM-local port
// stage. L_C commits it on the following cycle.
lb0_we_q <= 1'b0;
lb1_we_q <= 1'b0;
lb2_we_q <= 1'b0;
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
default: begin lb0_wdata_q <= rdata; lb0_waddr_q <= beat[RB_BITS-1:0]; lb0_we_q <= 1'b1; end
endcase
if (rresp != 2'b00) rd_errs <= rd_errs + 32'd1;
rready <= 1'b0;
if (beat == ROW_BEATS-1) begin
// The local register captures this last response
// now; L_C flushes it into RAM on the next edge.
lst <= L_C;
end else begin
// Previous behavior inserted L_C here and lost one
// EMIF clock per beat. The RAM-local stage commits
// independently above, so immediately issue the
// next read just as the pre-Ch439 FSM did.
beat <= beat + 1'b1;
araddr <= araddr + 30'd32;
arvalid <= 1'b1;
lst <= L_AR;
end
end
end
L_C: begin
// The global pipeline commit above writes the final beat
// on this edge. Publish the completed row only now.
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 + 1'b1;
else
next_fetch_buf <= {1'b0, ~next_fetch_buf[0]};
lst <= L_IDLE;
end
default: lst <= L_IDLE;
endcase
end
end
// underflow (sticky, video domain): an in-window pixel for line pixel_y is read
// before that row was prefetched. The axi side loads rows 0..next_fetch-1, so row
// pixel_y is ready iff pixel_y < next_fetch. next_fetch crosses axi->video synced
// (slowly-changing; a 1-off transient is harmless). Resets on vsync.
logic [$clog2(N_ROWS):0] nf_s0, nf_v;
logic underflow_v;
always_ff @(posedge video_clk) begin
nf_s0 <= next_fetch; nf_v <= nf_s0;
if (!enable || fs_edge_v) underflow_v <= 1'b0;
else if (in_window && (scan_y < ($clog2(N_ROWS)+1)'(N_ROWS)) &&
((scan_y >= nf_v) ||
((BINOMIAL_3X3_FILTER ||
(V_LINEAR_FILTER && (stretch_vphase_q != 4'd0))) &&
(scan_y + 1'b1 < ($clog2(N_ROWS)+1)'(N_ROWS)) &&
(scan_y + 1'b1 >= nf_v))))
underflow_v <= 1'b1;
end
assign underflow = underflow_v;
endmodule