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>
This commit is contained in:
+191
-16
@@ -14,7 +14,21 @@
|
||||
|
||||
module gs_async_fifo #(
|
||||
parameter int WIDTH = 320, // {addr[31:0], data[255:0], strb[31:0]}
|
||||
parameter int DEPTH = 16 // power of two
|
||||
parameter int DEPTH = 16, // power of two
|
||||
// Infer a synchronous read port when set. This is useful for deep/wide
|
||||
// FIFOs whose bank-select mux cannot meet a fast rclk as an FWFT output.
|
||||
parameter bit REGISTERED_READ = 1'b0,
|
||||
// Ch438 timing cut for very deep/wide registered-read FIFOs. Splitting the
|
||||
// payload into two independently inferred RAMs gives each half its own
|
||||
// preserved read-address launch register. This removes the single 744-load
|
||||
// port-B address net seen on the 93x16K production request FIFO while
|
||||
// preserving depth, order, and one-cycle read behavior.
|
||||
parameter bit BANKED_READ = 1'b0,
|
||||
// Ch439e: split a deep/wide memory in both dimensions. Two depth banks
|
||||
// times two width banks leave each physical read-address copy driving
|
||||
// roughly one quarter of the original M20K tree. The registered outputs
|
||||
// need only a 2:1 depth-bank select; FIFO depth and latency are unchanged.
|
||||
parameter bit QUADRANT_READ = 1'b0
|
||||
) (
|
||||
// write domain
|
||||
input logic wclk,
|
||||
@@ -32,11 +46,52 @@ module gs_async_fifo #(
|
||||
localparam int AW = $clog2(DEPTH);
|
||||
|
||||
logic [WIDTH-1:0] mem [0:DEPTH-1];
|
||||
localparam int BANK_LO_W = WIDTH / 2;
|
||||
localparam int BANK_HI_W = WIDTH - BANK_LO_W;
|
||||
logic [BANK_LO_W-1:0] mem_lo [0:DEPTH-1];
|
||||
logic [BANK_HI_W-1:0] mem_hi [0:DEPTH-1];
|
||||
localparam int HALF_DEPTH = DEPTH / 2;
|
||||
localparam int HALF_AW = AW - 1;
|
||||
logic [BANK_LO_W-1:0] mem_lo0 [0:HALF_DEPTH-1];
|
||||
logic [BANK_LO_W-1:0] mem_lo1 [0:HALF_DEPTH-1];
|
||||
logic [BANK_HI_W-1:0] mem_hi0 [0:HALF_DEPTH-1];
|
||||
logic [BANK_HI_W-1:0] mem_hi1 [0:HALF_DEPTH-1];
|
||||
// Dedicated write-port staging lets the fitter duplicate/place the RAM
|
||||
// address register beside a wide banked memory. Driving every bank
|
||||
// directly from the shared binary pointer created a 310 MHz high-fanout
|
||||
// wbin -> RAM-address path in the 321-bit color FIFO. The opposite-domain
|
||||
// pointer requires two synchronizer cycles before a reader can observe a
|
||||
// write, so committing the RAM one local cycle later is CDC-safe.
|
||||
logic [AW-1:0] waddr_q;
|
||||
logic [WIDTH-1:0] wdata_q;
|
||||
logic wwrite_q;
|
||||
|
||||
// ---- binary + gray pointers (one extra MSB for full/empty disambiguation) ----
|
||||
logic [AW:0] wbin, wgray, wbin_nxt, wgray_nxt;
|
||||
logic [AW:0] wbin, wgray, wbin_nxt;
|
||||
logic [AW:0] wcommit, wcommit_nxt;
|
||||
logic wfull_nxt; // Ch352 — combinational next-value for the now-REGISTERED wfull
|
||||
logic [AW:0] rbin, rgray, rbin_nxt, rgray_nxt;
|
||||
logic [AW:0] rbin_inc, rgray_inc;
|
||||
(* keep *) logic rempty_if_hold, rempty_if_pop;
|
||||
logic rempty_nxt; // Ch357 — combinational next-value for the now-REGISTERED rempty (read-side twin)
|
||||
logic [WIDTH-1:0] rdata_q;
|
||||
logic [BANK_LO_W-1:0] rdata_lo_q;
|
||||
logic [BANK_HI_W-1:0] rdata_hi_q;
|
||||
// Keep the RAM-facing address distinct from the binary/Gray pointer. The
|
||||
// production request FIFO is one packed 93-bit x 16K macro; splitting it
|
||||
// into explicit width banks wastes M20Ks at each bank boundary. Retain
|
||||
// that efficient packing and ask synthesis to duplicate only this launch
|
||||
// register so no copy drives the complete physical port-B address tree.
|
||||
(* dont_merge, preserve *) logic [AW-1:0] raddr_q /* synthesis maxfan = 64 */;
|
||||
(* dont_merge, preserve *) logic [AW-1:0] raddr_lo_q /* synthesis maxfan = 64 */;
|
||||
(* dont_merge, preserve *) logic [AW-1:0] raddr_hi_q /* synthesis maxfan = 64 */;
|
||||
(* dont_merge, preserve *) logic [HALF_AW-1:0] raddr_lo0_q /* synthesis maxfan = 32 */;
|
||||
(* dont_merge, preserve *) logic [HALF_AW-1:0] raddr_lo1_q /* synthesis maxfan = 32 */;
|
||||
(* dont_merge, preserve *) logic [HALF_AW-1:0] raddr_hi0_q /* synthesis maxfan = 32 */;
|
||||
(* dont_merge, preserve *) logic [HALF_AW-1:0] raddr_hi1_q /* synthesis maxfan = 32 */;
|
||||
logic [BANK_LO_W-1:0] rdata_lo0_q, rdata_lo1_q;
|
||||
logic [BANK_HI_W-1:0] rdata_hi0_q, rdata_hi1_q;
|
||||
logic rbank_addr_q, rbank_data_q;
|
||||
|
||||
// synchronized opposite-domain gray pointers (2-FF)
|
||||
logic [AW:0] rgray_s1, rgray_s2; // read gray -> write domain
|
||||
@@ -48,42 +103,162 @@ module gs_async_fifo #(
|
||||
|
||||
// ---------------- write domain ----------------
|
||||
assign wbin_nxt = wbin + (wr && !wfull);
|
||||
assign wgray_nxt = bin2gray(wbin_nxt);
|
||||
// full: next write gray == read gray with top two bits inverted. Ch352 — wfull is now a REGISTERED flag
|
||||
// (Cummings canonical). The previous `assign wfull = (wgray_nxt == ...)` was combinational, and since
|
||||
// wgray_nxt <- wbin_nxt <- wfull, it formed a wbin_nxt->wgray_nxt->wfull->wbin_nxt COMBINATIONAL LOOP that
|
||||
// Quartus reports and that made Place churn. Registering it breaks the loop with no overflow-behavior change:
|
||||
// wfull still asserts the cycle after the filling write (full is computed from wgray_nxt = the pointer AFTER
|
||||
// the current write), so the (DEPTH+1)th write is still blocked. rempty is intentionally left unchanged.
|
||||
assign wfull_nxt = (wgray_nxt == {~rgray_s2[AW:AW-1], rgray_s2[AW-2:0]});
|
||||
// the current write), so the (DEPTH+1)th write is still blocked. Ch357 — rempty is now the registered read-side twin.
|
||||
assign wfull_nxt = (bin2gray(wbin_nxt) == {~rgray_s2[AW:AW-1], rgray_s2[AW-2:0]});
|
||||
// `wbin` is the allocation pointer (an input handshake reserves an
|
||||
// address). `wcommit` trails it by the one-entry write-port stage and is
|
||||
// the ONLY pointer published to the read domain. Publishing allocation
|
||||
// early is unsafe when rclk is faster than wclk: the reader can otherwise
|
||||
// observe a new pointer before the staged RAM write has occurred.
|
||||
assign wcommit_nxt = wcommit + wwrite_q;
|
||||
always_ff @(posedge wclk or negedge wrst_n) begin
|
||||
if (!wrst_n) begin
|
||||
wbin <= '0; wgray <= '0; wfull <= 1'b0;
|
||||
wbin <= '0; wcommit <= '0; wgray <= '0; wfull <= 1'b0;
|
||||
rgray_s1 <= '0; rgray_s2 <= '0;
|
||||
waddr_q <= '0; wdata_q <= '0; wwrite_q <= 1'b0;
|
||||
end else begin
|
||||
wbin <= wbin_nxt;
|
||||
wgray <= wgray_nxt;
|
||||
wcommit <= wcommit_nxt;
|
||||
wgray <= bin2gray(wcommit_nxt);
|
||||
wfull <= wfull_nxt;
|
||||
rgray_s1 <= rgray; // sync read gray into write domain
|
||||
rgray_s2 <= rgray_s1;
|
||||
waddr_q <= wbin[AW-1:0];
|
||||
wdata_q <= wdata;
|
||||
wwrite_q <= wr && !wfull;
|
||||
end
|
||||
end
|
||||
always_ff @(posedge wclk) if (wr && !wfull) mem[wbin[AW-1:0]] <= wdata;
|
||||
|
||||
// ---------------- read domain ----------------
|
||||
assign rbin_nxt = rbin + (rd && !rempty);
|
||||
assign rgray_nxt = bin2gray(rbin_nxt);
|
||||
// `rd` is an accepted-read handshake by contract: every wrapper gates it
|
||||
// with !rempty. Do not gate it again here. The redundant internal gate
|
||||
// put rempty in front of the AW+1 pointer adder and, for a deep FIFO, also
|
||||
// in front of every RAM read-address bank. That feedback was the complete
|
||||
// Ch405 310 MHz setup-failure family.
|
||||
// Precompute the increment independent of `rd`, then select between the
|
||||
// hold/pop results. Writing this as `rbin + rd` put the registered pop
|
||||
// pulse on the carry input of the complete AW+1 adder and then through
|
||||
// Gray conversion + empty equality at 310 MHz. The explicit two-result
|
||||
// form is behavior-identical but leaves `rd` driving only final muxes.
|
||||
assign rbin_inc = rbin + {{AW{1'b0}}, 1'b1};
|
||||
assign rgray_inc = bin2gray(rbin_inc);
|
||||
assign rbin_nxt = rd ? rbin_inc : rbin;
|
||||
assign rgray_nxt = rd ? rgray_inc : rgray;
|
||||
assign rempty_if_hold = (rgray == wgray_s2);
|
||||
assign rempty_if_pop = (rgray_inc == wgray_s2);
|
||||
assign rempty_nxt = rd ? rempty_if_pop : rempty_if_hold;
|
||||
always_ff @(posedge rclk or negedge rrst_n) begin
|
||||
if (!rrst_n) begin
|
||||
rbin <= '0; rgray <= '0;
|
||||
rbin <= '0; rgray <= '0; rempty <= 1'b1;
|
||||
wgray_s1 <= '0; wgray_s2 <= '0;
|
||||
end else begin
|
||||
rbin <= rbin_nxt;
|
||||
rgray <= rgray_nxt;
|
||||
rbin <= rbin_nxt;
|
||||
rgray <= rgray_nxt;
|
||||
rempty <= rempty_nxt;
|
||||
wgray_s1 <= wgray; // sync write gray into read domain
|
||||
wgray_s2 <= wgray_s1;
|
||||
end
|
||||
end
|
||||
assign rdata = mem[rbin[AW-1:0]];
|
||||
assign rempty = (rgray == wgray_s2);
|
||||
generate
|
||||
if (QUADRANT_READ) begin : g_quadrant_storage
|
||||
// Four physical RAM quadrants: low/high payload width crossed with
|
||||
// lower/upper address half. Writes remain atomic and use the
|
||||
// staged allocation address exactly as the monolithic form does.
|
||||
always_ff @(posedge wclk) begin
|
||||
if (wwrite_q) begin
|
||||
if (waddr_q[AW-1]) begin
|
||||
mem_lo1[waddr_q[HALF_AW-1:0]] <= wdata_q[0 +: BANK_LO_W];
|
||||
mem_hi1[waddr_q[HALF_AW-1:0]] <= wdata_q[BANK_LO_W +: BANK_HI_W];
|
||||
end else begin
|
||||
mem_lo0[waddr_q[HALF_AW-1:0]] <= wdata_q[0 +: BANK_LO_W];
|
||||
mem_hi0[waddr_q[HALF_AW-1:0]] <= wdata_q[BANK_LO_W +: BANK_HI_W];
|
||||
end
|
||||
end
|
||||
end
|
||||
if (REGISTERED_READ) begin : g_registered_read
|
||||
always_ff @(posedge rclk) begin
|
||||
// Separate launch copies are intentional: each feeds only
|
||||
// one depth/width quadrant. rbank_data_q trails the
|
||||
// address-bank selector by the same cycle as the four
|
||||
// synchronous RAM outputs.
|
||||
raddr_lo0_q <= rbin_nxt[HALF_AW-1:0];
|
||||
raddr_lo1_q <= rbin_nxt[HALF_AW-1:0];
|
||||
raddr_hi0_q <= rbin_nxt[HALF_AW-1:0];
|
||||
raddr_hi1_q <= rbin_nxt[HALF_AW-1:0];
|
||||
rbank_addr_q <= rbin_nxt[AW-1];
|
||||
rbank_data_q <= rbank_addr_q;
|
||||
rdata_lo0_q <= mem_lo0[raddr_lo0_q];
|
||||
rdata_lo1_q <= mem_lo1[raddr_lo1_q];
|
||||
rdata_hi0_q <= mem_hi0[raddr_hi0_q];
|
||||
rdata_hi1_q <= mem_hi1[raddr_hi1_q];
|
||||
end
|
||||
assign rdata = rbank_data_q ? {rdata_hi1_q, rdata_lo1_q}
|
||||
: {rdata_hi0_q, rdata_lo0_q};
|
||||
end else begin : g_fwft_read
|
||||
assign rdata = rbin[AW-1]
|
||||
? {mem_hi1[rbin[HALF_AW-1:0]], mem_lo1[rbin[HALF_AW-1:0]]}
|
||||
: {mem_hi0[rbin[HALF_AW-1:0]], mem_lo0[rbin[HALF_AW-1:0]]};
|
||||
end
|
||||
end else if (BANKED_READ) begin : g_banked_storage
|
||||
// Two physical payload banks, written atomically from the same
|
||||
// staged tuple. Each registered read address drives only its own
|
||||
// half of the inferred RAM instead of the entire packed macro.
|
||||
always_ff @(posedge wclk) begin
|
||||
if (wwrite_q) begin
|
||||
mem_lo[waddr_q] <= wdata_q[0 +: BANK_LO_W];
|
||||
mem_hi[waddr_q] <= wdata_q[BANK_LO_W +: BANK_HI_W];
|
||||
end
|
||||
end
|
||||
if (REGISTERED_READ) begin : g_registered_read
|
||||
always_ff @(posedge rclk) begin
|
||||
raddr_lo_q <= rbin_nxt[AW-1:0];
|
||||
raddr_hi_q <= rbin_nxt[AW-1:0];
|
||||
rdata_lo_q <= mem_lo[raddr_lo_q];
|
||||
rdata_hi_q <= mem_hi[raddr_hi_q];
|
||||
end
|
||||
assign rdata = {rdata_hi_q, rdata_lo_q};
|
||||
end else begin : g_fwft_read
|
||||
assign rdata = {mem_hi[rbin[AW-1:0]], mem_lo[rbin[AW-1:0]]};
|
||||
end
|
||||
end else begin : g_monolithic_storage
|
||||
always_ff @(posedge wclk)
|
||||
if (wwrite_q) mem[waddr_q] <= wdata_q;
|
||||
if (REGISTERED_READ) begin : g_registered_read
|
||||
// A synchronous read lets Quartus use the memory output register
|
||||
// instead of timing a deep bank mux directly into request decode.
|
||||
// Read the current head every cycle and qualify rdata only at the
|
||||
// interface. The pointer still advances exclusively on `rd`, so
|
||||
// this does not consume an entry or change the one-cycle accepted-
|
||||
// read latency. Leaving the inferred RAM read enable permanently
|
||||
// active is important for a very wide FIFO: using `rd` as the RAM
|
||||
// enable made one pop register drive every physical data bank
|
||||
// (749 loads in the production request FIFO) at 310 MHz.
|
||||
//
|
||||
// Ch420: the Ch419 fit proved the enable cut and exposed the same
|
||||
// topology on portbaddr: rbin[6] directly drove 713 RAM-address
|
||||
// loads. `raddr_q` tracks the pointer's selected next value, so
|
||||
// before every edge it equals the current head address. The RAM
|
||||
// read therefore returns the same entry on the same edge as the
|
||||
// prior `mem[rbin]` form, including consecutive accepted pops,
|
||||
// while splitting pointer selection from physical RAM addressing.
|
||||
//
|
||||
// raddr_q/rdata_q intentionally have neither enables nor resets.
|
||||
// The FIFO cannot become nonempty until the synchronized write
|
||||
// pointer arrives, giving raddr_q multiple clocks to initialize to
|
||||
// zero after reset. Resetting the wide inferred read structure
|
||||
// previously created its own high-fanout recovery/setup family.
|
||||
always_ff @(posedge rclk) begin
|
||||
raddr_q <= rbin_nxt[AW-1:0];
|
||||
rdata_q <= mem[raddr_q];
|
||||
end
|
||||
assign rdata = rdata_q;
|
||||
end else begin : g_fwft_read
|
||||
assign rdata = mem[rbin[AW-1:0]];
|
||||
end
|
||||
end
|
||||
endgenerate
|
||||
endmodule : gs_async_fifo
|
||||
|
||||
@@ -25,7 +25,17 @@
|
||||
// overflow, done-ish (idle && fifo empty). enable=0 → fully inert.
|
||||
|
||||
module gs_lpddr_axi_master #(
|
||||
parameter int FIFO_DEPTH = 16
|
||||
parameter int FIFO_DEPTH = 16,
|
||||
// Ch353 — pixel width in BYTES: 2 = PSMCT16 (default, byte-identical to Ch318), 4 = PSMCT32.
|
||||
// A 32-byte (256-bit) beat holds 32/PIX_BYTES lanes (16 for PSMCT16, 8 for PSMCT32). The
|
||||
// &ns beat-complete test is unchanged (a full beat is always 32 strobe bits either way).
|
||||
parameter int PIX_BYTES = 2,
|
||||
// Ch357 (Codex) — ELASTIC_BACKPRESSURE: when 1, insert a one-entry elastic (skid) stage between the packer and the
|
||||
// async FIFO and expose real producer backpressure via px_ready (= the stage can accept a beat this cycle). The packer
|
||||
// then advances ONLY on px_emit && px_ready, and the partial/EOF flushes retry until the stage accepts — so a beat is
|
||||
// NEVER dropped even if the FIFO momentarily fills, WITHOUT relying on the "packer never outruns the drain" invariant.
|
||||
// Default 0 = the legacy direct-write packer, byte-identical (px_ready tied 1). Only u_zc_emit|u_c sets this to 1.
|
||||
parameter bit ELASTIC_BACKPRESSURE = 1'b0
|
||||
) (
|
||||
// GS clock domain — flush pixel stream
|
||||
input logic gs_clk,
|
||||
@@ -50,7 +60,15 @@ module gs_lpddr_axi_master #(
|
||||
input logic ctrl_commit,
|
||||
input logic px_emit,
|
||||
input logic [31:0] px_addr, // FB-relative byte address (raster_pixel_fb_addr_q)
|
||||
input logic [15:0] px_pix16,
|
||||
input logic [31:0] px_pix32, // pixel data; PSMCT16 callers drive {16'd0, pix16} (low PIX_BITS used)
|
||||
// Ch357 (Codex) — producer backpressure. ELASTIC_BACKPRESSURE=1: high when the packer can accept px_emit this cycle
|
||||
// (the elastic stage can take a beat); the producer must HOLD px_emit/px_addr/px_pix32 until px_ready. Tied 1 for the
|
||||
// legacy packer (no backpressure), so legacy producers that leave it unconnected are unaffected.
|
||||
output logic px_ready,
|
||||
// Ch353 — end-of-scene partial-beat flush (pulse). Sparse triangle coverage may leave the final
|
||||
// beat incomplete; pulse `flush` after the last px_emit to push the dangling partial beat. The
|
||||
// PSMCT16 tile path always completes beats on tile-row boundaries, so it ties this to 0.
|
||||
input logic flush,
|
||||
|
||||
// f2sdram (LPDDR AXI) clock domain
|
||||
input logic axi_clk,
|
||||
@@ -79,15 +97,29 @@ module gs_lpddr_axi_master #(
|
||||
output logic [31:0] bursts_issued,
|
||||
output logic [31:0] bresp_err_count,
|
||||
output logic [31:0] fifo_overflow_count,
|
||||
output logic idle
|
||||
output logic idle,
|
||||
// Ch353 (Codex) — ORDERED drain ack: high after the EOF marker pops (i.e. after the last data beat's BRESP).
|
||||
// Coherent in the axi_clk domain — the scanout gate uses THIS, not a GS-domain sample of fbw_idle.
|
||||
output logic frame_drained
|
||||
);
|
||||
localparam int PW = 320; // {addr[31:0], data[255:0], strb[31:0]}
|
||||
localparam int PW = 321; // {marker, addr[31:0], data[255:0], strb[31:0]}
|
||||
localparam int MARK = PW-1; // Ch353 (Codex) — ordered EOF marker bit. A flush pushes any partial beat then an
|
||||
// EOF marker; the AXI FSM sets frame_drained when it POPS the marker — which, by the
|
||||
// in-order FIFO + per-beat B handshake, happens only AFTER the last data beat's BRESP.
|
||||
// This is a coherent same-(axi)-domain drain ack for the scanout gate (no GS-domain
|
||||
// sampling of fbw_idle, no pointer-in-flight race).
|
||||
// Ch353 — pixel-width derived params. At PIX_BYTES=2 these reproduce the exact Ch318 PSMCT16 logic.
|
||||
localparam int PIX_BITS = PIX_BYTES * 8; // 16 or 32
|
||||
localparam int LANE_LO = (PIX_BYTES == 4) ? 2 : 1; // low addr bits inside one pixel ($clog2 PIX_BYTES)
|
||||
localparam logic [PIX_BYTES-1:0] STRB1 = '1; // per-pixel byte-strobe mask (2'b11 or 4'b1111)
|
||||
localparam logic [255:0] PIX_MASK = {{(256-PIX_BITS){1'b0}}, {PIX_BITS{1'b1}}};
|
||||
|
||||
// ============================ GS-clock PACKER ============================
|
||||
logic [31:0] cur_addr;
|
||||
logic [255:0] cur_data;
|
||||
logic [31:0] cur_strb;
|
||||
logic has_data;
|
||||
logic flush_pending; // Ch353 — latched end-of-scene flush request (pushes partial then EOF marker)
|
||||
logic fifo_wr;
|
||||
logic [PW-1:0] fifo_wdata;
|
||||
logic fifo_wfull;
|
||||
@@ -112,10 +144,6 @@ module gs_lpddr_axi_master #(
|
||||
end
|
||||
end
|
||||
|
||||
// High for the one cycle the snapshot updates. Admission is blocked then so the FSM never consumes a beat
|
||||
// straddling a config change (old base/arm on the pop cycle, new on the next).
|
||||
wire commit_edge = (commit_sync[2] != commit_sync[1]);
|
||||
|
||||
// arm crosses from axi_clk into gs_clk — 2-FF synchronizer (from the COHERENT latched arm).
|
||||
logic arm_s1, arm_gs;
|
||||
always_ff @(posedge gs_clk or negedge gs_rst_n) begin
|
||||
@@ -123,35 +151,50 @@ module gs_lpddr_axi_master #(
|
||||
else begin arm_s1 <= arm_axi; arm_gs <= arm_s1; end
|
||||
end
|
||||
|
||||
// Ch357 (Codex) — effective FIFO write bus (muxed by the generate below) + one-entry elastic stage (elastic mode only).
|
||||
logic fifo_wr_ram; // drive to u_fifo.wr (already ANDed with !fifo_wfull in both modes)
|
||||
logic [PW-1:0] fifo_wdata_ram; // drive to u_fifo.wdata
|
||||
logic stage_valid; // elastic: a beat waits in the skid stage
|
||||
logic [PW-1:0] stage_data; // elastic: the staged beat
|
||||
wire stage_ready = !stage_valid || !fifo_wfull; // elastic: stage can accept a beat THIS cycle
|
||||
wire stage_drain = stage_valid && !fifo_wfull; // elastic: staged beat enters FIFO THIS cycle
|
||||
|
||||
generate
|
||||
if (!ELASTIC_BACKPRESSURE) begin : g_legacy
|
||||
// ------- LEGACY packer (byte-identical to the pre-Ch357 direct-write path) -------
|
||||
assign px_ready = 1'b1; // no backpressure exposed
|
||||
assign fifo_wr_ram = fifo_wr && !fifo_wfull;
|
||||
assign fifo_wdata_ram = fifo_wdata;
|
||||
always_ff @(posedge gs_clk or negedge gs_rst_n) begin
|
||||
if (!gs_rst_n) begin
|
||||
cur_addr <= '0; cur_data <= '0; cur_strb <= '0; has_data <= 1'b0;
|
||||
fifo_wr <= 1'b0; fifo_wdata <= '0; fifo_overflow_count <= '0;
|
||||
fifo_wr <= 1'b0; fifo_wdata <= '0; fifo_overflow_count <= '0; flush_pending <= 1'b0;
|
||||
end else begin
|
||||
fifo_wr <= 1'b0;
|
||||
if (flush) flush_pending <= 1'b1; // Ch353 — latch the end-of-scene flush request
|
||||
if (enable && arm_gs && px_emit) begin // gate: no accumulation until armed
|
||||
logic [31:0] abeat;
|
||||
logic [3:0] lane; // 0..15 (which 16-bit lane)
|
||||
logic [4:0] lane; // 0..LANES-1 (which PIX_BYTES-wide lane in the 32-byte beat)
|
||||
logic [255:0] nd;
|
||||
logic [31:0] ns;
|
||||
abeat = {px_addr[31:5], 5'd0};
|
||||
lane = px_addr[4:1];
|
||||
lane = px_addr[4:0] >> LANE_LO;
|
||||
if (has_data && (abeat != cur_addr)) begin
|
||||
// line changed before the previous beat filled — flush it, restart
|
||||
fifo_wdata <= {cur_addr, cur_data, cur_strb};
|
||||
// line changed before the previous beat filled — flush it (marker=0), restart
|
||||
fifo_wdata <= {1'b0, cur_addr, cur_data, cur_strb};
|
||||
fifo_wr <= 1'b1;
|
||||
cur_addr <= abeat;
|
||||
cur_data <= (256'(px_pix16) << ({28'd0, lane} * 16));
|
||||
cur_strb <= (32'd3 << ({28'd0, lane} * 2));
|
||||
cur_data <= (256'(px_pix32[PIX_BITS-1:0]) << ({27'd0, lane} * PIX_BITS));
|
||||
cur_strb <= (32'(STRB1) << ({27'd0, lane} * PIX_BYTES));
|
||||
has_data <= 1'b1;
|
||||
end else begin
|
||||
nd = has_data ? cur_data : 256'd0;
|
||||
ns = has_data ? cur_strb : 32'd0;
|
||||
nd[ ({28'd0, lane} * 16) +: 16 ] = px_pix16;
|
||||
ns[ ({28'd0, lane} * 2) +: 2 ] = 2'b11;
|
||||
nd[ ({27'd0, lane} * PIX_BITS) +: PIX_BITS ] = px_pix32[PIX_BITS-1:0];
|
||||
ns[ ({27'd0, lane} * PIX_BYTES) +: PIX_BYTES ] = STRB1;
|
||||
if (&ns) begin
|
||||
// beat complete (all 16 lanes) — flush, beat consumed
|
||||
fifo_wdata <= {abeat, nd, ns};
|
||||
// beat complete (all lanes strobed) — flush (marker=0), beat consumed
|
||||
fifo_wdata <= {1'b0, abeat, nd, ns};
|
||||
fifo_wr <= 1'b1;
|
||||
has_data <= 1'b0;
|
||||
end else begin
|
||||
@@ -161,12 +204,148 @@ module gs_lpddr_axi_master #(
|
||||
has_data <= 1'b1;
|
||||
end
|
||||
end
|
||||
end else if (enable && arm_gs && flush_pending && has_data && !fifo_wr && !fifo_wfull) begin
|
||||
// Ch353 — end-of-scene partial-beat flush: push the dangling beat (marker=0) so no pixels are
|
||||
// stranded. Codex — gate on `!fifo_wr && !fifo_wfull`: a PRIOR registered push may still be entering
|
||||
// the FIFO this cycle (fifo_wfull is REGISTERED, lags a cycle), so wait until no push is in flight
|
||||
// AND not full, then the !wfull check is accurate. Covers a scene ending on a full beat, a partial
|
||||
// right after a line-change, and (since the marker also waits for fifo_wr to clear) partial->marker.
|
||||
// If gated, state is RETAINED and retried — the partial is never dropped.
|
||||
fifo_wdata <= {1'b0, cur_addr, cur_data, cur_strb};
|
||||
fifo_wr <= 1'b1;
|
||||
has_data <= 1'b0;
|
||||
end else if (enable && arm_gs && flush_pending && !has_data && !fifo_wr && !fifo_wfull) begin
|
||||
// Ch353 (Codex) — partial (if any) is pushed; now push the ordered EOF MARKER (payload irrelevant),
|
||||
// same `!fifo_wr && !fifo_wfull` gate. flush_pending retained until accepted. The AXI FSM sets
|
||||
// frame_drained when it pops this, i.e. after the last data beat's BRESP.
|
||||
fifo_wdata <= {1'b1, cur_addr, cur_data, cur_strb};
|
||||
fifo_wr <= 1'b1;
|
||||
flush_pending <= 1'b0;
|
||||
end
|
||||
// overflow witness: a push attempt while the FIFO is full (must stay 0)
|
||||
if (fifo_wr && fifo_wfull)
|
||||
fifo_overflow_count <= fifo_overflow_count + 32'd1;
|
||||
end
|
||||
end
|
||||
end else begin : g_elastic
|
||||
// ------- ELASTIC packer (Codex): full producer backpressure via a one-entry skid stage -------
|
||||
// px_ready: the input stage can accept a pixel THIS cycle. The packer advances (accumulates, produces a
|
||||
// beat, retries a flush) ONLY when stage_ready, so every produced beat lands in the stage the SAME cycle
|
||||
// it is produced -> never dropped. The stage drains into the FIFO whenever !fifo_wfull; simultaneous
|
||||
// drain+refill keeps the stage full with the new beat (the drain writes the OLD stage_data to the FIFO
|
||||
// first, via fifo_wr_ram below).
|
||||
//
|
||||
// Ch358 (Codex) — REGISTERED INPUT STAGE: capture {derived beat address, lane, pixel} into in_* registers
|
||||
// BEFORE the packer. The high-address equality (beat-change) compare and the lane shifts then run off
|
||||
// LOCAL in_* registers instead of gating stage_data[255:0] straight from the producer's cross-module
|
||||
// px_addr register — the Ch358 fit's failing setup cone (col_out_addr -> stage_data, WNS -0.176). +1 cycle
|
||||
// pixel latency; ordering preserved: flush_pending is serviced only when in_valid is EMPTY, so the
|
||||
// partial/EOF marker can never overtake a captured pixel.
|
||||
logic in_valid;
|
||||
logic [31:0] in_beat; // {px_addr[31:5], 5'd0} — derived beat address (low 5 bits constant 0)
|
||||
logic [4:0] in_lane; // px_addr[4:0] >> LANE_LO
|
||||
logic [PIX_BITS-1:0] in_pix;
|
||||
// Ch358 (Codex) — PRE-REGISTERED beat-change decision (the 26.1 fit's residual -0.012 family was
|
||||
// in_beat -> the compare -> stage_data[255:0]): in_same_q = (this pixel's beat == the PREVIOUSLY captured
|
||||
// pixel's beat, last_beat_q), registered at capture. Exact by invariant: the packer reads the compare ONLY
|
||||
// when has_data=1 at consumption, and has_data=1 implies the preceding captured pixel MERGED into cur_addr
|
||||
// (beat-complete and flush both clear has_data and never read it) -> cur_addr == last_beat_q at capture.
|
||||
// A pending flush cannot intervene while in_valid holds a pixel. No latency change.
|
||||
logic in_same_q;
|
||||
logic [31:0] last_beat_q;
|
||||
// Ch367 — map the lane-selected pixel into full beat masks in a distinct registered stage. The beat
|
||||
// accumulator below then sees only registered masks, not in_lane driving cur_data[255:0] directly.
|
||||
logic map_valid, map_same_q;
|
||||
logic [31:0] map_beat;
|
||||
logic [255:0] map_data;
|
||||
logic [255:0] map_mask;
|
||||
logic [31:0] map_strb;
|
||||
wire map_ready = !map_valid || stage_ready;
|
||||
wire in_advance = in_valid && map_ready; // input stage advances into the map stage
|
||||
assign px_ready = !in_valid || map_ready;
|
||||
assign fifo_wr_ram = stage_drain; // drains the CURRENT stage_data (already implies !fifo_wfull)
|
||||
assign fifo_wdata_ram = stage_data;
|
||||
always_ff @(posedge gs_clk or negedge gs_rst_n) begin
|
||||
if (!gs_rst_n) begin
|
||||
cur_addr <= '0; cur_data <= '0; cur_strb <= '0; has_data <= 1'b0;
|
||||
stage_valid <= 1'b0; stage_data <= '0; fifo_overflow_count <= '0; flush_pending <= 1'b0;
|
||||
in_valid <= 1'b0; in_beat <= '0; in_lane <= '0; in_pix <= '0;
|
||||
in_same_q <= 1'b0; last_beat_q <= '0;
|
||||
map_valid <= 1'b0; map_same_q <= 1'b0; map_beat <= '0; map_data <= '0; map_mask <= '0; map_strb <= '0;
|
||||
end else begin
|
||||
logic stage_load; // a beat is being loaded into the stage this cycle
|
||||
stage_load = 1'b0;
|
||||
// (0) INPUT STAGE: consume first; a same-cycle capture below overrides (set wins) — classic
|
||||
// pipeline advance. Capture is gated on px_ready so an occupied, non-advancing stage is never
|
||||
// overwritten (the producer must HOLD px_emit/px_addr/px_pix32 until px_ready, as before).
|
||||
if (in_advance) in_valid <= 1'b0;
|
||||
if (enable && arm_gs && px_emit && px_ready) begin
|
||||
in_valid <= 1'b1;
|
||||
in_beat <= {px_addr[31:5], 5'd0};
|
||||
in_lane <= px_addr[4:0] >> LANE_LO;
|
||||
in_pix <= px_pix32[PIX_BITS-1:0];
|
||||
in_same_q <= ({px_addr[31:5], 5'd0} == last_beat_q); // vs the PREVIOUS captured pixel's beat
|
||||
last_beat_q <= {px_addr[31:5], 5'd0};
|
||||
end
|
||||
// (1) MAP: a drain and refill can coincide; the refill wins and preserves throughput.
|
||||
if (in_advance) begin
|
||||
map_valid <= 1'b1;
|
||||
map_same_q <= in_same_q;
|
||||
map_beat <= in_beat;
|
||||
map_data <= (256'(in_pix) << ({27'd0, in_lane} * PIX_BITS));
|
||||
map_mask <= (PIX_MASK << ({27'd0, in_lane} * PIX_BITS));
|
||||
map_strb <= (32'(STRB1) << ({27'd0, in_lane} * PIX_BYTES));
|
||||
end else if (stage_ready && map_valid) begin
|
||||
map_valid <= 1'b0;
|
||||
end
|
||||
// (2) DRAIN: the staged beat enters the FIFO if there is room (may be re-loaded below same cycle).
|
||||
if (stage_drain) stage_valid <= 1'b0;
|
||||
// (3) latch the flush request UNCONDITIONALLY so it is never lost while backpressured.
|
||||
if (flush) flush_pending <= 1'b1;
|
||||
// (4) PACKER: only when the stage can accept a beat this cycle; consumes the mapped input.
|
||||
if (stage_ready) begin
|
||||
if (map_valid) begin
|
||||
logic [255:0] nd; logic [31:0] ns;
|
||||
if (has_data && !map_same_q) begin
|
||||
stage_data <= {1'b0, cur_addr, cur_data, cur_strb}; stage_valid <= 1'b1; stage_load = 1'b1;
|
||||
cur_addr <= map_beat;
|
||||
cur_data <= map_data;
|
||||
cur_strb <= map_strb;
|
||||
has_data <= 1'b1;
|
||||
end else begin
|
||||
nd = has_data ? cur_data : 256'd0;
|
||||
ns = has_data ? cur_strb : 32'd0;
|
||||
// Preserve the legacy packer's last-writer-wins behavior when two accepted
|
||||
// fragments target one pixel in the same beat. map_data is sparse, so OR
|
||||
// would corrupt a later color whose bit pattern overlaps the earlier one.
|
||||
nd = (nd & ~map_mask) | map_data;
|
||||
ns = ns | map_strb;
|
||||
if (&ns) begin
|
||||
stage_data <= {1'b0, map_beat, nd, ns}; stage_valid <= 1'b1; stage_load = 1'b1;
|
||||
has_data <= 1'b0;
|
||||
end else begin
|
||||
cur_addr <= map_beat; cur_data <= nd; cur_strb <= ns; has_data <= 1'b1;
|
||||
end
|
||||
end
|
||||
end else if (!in_valid && enable && arm_gs && flush_pending && has_data) begin
|
||||
// partial-beat flush — retries here every cycle until stage_ready (guaranteed inside this if)
|
||||
stage_data <= {1'b0, cur_addr, cur_data, cur_strb}; stage_valid <= 1'b1; stage_load = 1'b1;
|
||||
has_data <= 1'b0;
|
||||
end else if (!in_valid && enable && arm_gs && flush_pending && !has_data) begin
|
||||
// ordered EOF marker — retries until accepted (in_valid empty => no pixel can be overtaken)
|
||||
stage_data <= {1'b1, cur_addr, cur_data, cur_strb}; stage_valid <= 1'b1; stage_load = 1'b1;
|
||||
flush_pending <= 1'b0;
|
||||
end
|
||||
end
|
||||
// overflow witness (accepted-write accounting): a stage load while the stage is occupied AND not
|
||||
// draining would DROP the previous beat. Gating on stage_ready makes this impossible; the witness
|
||||
// fires only if that invariant is ever violated. Must stay 0 (asserted by the saturation TB).
|
||||
if (stage_load && stage_valid && !stage_drain)
|
||||
fifo_overflow_count <= fifo_overflow_count + 32'd1;
|
||||
end
|
||||
end
|
||||
end
|
||||
endgenerate
|
||||
|
||||
// ============================ async FIFO (CDC) ============================
|
||||
logic [PW-1:0] fifo_rdata;
|
||||
@@ -183,7 +362,7 @@ module gs_lpddr_axi_master #(
|
||||
end
|
||||
wire fifo_wrst_n = wrst_sync[1];
|
||||
gs_async_fifo #(.WIDTH(PW), .DEPTH(FIFO_DEPTH)) u_fifo (
|
||||
.wclk(gs_clk), .wrst_n(fifo_wrst_n), .wr(fifo_wr && !fifo_wfull), .wdata(fifo_wdata), .wfull(fifo_wfull),
|
||||
.wclk(gs_clk), .wrst_n(fifo_wrst_n), .wr(fifo_wr_ram), .wdata(fifo_wdata_ram), .wfull(fifo_wfull),
|
||||
.rclk(axi_clk), .rrst_n(axi_rst_n), .rd(fifo_rd), .rdata(fifo_rdata), .rempty(fifo_rempty)
|
||||
);
|
||||
|
||||
@@ -209,26 +388,37 @@ module gs_lpddr_axi_master #(
|
||||
assign awvalid = (state == S_AW);
|
||||
assign wvalid = (state == S_W);
|
||||
assign bready = (state == S_B);
|
||||
assign fifo_rd = (state == S_IDLE) && !fifo_rempty && arm_axi && !commit_edge;
|
||||
// A commit and a FIFO admission may coincide safely: S_IDLE latches the full address and canary decision
|
||||
// from the OLD snapshot at that edge, then AW/W/B runs solely from those registered values. The next
|
||||
// admission sees the NEW snapshot. Do not gate this with the raw commit synchronizer: that turns the
|
||||
// control edge into a read-pointer/rempty timing cone inside the async FIFO.
|
||||
assign fifo_rd = (state == S_IDLE) && !fifo_rempty && arm_axi;
|
||||
assign idle = (state == S_IDLE) && fifo_rempty;
|
||||
|
||||
always_ff @(posedge axi_clk or negedge axi_rst_n) begin
|
||||
if (!axi_rst_n) begin
|
||||
state <= S_IDLE; beat_addr <= '0; beat_data <= '0; beat_strb <= '0; awaddr_q <= '0;
|
||||
beats_written <= '0; bursts_issued <= '0; bresp_err_count <= '0;
|
||||
beats_written <= '0; bursts_issued <= '0; bresp_err_count <= '0; frame_drained <= 1'b0;
|
||||
end else begin
|
||||
unique case (state)
|
||||
S_IDLE: if (!fifo_rempty && arm_axi && !commit_edge) begin
|
||||
beat_addr <= fifo_rdata[319:288]; // {addr, data, strb}
|
||||
beat_data <= fifo_rdata[287:32];
|
||||
beat_strb <= fifo_rdata[31:0];
|
||||
awaddr_q <= fb_base_axi + fifo_rdata[319:288]; // latch FULL AW addr from the STABLE base
|
||||
// canary: write ONLY the offset-0 (top-of-frame) 32-byte line;
|
||||
// discard every other beat (fifo_rd still pops it this cycle).
|
||||
if (canary_axi && (fifo_rdata[319:288] != 32'd0))
|
||||
state <= S_IDLE;
|
||||
else
|
||||
state <= S_AW;
|
||||
S_IDLE: if (!fifo_rempty && arm_axi) begin
|
||||
if (fifo_rdata[MARK]) begin
|
||||
// Ch353 — ordered EOF marker popped: every prior data beat's BRESP has completed (in-order
|
||||
// FIFO + per-beat B). Assert the drain ack; consume the marker (fifo_rd pops it, no AXI).
|
||||
frame_drained <= 1'b1;
|
||||
state <= S_IDLE;
|
||||
end else begin
|
||||
frame_drained <= 1'b0; // new frame data in flight — drop the ack
|
||||
beat_addr <= fifo_rdata[319:288]; // {marker, addr, data, strb}
|
||||
beat_data <= fifo_rdata[287:32];
|
||||
beat_strb <= fifo_rdata[31:0];
|
||||
awaddr_q <= fb_base_axi + fifo_rdata[319:288]; // latch FULL AW addr from the STABLE base
|
||||
// canary: write ONLY the offset-0 (top-of-frame) 32-byte line; discard every other beat.
|
||||
if (canary_axi && (fifo_rdata[319:288] != 32'd0))
|
||||
state <= S_IDLE;
|
||||
else
|
||||
state <= S_AW;
|
||||
end
|
||||
end
|
||||
S_AW: if (awready) begin
|
||||
bursts_issued <= bursts_issued + 32'd1;
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
// In-order LPDDR destination-read / generic GS ALPHA_1 blend stage.
|
||||
module gs_lpddr_color_blend (
|
||||
input logic clk, input logic rst_n,
|
||||
input logic in_valid, output logic in_ready,
|
||||
input logic [31:0] in_addr, input logic [31:0] in_color, input logic [16:0] in_alpha,
|
||||
input logic [3:0] in_be,
|
||||
output logic out_valid, input logic out_ready, output logic [31:0] out_addr, output logic [31:0] out_color, output logic idle,
|
||||
output logic [31:0] araddr, output logic [7:0] arlen, output logic [2:0] arsize, output logic [1:0] arburst,
|
||||
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
|
||||
);
|
||||
// The EMIF user clock is ~310 MHz. Keep the destination read, operand
|
||||
// select/subtract, multiply, and add/clamp on separate registered stages;
|
||||
// the original combinational gs_alpha_blend instance put all of them on
|
||||
// the rdata -> out_color path and could not meet a 3.225 ns period.
|
||||
typedef enum logic [2:0] {IDLE, AR, R, SEL, PREP, MUL, MAC, SUM} state_t; state_t state;
|
||||
logic [31:0] src_addr, src_color; logic [16:0] src_alpha; logic [3:0] src_be; logic [2:0] lane;
|
||||
logic [255:0] rdata_q;
|
||||
logic [31:0] dst_q;
|
||||
wire [1:0] a_sel=src_alpha[15:14], b_sel=src_alpha[13:12], c_sel=src_alpha[11:10], d_sel=src_alpha[9:8];
|
||||
wire [31:0] dst_lane=(lane==0)?rdata_q[31:0]:(lane==1)?rdata_q[63:32]:(lane==2)?rdata_q[95:64]:(lane==3)?rdata_q[127:96]:(lane==4)?rdata_q[159:128]:(lane==5)?rdata_q[191:160]:(lane==6)?rdata_q[223:192]:rdata_q[255:224];
|
||||
|
||||
function automatic logic [7:0] color_operand(
|
||||
input logic [7:0] cs, input logic [7:0] cd,
|
||||
input logic [1:0] sel);
|
||||
return (sel == 2'd0) ? cs : (sel == 2'd1) ? cd : 8'd0;
|
||||
endfunction
|
||||
|
||||
function automatic logic [7:0] clamp_sum(
|
||||
input logic signed [17:0] prod, input logic [7:0] op_d);
|
||||
logic signed [18:0] sum;
|
||||
sum = (prod >>> 7) + $signed({11'd0, op_d});
|
||||
if (sum < 19'sd0) return 8'd0;
|
||||
else if (sum > 19'sd255) return 8'd255;
|
||||
else return sum[7:0];
|
||||
endfunction
|
||||
|
||||
// FRAME.FBMSK is compressed to a byte-enable before entering this block.
|
||||
// A disabled byte preserves the destination value after either the GS
|
||||
// ALPHA equation (ABE=1) or the opaque source selection (ABE=0).
|
||||
function automatic logic [31:0] merge_be(
|
||||
input logic [31:0] src, input logic [31:0] dst,
|
||||
input logic [3:0] be);
|
||||
for (int i=0; i<4; i++)
|
||||
merge_be[i*8 +: 8] = be[i] ? src[i*8 +: 8] : dst[i*8 +: 8];
|
||||
endfunction
|
||||
|
||||
wire [7:0] ar_op = color_operand(src_color[7:0], dst_q[7:0], a_sel);
|
||||
wire [7:0] ag_op = color_operand(src_color[15:8], dst_q[15:8], a_sel);
|
||||
wire [7:0] ab_op = color_operand(src_color[23:16], dst_q[23:16], a_sel);
|
||||
wire [7:0] br_op = color_operand(src_color[7:0], dst_q[7:0], b_sel);
|
||||
wire [7:0] bg_op = color_operand(src_color[15:8], dst_q[15:8], b_sel);
|
||||
wire [7:0] bb_op = color_operand(src_color[23:16], dst_q[23:16], b_sel);
|
||||
wire [7:0] dr_op = color_operand(src_color[7:0], dst_q[7:0], d_sel);
|
||||
wire [7:0] dg_op = color_operand(src_color[15:8], dst_q[15:8], d_sel);
|
||||
wire [7:0] db_op = color_operand(src_color[23:16], dst_q[23:16], d_sel);
|
||||
wire [7:0] src_alpha_eff = (src_color[31:24] > 8'h80) ? 8'h80 : src_color[31:24];
|
||||
wire [7:0] coef = (c_sel == 2'd0) ? src_alpha_eff :
|
||||
(c_sel == 2'd1) ? dst_q[31:24] : src_alpha[7:0];
|
||||
|
||||
logic signed [8:0] diff_r_q, diff_g_q, diff_b_q;
|
||||
logic signed [17:0] prod_r_q, prod_g_q, prod_b_q;
|
||||
logic signed [17:0] mcand_r_q, mcand_g_q, mcand_b_q;
|
||||
logic [7:0] multiplier_q;
|
||||
logic [2:0] mac_count_q;
|
||||
logic [7:0] coef_q, d_r_q, d_g_q, d_b_q, alpha_q;
|
||||
assign in_ready=(state==IDLE)&&!out_valid; assign idle=(state==IDLE)&&!out_valid; assign araddr={src_addr[31:5],5'd0};
|
||||
assign arlen=0; assign arsize=3'b101; assign arburst=2'b01; assign arvalid=(state==AR); assign rready=(state==R);
|
||||
always_ff @(posedge clk or negedge rst_n) begin
|
||||
if(!rst_n) begin
|
||||
state<=IDLE;out_valid<=0;out_addr<=0;out_color<=0;
|
||||
src_addr<=0;src_color<=0;src_alpha<=0;src_be<=4'hF;lane<=0;rdata_q<=0;dst_q<=0;
|
||||
diff_r_q<=0;diff_g_q<=0;diff_b_q<=0;
|
||||
prod_r_q<=0;prod_g_q<=0;prod_b_q<=0;
|
||||
mcand_r_q<=0;mcand_g_q<=0;mcand_b_q<=0;
|
||||
multiplier_q<=0;mac_count_q<=0;
|
||||
coef_q<=0;d_r_q<=0;d_g_q<=0;d_b_q<=0;alpha_q<=0;
|
||||
end
|
||||
else begin
|
||||
if(out_valid&&out_ready) out_valid<=0;
|
||||
case(state)
|
||||
IDLE: if(in_valid&&in_ready) begin
|
||||
src_addr<=in_addr;src_color<=in_color;src_alpha<=in_alpha;src_be<=in_be;lane<=in_addr[4:2];
|
||||
// A partial write is a destination RMW even when ABE is off.
|
||||
if(in_alpha[16] || (in_be != 4'hF)) state<=AR;
|
||||
else begin out_valid<=1;out_addr<=in_addr;out_color<=in_color;end
|
||||
end
|
||||
AR: if(arready) state<=R;
|
||||
R: if(rvalid&&rlast) begin
|
||||
rdata_q <= rdata;
|
||||
state<=SEL;
|
||||
end
|
||||
SEL: begin
|
||||
dst_q <= dst_lane;
|
||||
state<=PREP;
|
||||
end
|
||||
PREP: begin
|
||||
diff_r_q <= $signed({1'b0, ar_op}) - $signed({1'b0, br_op});
|
||||
diff_g_q <= $signed({1'b0, ag_op}) - $signed({1'b0, bg_op});
|
||||
diff_b_q <= $signed({1'b0, ab_op}) - $signed({1'b0, bb_op});
|
||||
coef_q <= coef;
|
||||
d_r_q <= dr_op; d_g_q <= dg_op; d_b_q <= db_op;
|
||||
alpha_q <= src_color[31:24];
|
||||
state<=MUL;
|
||||
end
|
||||
MUL: begin
|
||||
// Exact signed-difference * unsigned-coefficient multiply,
|
||||
// implemented as eight short shift/add cycles. Agilex 5's
|
||||
// inferred 18x18 DSP has a 3.751 ns minimum period in this
|
||||
// configuration, slower than the 3.225 ns EMIF user clock.
|
||||
// The emitter is already single-request/in-order, so latency is
|
||||
// harmless and this avoids both the primitive limit and any
|
||||
// timing exception.
|
||||
prod_r_q<=0;prod_g_q<=0;prod_b_q<=0;
|
||||
mcand_r_q<={{9{diff_r_q[8]}},diff_r_q};
|
||||
mcand_g_q<={{9{diff_g_q[8]}},diff_g_q};
|
||||
mcand_b_q<={{9{diff_b_q[8]}},diff_b_q};
|
||||
multiplier_q<=coef_q;mac_count_q<=0;state<=MAC;
|
||||
end
|
||||
MAC: begin
|
||||
if(multiplier_q[0]) begin
|
||||
prod_r_q<=prod_r_q+mcand_r_q;
|
||||
prod_g_q<=prod_g_q+mcand_g_q;
|
||||
prod_b_q<=prod_b_q+mcand_b_q;
|
||||
end
|
||||
mcand_r_q<=mcand_r_q<<<1;
|
||||
mcand_g_q<=mcand_g_q<<<1;
|
||||
mcand_b_q<=mcand_b_q<<<1;
|
||||
multiplier_q<=multiplier_q>>1;
|
||||
if(mac_count_q==3'd7) state<=SUM;
|
||||
else mac_count_q<=mac_count_q+1'b1;
|
||||
end
|
||||
SUM: begin
|
||||
out_valid<=1;out_addr<=src_addr;
|
||||
out_color<=merge_be(
|
||||
src_alpha[16]
|
||||
? {alpha_q,clamp_sum(prod_b_q,d_b_q),clamp_sum(prod_g_q,d_g_q),clamp_sum(prod_r_q,d_r_q)}
|
||||
: src_color,
|
||||
dst_q, src_be);
|
||||
state<=IDLE;
|
||||
end
|
||||
endcase
|
||||
end
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,18 @@
|
||||
// retroDE_ps2 — gs_lpddr_map_pkg (Ch357 — future-compatible private-LPDDR region map)
|
||||
//
|
||||
// Codex-locked LPDDR byte-offset map for the scheduler + persistent-Z path. These are OFFSETS within our PRIVATE reserved
|
||||
// LPDDR region (fb_base is added at the board boundary). The layout fits the current 384x381 rung AND future 640x480 Z
|
||||
// WITHOUT relocation, and keeps color / Z / texture ranges provably DISJOINT:
|
||||
//
|
||||
// color FB : 0x000000 .. (384x381x4 = 0x08EE00 ; 640x480x4 = 0x12C000 -> both < 0x140000)
|
||||
// Z buffer : 0x140000 .. (384x381x2 = 0x047700 -> ..0x187700 ; 640x480x2 = 0x096000 -> ..0x1D6000)
|
||||
// texture : 0x200000 .. (single-region tex cache; 0x1D6000 < 0x200000, so Z never reaches it)
|
||||
//
|
||||
// NOTE (Codex): the Z buffer is INTERNAL — it is never exposed as GS local memory, so it uses a simple LINEAR packed-16b
|
||||
// layout. Only the PSMZ16S VALUE/TEST semantics (clamp16 source, GEQUAL, ZMSK) are authentic; its physical storage swizzle
|
||||
// is deliberately NOT PSMZ16S-swizzled (we own the buffer). See gs_lpddr_z_rmw.sv.
|
||||
package gs_lpddr_map_pkg;
|
||||
localparam logic [31:0] LPDDR_COLOR_BASE = 32'h0000_0000;
|
||||
localparam logic [31:0] LPDDR_Z_BASE = 32'h0014_0000;
|
||||
localparam logic [31:0] LPDDR_TEX_BASE = 32'h0020_0000;
|
||||
endpackage : gs_lpddr_map_pkg
|
||||
@@ -77,6 +77,20 @@ module gs_lpddr_rd_arb (
|
||||
output logic s3_rvalid,
|
||||
input logic s3_rready,
|
||||
|
||||
// ---- Port 4: destination-color read for SH3 alpha ROP ----
|
||||
input logic [29:0] s4_araddr,
|
||||
input logic [1:0] s4_arburst,
|
||||
input logic [6:0] s4_arid,
|
||||
input logic [7:0] s4_arlen,
|
||||
input logic [2:0] s4_arsize,
|
||||
input logic s4_arvalid,
|
||||
output logic s4_arready,
|
||||
output logic [255:0] s4_rdata,
|
||||
output logic [1:0] s4_rresp,
|
||||
output logic s4_rlast,
|
||||
output logic s4_rvalid,
|
||||
input logic s4_rready,
|
||||
|
||||
// ---- Master out: EMIF read channel ----
|
||||
output logic [29:0] m_araddr,
|
||||
output logic [1:0] m_arburst,
|
||||
@@ -91,7 +105,7 @@ module gs_lpddr_rd_arb (
|
||||
input logic m_rvalid,
|
||||
output logic m_rready
|
||||
);
|
||||
// grant: 0=idle, 1=s0 scanout, 2=s1 probe, 3=s2 texfill, 4=s3 tile-reload.
|
||||
// grant: 0=idle, 1=s0 scanout, 2=s1 probe, 3=s2 texfill, 4=s3 reload/Z, 5=s4 alpha destination.
|
||||
// EXPLICIT priority (Ch323, Codex): scanout > tile_reload > probe > texture_fill — i.e.
|
||||
// s0 > s3 > s1 > s2. Render-display (scanout) highest; the render-prep tile reload above
|
||||
// the debug read-probe so a debug read can never starve a render's Z/color reload.
|
||||
@@ -105,26 +119,37 @@ module gs_lpddr_rd_arb (
|
||||
// transaction committed yet — safe to drop); after AR acceptance the grant is held until
|
||||
// m_rvalid && m_rlast && selected_rready, regardless of how long the read takes.
|
||||
reg ar_done; // AR handshake captured for the active grant -> never abort past here
|
||||
// Register the terminal R handshake before releasing the grant. Besides
|
||||
// making the ownership boundary explicit, this removes the selected
|
||||
// requester's rready mux from the encoded grant register's D cone. The
|
||||
// old direct clear path was the secondary 310 MHz setup family after the
|
||||
// wide request-FIFO RAM-enable fanout.
|
||||
reg response_done_q;
|
||||
reg [21:0] watchdog; // pre-AR only (waiting for m_arready); ~6.7 ms @ 310 MHz dead-bus backstop
|
||||
wire wd_expired = watchdog[21];
|
||||
wire sel_rready = (grant==3'd1)?s0_rready:(grant==3'd2)?s1_rready:
|
||||
(grant==3'd3)?s2_rready:(grant==3'd4)?s3_rready:1'b1;
|
||||
(grant==3'd3)?s2_rready:(grant==3'd4)?s3_rready:(grant==3'd5)?s4_rready:1'b1;
|
||||
|
||||
always_ff @(posedge clk or negedge rst_n) begin
|
||||
if (!rst_n) begin
|
||||
grant <= 3'd0; ar_done <= 1'b0; watchdog <= '0;
|
||||
grant <= 3'd0; ar_done <= 1'b0; response_done_q <= 1'b0; watchdog <= '0;
|
||||
end else if (grant == 3'd0) begin
|
||||
ar_done <= 1'b0; watchdog <= '0;
|
||||
ar_done <= 1'b0; response_done_q <= 1'b0; watchdog <= '0;
|
||||
if (s0_arvalid) grant <= 3'd1; // scanout (highest)
|
||||
else if (s3_arvalid) grant <= 3'd4; // tile reload (render-prep)
|
||||
else if (s4_arvalid) grant <= 3'd5; // alpha destination RMW
|
||||
else if (s1_arvalid) grant <= 3'd2; // read probe (debug)
|
||||
else if (s2_arvalid) grant <= 3'd3; // texture fill (lowest)
|
||||
end else if (response_done_q) begin
|
||||
// The selected requester accepted RLAST on the preceding cycle.
|
||||
// Hold ownership through that handshake, then release here.
|
||||
grant <= 3'd0; ar_done <= 1'b0; response_done_q <= 1'b0; watchdog <= '0;
|
||||
end else begin
|
||||
if (m_arvalid && m_arready) ar_done <= 1'b1; // AR accepted -> COMMITTED
|
||||
if (m_rvalid && m_rlast && sel_rready) begin
|
||||
grant <= 3'd0; ar_done <= 1'b0; watchdog <= '0; // response delivered -> release
|
||||
response_done_q <= 1'b1; watchdog <= '0; // response delivered; release next cycle
|
||||
end else if (!ar_done) begin // still waiting for AR (nothing owed)
|
||||
if (wd_expired) begin grant <= 3'd0; ar_done <= 1'b0; watchdog <= '0; end
|
||||
if (wd_expired) begin grant <= 3'd0; ar_done <= 1'b0; response_done_q <= 1'b0; watchdog <= '0; end
|
||||
else watchdog <= watchdog + 22'd1;
|
||||
end
|
||||
// ar_done && response not yet complete: HOLD the grant, never abort.
|
||||
@@ -132,24 +157,30 @@ module gs_lpddr_rd_arb (
|
||||
end
|
||||
|
||||
// AR mux
|
||||
assign m_araddr = (grant==3'd4)?s3_araddr :(grant==3'd3)?s2_araddr :(grant==3'd2)?s1_araddr :s0_araddr;
|
||||
assign m_arburst = (grant==3'd4)?s3_arburst:(grant==3'd3)?s2_arburst:(grant==3'd2)?s1_arburst:s0_arburst;
|
||||
assign m_arid = (grant==3'd4)?s3_arid :(grant==3'd3)?s2_arid :(grant==3'd2)?s1_arid :s0_arid;
|
||||
assign m_arlen = (grant==3'd4)?s3_arlen :(grant==3'd3)?s2_arlen :(grant==3'd2)?s1_arlen :s0_arlen;
|
||||
assign m_arsize = (grant==3'd4)?s3_arsize :(grant==3'd3)?s2_arsize :(grant==3'd2)?s1_arsize :s0_arsize;
|
||||
assign m_arvalid = (grant==3'd1)?s0_arvalid:(grant==3'd2)?s1_arvalid:(grant==3'd3)?s2_arvalid:(grant==3'd4)?s3_arvalid:1'b0;
|
||||
assign s0_arready = (grant==3'd1)?m_arready:1'b0;
|
||||
assign s1_arready = (grant==3'd2)?m_arready:1'b0;
|
||||
assign s2_arready = (grant==3'd3)?m_arready:1'b0;
|
||||
assign s3_arready = (grant==3'd4)?m_arready:1'b0;
|
||||
assign m_araddr = (grant==3'd5)?s4_araddr :(grant==3'd4)?s3_araddr :(grant==3'd3)?s2_araddr :(grant==3'd2)?s1_araddr :s0_araddr;
|
||||
assign m_arburst = (grant==3'd5)?s4_arburst:(grant==3'd4)?s3_arburst:(grant==3'd3)?s2_arburst:(grant==3'd2)?s1_arburst:s0_arburst;
|
||||
assign m_arid = (grant==3'd5)?s4_arid :(grant==3'd4)?s3_arid :(grant==3'd3)?s2_arid :(grant==3'd2)?s1_arid :s0_arid;
|
||||
assign m_arlen = (grant==3'd5)?s4_arlen :(grant==3'd4)?s3_arlen :(grant==3'd3)?s2_arlen :(grant==3'd2)?s1_arlen :s0_arlen;
|
||||
assign m_arsize = (grant==3'd5)?s4_arsize :(grant==3'd4)?s3_arsize :(grant==3'd3)?s2_arsize :(grant==3'd2)?s1_arsize :s0_arsize;
|
||||
// Once an address is accepted, do not expose another address from the
|
||||
// selected requester while its response (or registered release) is active.
|
||||
// This also makes the deliberate one-cycle release bubble AXI-safe.
|
||||
wire ar_open = !ar_done && !response_done_q;
|
||||
assign m_arvalid = ar_open && ((grant==3'd1)?s0_arvalid:(grant==3'd2)?s1_arvalid:(grant==3'd3)?s2_arvalid:(grant==3'd4)?s3_arvalid:(grant==3'd5)?s4_arvalid:1'b0);
|
||||
assign s0_arready = (ar_open && grant==3'd1)?m_arready:1'b0;
|
||||
assign s1_arready = (ar_open && grant==3'd2)?m_arready:1'b0;
|
||||
assign s2_arready = (ar_open && grant==3'd3)?m_arready:1'b0;
|
||||
assign s3_arready = (ar_open && grant==3'd4)?m_arready:1'b0;
|
||||
assign s4_arready = (ar_open && grant==3'd5)?m_arready:1'b0;
|
||||
|
||||
// R demux (idle: rready=1 drains any stale/late response)
|
||||
assign s0_rdata=m_rdata; assign s1_rdata=m_rdata; assign s2_rdata=m_rdata; assign s3_rdata=m_rdata;
|
||||
assign s0_rresp=m_rresp; assign s1_rresp=m_rresp; assign s2_rresp=m_rresp; assign s3_rresp=m_rresp;
|
||||
assign s0_rlast=m_rlast; assign s1_rlast=m_rlast; assign s2_rlast=m_rlast; assign s3_rlast=m_rlast;
|
||||
assign s0_rdata=m_rdata; assign s1_rdata=m_rdata; assign s2_rdata=m_rdata; assign s3_rdata=m_rdata; assign s4_rdata=m_rdata;
|
||||
assign s0_rresp=m_rresp; assign s1_rresp=m_rresp; assign s2_rresp=m_rresp; assign s3_rresp=m_rresp; assign s4_rresp=m_rresp;
|
||||
assign s0_rlast=m_rlast; assign s1_rlast=m_rlast; assign s2_rlast=m_rlast; assign s3_rlast=m_rlast; assign s4_rlast=m_rlast;
|
||||
assign s0_rvalid = (grant==3'd1)?m_rvalid:1'b0;
|
||||
assign s1_rvalid = (grant==3'd2)?m_rvalid:1'b0;
|
||||
assign s2_rvalid = (grant==3'd3)?m_rvalid:1'b0;
|
||||
assign s3_rvalid = (grant==3'd4)?m_rvalid:1'b0;
|
||||
assign m_rready = (grant==3'd1)?s0_rready:(grant==3'd2)?s1_rready:(grant==3'd3)?s2_rready:(grant==3'd4)?s3_rready:1'b1;
|
||||
assign s4_rvalid = (grant==3'd5)?m_rvalid:1'b0;
|
||||
assign m_rready = (grant==3'd1)?s0_rready:(grant==3'd2)?s1_rready:(grant==3'd3)?s2_rready:(grant==3'd4)?s3_rready:(grant==3'd5)?s4_rready:1'b1;
|
||||
endmodule
|
||||
|
||||
@@ -58,7 +58,11 @@ module gs_lpddr_rd_probe #(
|
||||
|
||||
reg [2:0] rd_lane; // which 32-bit lane of the 256-bit beat (addr[4:2])
|
||||
|
||||
typedef enum logic [1:0] { S_IDLE, S_AR, S_R } st_t;
|
||||
// Capture the complete EMIF beat before lane selection. Selecting one of
|
||||
// eight 32-bit lanes directly from the EMIF response into rd_data placed a
|
||||
// two-level mux plus long EMIF routing in one 310 MHz cycle.
|
||||
logic [255:0] rdata_q;
|
||||
typedef enum logic [1:0] { S_IDLE, S_AR, S_R, S_SEL } st_t;
|
||||
st_t st;
|
||||
|
||||
always_ff @(posedge axi_clk) begin
|
||||
@@ -72,6 +76,7 @@ module gs_lpddr_rd_probe #(
|
||||
rd_data <= 32'd0;
|
||||
rd_busy <= 1'b0;
|
||||
rd_lane <= 3'd0;
|
||||
rdata_q <= 256'd0;
|
||||
end else begin
|
||||
pulse_sync <= {pulse_sync[1:0], rd_pulse};
|
||||
|
||||
@@ -95,20 +100,24 @@ module gs_lpddr_rd_probe #(
|
||||
S_R: begin
|
||||
if (rvalid) begin
|
||||
rready <= 1'b0;
|
||||
rdata_q <= rdata;
|
||||
st <= S_SEL;
|
||||
end
|
||||
end
|
||||
S_SEL: begin
|
||||
case (rd_lane)
|
||||
3'd0: rd_data <= rdata[31:0];
|
||||
3'd1: rd_data <= rdata[63:32];
|
||||
3'd2: rd_data <= rdata[95:64];
|
||||
3'd3: rd_data <= rdata[127:96];
|
||||
3'd4: rd_data <= rdata[159:128];
|
||||
3'd5: rd_data <= rdata[191:160];
|
||||
3'd6: rd_data <= rdata[223:192];
|
||||
default: rd_data <= rdata[255:224];
|
||||
3'd0: rd_data <= rdata_q[31:0];
|
||||
3'd1: rd_data <= rdata_q[63:32];
|
||||
3'd2: rd_data <= rdata_q[95:64];
|
||||
3'd3: rd_data <= rdata_q[127:96];
|
||||
3'd4: rd_data <= rdata_q[159:128];
|
||||
3'd5: rd_data <= rdata_q[191:160];
|
||||
3'd6: rd_data <= rdata_q[223:192];
|
||||
default: rd_data <= rdata_q[255:224];
|
||||
endcase
|
||||
rd_busy <= 1'b0;
|
||||
rd_done <= ~rd_done;
|
||||
st <= S_IDLE;
|
||||
end
|
||||
end
|
||||
default: st <= S_IDLE;
|
||||
endcase
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
// 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: it displays row L from one buffer while prefetching row
|
||||
// L+1 into the other. On-chip cost is O(width), not O(width*height).
|
||||
// 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
|
||||
@@ -30,7 +30,39 @@ module gs_lpddr_scanout_lb #(
|
||||
// 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
|
||||
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,
|
||||
@@ -75,9 +107,11 @@ module gs_lpddr_scanout_lb #(
|
||||
// burst (arlen=ROW_BEATS-1) was untested and garbled on hardware.
|
||||
assign arsize = 3'b101; // 32 bytes
|
||||
|
||||
// Two line buffers, ROW_BEATS x 256-bit each (one display row).
|
||||
// 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
|
||||
@@ -85,81 +119,585 @@ module gs_lpddr_scanout_lb #(
|
||||
// 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[2] != fs_sync_v[1]);
|
||||
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.
|
||||
wire disp_buf = pixel_y[0];
|
||||
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 <= '0; fs_sync_v <= 3'd0;
|
||||
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 <= '0;
|
||||
else if (in_window && (pixel_y < N_ROWS)) disp_row_v <= ($clog2(N_ROWS)+1)'(pixel_y);
|
||||
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
|
||||
|
||||
// Registered (sync-read) pixel: pick buffer + beat + within-beat lane from pixel_x.
|
||||
// PSMCT32: 8 px/256-bit beat -> beat = pixel_x>>3, lane = pixel_x[2:0] (32-bit).
|
||||
// PSMCT16: 16 px/beat -> beat = pixel_x>>4, lane = pixel_x[3:0] (16-bit).
|
||||
// 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);
|
||||
wire [RB_BITS-1:0] col_beat = pixel_x[RB_BITS+PXSH-1 -: RB_BITS];
|
||||
wire [3:0] col_lane = PSMCT32 ? {1'b0, pixel_x[2:0]} : pixel_x[3:0];
|
||||
logic [255:0] word_q; logic [3:0] lane_q; logic in_q;
|
||||
always_ff @(posedge video_clk) begin
|
||||
word_q <= disp_buf ? lb1[col_beat] : lb0[col_beat];
|
||||
lane_q <= col_lane;
|
||||
in_q <= in_window && (pixel_x < PX_PER_ROW) && (pixel_y < N_ROWS);
|
||||
end
|
||||
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 = word_q[lane_q[2:0]*32 +: 32]; // 3-bit lane: always in-range (0..224)
|
||||
wire [7:0] r32 = px32[7:0], g32 = px32[15:8], b32 = px32[23:16];
|
||||
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 = word_q[lane_q*16 +: 16];
|
||||
wire [4:0] r5 = px16[4:0], g5 = px16[9:5], b5 = px16[14:10];
|
||||
assign r = !in_q ? 8'd0 : (PSMCT32 ? r32 : {r5, r5[4:2]});
|
||||
assign g = !in_q ? 8'd0 : (PSMCT32 ? g32 : {g5, g5[4:2]});
|
||||
assign b = !in_q ? 8'd0 : (PSMCT32 ? b32 : {b5, b5[4:2]});
|
||||
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[2] != fs_sync_e[1]);
|
||||
logic [$clog2(N_ROWS):0] disp_row_s0, disp_row_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)
|
||||
typedef enum logic [1:0] { L_IDLE, L_AR, L_R } lstate_t;
|
||||
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 cur_buf;
|
||||
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 <= '0; disp_row_e <= '0; next_fetch <= '0;
|
||||
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 <= 1'b0; beat <= '0;
|
||||
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
|
||||
disp_row_e <= disp_row_s0;
|
||||
// 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 <= '0; // restart prefetch sequence from row 0
|
||||
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_e + 1'b1)) begin
|
||||
end else if (enable && (next_fetch < N_ROWS) && (next_fetch <= disp_row_limit_e)) begin
|
||||
cur_row <= next_fetch;
|
||||
cur_buf <= next_fetch[0];
|
||||
cur_buf <= next_fetch_buf;
|
||||
araddr <= FB_BASE + (next_fetch * STRIDE_BYTES);
|
||||
beat <= '0;
|
||||
arvalid <= 1'b1;
|
||||
@@ -175,16 +713,27 @@ module gs_lpddr_scanout_lb #(
|
||||
end
|
||||
L_R: begin
|
||||
if (rvalid) begin
|
||||
if (cur_buf) lb1[beat[RB_BITS-1:0]] <= rdata;
|
||||
else lb0[beat[RB_BITS-1:0]] <= rdata;
|
||||
// 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
|
||||
line_valid <= 1'b1;
|
||||
next_fetch <= next_fetch + 1'b1; // advance prefetch (rows 0..next_fetch-1 loaded)
|
||||
lst <= L_IDLE;
|
||||
// The local register captures this last response
|
||||
// now; L_C flushes it into RAM on the next edge.
|
||||
lst <= L_C;
|
||||
end else begin
|
||||
// next single-beat read of this row (arlen=0 each).
|
||||
// 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;
|
||||
@@ -192,6 +741,18 @@ module gs_lpddr_scanout_lb #(
|
||||
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
|
||||
@@ -206,7 +767,12 @@ module gs_lpddr_scanout_lb #(
|
||||
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 && (pixel_y < N_ROWS) && (($clog2(N_ROWS)+1)'(pixel_y) >= nf_v))
|
||||
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;
|
||||
|
||||
@@ -121,24 +121,28 @@ module gs_lpddr_wr_arb (
|
||||
// so AW sets it first, but tracking either makes this a GENERAL AXI write arbiter that never
|
||||
// abandons a transaction regardless of AW/W ordering (Codex audit note).
|
||||
reg aw_done; // a write beat/addr accepted for the active grant -> never abort past here
|
||||
// Ch368 -- arm EMIF BREADY from the final W handshake, rather than feeding it through the
|
||||
// live grant/client-ready mux. All current writers enter their B-wait state on that same
|
||||
// handshake (or hold BREADY high), so this preserves the AW->W->B contract while cutting
|
||||
// the grant/bready -> EMIF response-FIFO timing cone.
|
||||
reg bready_q;
|
||||
reg [21:0] watchdog; // pre-commit only; ~6.7 ms @ 310 MHz dead-bus backstop
|
||||
wire wd_expired = watchdog[21];
|
||||
wire sel_bready = (grant==3'd1)?s0_bready:(grant==3'd2)?s1_bready:
|
||||
(grant==3'd3)?s2_bready:(grant==3'd4)?s3_bready:1'b1;
|
||||
|
||||
always_ff @(posedge clk or negedge rst_n) begin
|
||||
if (!rst_n) begin
|
||||
grant <= 3'd0; aw_done <= 1'b0; watchdog <= '0;
|
||||
grant <= 3'd0; aw_done <= 1'b0; bready_q <= 1'b0; watchdog <= '0;
|
||||
end else if (grant == 3'd0) begin
|
||||
aw_done <= 1'b0; watchdog <= '0;
|
||||
aw_done <= 1'b0; bready_q <= 1'b0; watchdog <= '0;
|
||||
if (s0_awvalid) grant <= 3'd1; // FB writer (highest)
|
||||
else if (s2_awvalid) grant <= 3'd3; // Z spill (render-flush)
|
||||
else if (s1_awvalid) grant <= 3'd2; // color spill (render-flush)
|
||||
else if (s3_awvalid) grant <= 3'd4; // HPS write-probe (debug, lowest)
|
||||
end else begin
|
||||
if ((m_awvalid && m_awready) || (m_wvalid && m_wready)) aw_done <= 1'b1; // AW or W accepted -> COMMITTED
|
||||
if (m_bvalid && sel_bready) begin
|
||||
grant <= 3'd0; aw_done <= 1'b0; watchdog <= '0; // B delivered -> release
|
||||
if (m_wvalid && m_wready && m_wlast) bready_q <= 1'b1;
|
||||
if (m_bvalid && bready_q) begin
|
||||
grant <= 3'd0; aw_done <= 1'b0; bready_q <= 1'b0; watchdog <= '0; // B delivered -> release
|
||||
end else if (!aw_done) begin // still waiting for AW (nothing owed)
|
||||
if (wd_expired) begin grant <= 3'd0; aw_done <= 1'b0; watchdog <= '0; end
|
||||
else watchdog <= watchdog + 22'd1;
|
||||
@@ -169,11 +173,12 @@ module gs_lpddr_wr_arb (
|
||||
assign s2_wready = (grant==3'd3)?m_wready:1'b0;
|
||||
assign s3_wready = (grant==3'd4)?m_wready:1'b0;
|
||||
|
||||
// B demux (idle: bready=1 drains any stale/late response)
|
||||
// B demux. bready_q arms exactly after the selected final W handshake, one cycle before
|
||||
// the earliest response the existing writers can consume. It is held through B acceptance.
|
||||
assign s0_bresp = m_bresp; assign s1_bresp = m_bresp; assign s2_bresp = m_bresp; assign s3_bresp = m_bresp;
|
||||
assign s0_bvalid = (grant==3'd1)?m_bvalid:1'b0;
|
||||
assign s1_bvalid = (grant==3'd2)?m_bvalid:1'b0;
|
||||
assign s2_bvalid = (grant==3'd3)?m_bvalid:1'b0;
|
||||
assign s3_bvalid = (grant==3'd4)?m_bvalid:1'b0;
|
||||
assign m_bready = (grant==3'd1)?s0_bready:(grant==3'd2)?s1_bready:(grant==3'd3)?s2_bready:(grant==3'd4)?s3_bready:1'b1;
|
||||
assign m_bready = bready_q;
|
||||
endmodule
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
// retroDE_ps2 — gs_lpddr_z_rmw (Ch357 — packed PSMZ16S persistent-Z LPDDR read-modify-write engine, STANDALONE unit)
|
||||
//
|
||||
// Codex Ch357 gate 1: "Build the packed PSMZ16S LPDDR RMW engine as a standalone unit first." This is the depth-test
|
||||
// core for Option B (LPDDR-persistent Z). It owns a PRIVATE, LINEAR, packed-16-bit Z buffer in LPDDR.
|
||||
//
|
||||
// AUTHENTICITY NOTE (Codex): this Z buffer is INTERNAL — it is NEVER exposed as GS local memory. Therefore only the
|
||||
// PSMZ16S VALUE/TEST semantics are authentic (clamp16 source, GEQUAL, ZMSK); its PHYSICAL storage layout is deliberately a
|
||||
// simple linear packing, NOT the PSMZ16S memory swizzle. That is valid precisely because nothing outside reads it as GS VRAM.
|
||||
//
|
||||
// PSMZ16S depth semantics (pinned against PCSX2 SW raster — GSRendererSW.cpp:1439 z_max=0xFFFF, GSDrawScanline…:1129
|
||||
// source clamp, :1157 dest mask + GEQUAL):
|
||||
// src_z = min(frag_z, 0xFFFF) // clamp16 (NOT mask)
|
||||
// dest_z = stored 16-bit Z at the pixel
|
||||
// pass = (src_z >= dest_z) // GEQUAL, larger Z = nearer
|
||||
// on pass && !zmsk: stored := src_z // ZMSK=1 => test still runs, WRITE suppressed only
|
||||
//
|
||||
// Storage: pixel_index = y*FB_PXW + x ; 16 Z per 256-bit (32-byte) beat ; beat = pixel_index>>4, lane = pixel_index[3:0];
|
||||
// byte addr = ZBASE + beat*32. Raster (scanline) order gives strong beat locality.
|
||||
//
|
||||
// Hazards / backpressure (Codex gates): a SINGLE write-back cache line (one 256-bit beat) holds all 16 lanes, so
|
||||
// consecutive fragments to the same beat (incl. the SAME pixel) read the latest pending Z IN-PLACE — same-beat RMW hazards
|
||||
// are forwarded with 1-cycle throughput and no stale read. On a beat MISS the engine flushes the dirty line, reads the new
|
||||
// beat, and BACKPRESSURES the fragment producer (f_ready=0) throughout — a bounded producer, never an unbounded stream
|
||||
// feeding a latent LPDDR read. Color is emitted by the CONSUMER only after p_pass (this unit produces the pass bit).
|
||||
//
|
||||
// Single clock (axi_clk domain). Integration adds the raster(gs_clk)->axi_clk CDC (async-FIFO, like gs_lpddr_axi_master).
|
||||
// AXI4: single-beat INCR (arsize/awsize=5=32B, len=0), full per-byte wstrb, backpressured handshakes.
|
||||
|
||||
module gs_lpddr_z_rmw #(
|
||||
parameter [31:0] ZBASE = 32'h0030_0000, // LPDDR byte base of the private Z buffer (Linux-safe reserved region)
|
||||
parameter int FB_PXW = 256, // framebuffer pixel width (pixel-index stride)
|
||||
parameter int FB_H = 210, // framebuffer height (bounds the preclear loop)
|
||||
parameter [15:0] Z_CLEAR = 16'h0000 // GEQUAL clear value (0 = farthest; any fragment passes first)
|
||||
) (
|
||||
input logic clk,
|
||||
input logic rst_n,
|
||||
input logic enable, // 0 => fully inert (no AXI activity)
|
||||
// ---- preclear (Codex: preclear shared Z once to the GEQUAL clear value) ----
|
||||
input logic clear_start, // pulse: write Z_CLEAR to every beat, then clear_done
|
||||
output logic clear_done,
|
||||
// ---- scene-end flush (Codex: on the ordered end-of-scene marker, flush the dirty Z line + wait its BRESP). The cache
|
||||
// stays VALID and CLEAN afterwards so Z PERSISTS across scheduler epochs (texture rebind must not clear it). ----
|
||||
input logic scene_flush, // hold high until z_drained; flushes the dirty line to LPDDR
|
||||
output logic z_drained, // all Z durable in LPDDR (idle, cache clean, no pending AXI)
|
||||
// ---- fragment input stream (valid/ready) ----
|
||||
input logic f_valid,
|
||||
output logic f_ready,
|
||||
input logic [11:0] f_x,
|
||||
input logic [11:0] f_y,
|
||||
input logic [31:0] f_z, // fragment Z (pre-clamp)
|
||||
input logic f_zmsk, // 1 => suppress Z write (test still occurs)
|
||||
input logic [1:0] f_ztst, // 0 NEVER, 1 ALWAYS, 2 GEQUAL, 3 GREATER
|
||||
// ---- result output stream (in fragment order): p_pass gates the color write downstream ----
|
||||
output logic p_valid,
|
||||
input logic p_ready,
|
||||
output logic p_pass,
|
||||
output logic [11:0] p_x,
|
||||
output logic [11:0] p_y,
|
||||
output logic [15:0] p_zq, // the clamped fragment Z (for the color path / debug)
|
||||
// ---- AXI4 read (Z fetch) ----
|
||||
output logic [31:0] araddr,
|
||||
output logic [7:0] arlen,
|
||||
output logic [2:0] arsize,
|
||||
output logic [1:0] arburst,
|
||||
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,
|
||||
// ---- AXI4 write (Z flush) ----
|
||||
output logic [31:0] awaddr,
|
||||
output logic [7:0] awlen,
|
||||
output logic [2:0] awsize,
|
||||
output logic [1:0] awburst,
|
||||
output logic awvalid,
|
||||
input logic awready,
|
||||
output logic [255:0] wdata,
|
||||
output logic [31:0] wstrb,
|
||||
output logic wlast,
|
||||
output logic wvalid,
|
||||
input logic wready,
|
||||
input logic bvalid,
|
||||
output logic bready,
|
||||
input logic [1:0] bresp,
|
||||
// ---- status ----
|
||||
output logic [31:0] beats_read,
|
||||
output logic [31:0] beats_written,
|
||||
output logic [31:0] bresp_err,
|
||||
output logic idle
|
||||
);
|
||||
localparam int NPX = FB_PXW*FB_H;
|
||||
localparam int NBEATS = (NPX + 15) / 16;
|
||||
localparam int BW = (NBEATS <= 1) ? 1 : $clog2(NBEATS);
|
||||
|
||||
// ---- clamp16 (PSMZ16S source clamp) ----
|
||||
function automatic logic [15:0] clamp16(input logic [31:0] z);
|
||||
clamp16 = (|z[31:16]) ? 16'hFFFF : z[15:0];
|
||||
endfunction
|
||||
|
||||
// ---- single write-back Z-cache line ----
|
||||
logic [255:0] cache_data; // 16 lanes x 16-bit Z
|
||||
// Ch357 (Codex) — register the accepted AXI read data BEFORE updating cache_data, so the EMIF read-FIFO -> cache
|
||||
// 310 MHz path is register->register (the -0.8 ns closure). S_FILL_R captures rdata here; S_FILL_C commits it.
|
||||
logic [255:0] z_rd_q;
|
||||
logic [BW-1:0] cache_beat;
|
||||
logic cache_valid, cache_dirty;
|
||||
|
||||
// ---- pending fragment (latched during a miss) ----
|
||||
logic [11:0] pf_x, pf_y;
|
||||
logic [15:0] pf_zq;
|
||||
logic pf_zmsk;
|
||||
logic [1:0] pf_ztst;
|
||||
logic [BW-1:0] pf_beat;
|
||||
logic [3:0] pf_lane;
|
||||
// Ch357 — DECODE-STAGE pipeline register (Codex: pipeline the 310 MHz u_req->cache path). The incoming fragment's
|
||||
// index/lane/beat + clamp16(z) are computed combinationally and REGISTERED here on accept; the cache read/compare/
|
||||
// write then runs the NEXT cycle off the registered fields. This splits the single-cycle FIFO->index->lane->256-bit
|
||||
// cache-mux->compare->cache-write cone (routed -2.240 ns @ 310 MHz) into two shorter registered stages. Same-beat
|
||||
// forwarding is preserved: fragments are 2 cycles apart in the RMW stage, so an in-place cache write is committed a
|
||||
// cycle before the next same-beat read. Throughput 155 MHz >> the raster fragment rate (no new FIFO pressure).
|
||||
logic d_valid;
|
||||
logic [3:0] d_lane;
|
||||
logic [15:0] d_zq;
|
||||
logic d_zmsk;
|
||||
logic [1:0] d_ztst;
|
||||
logic [11:0] d_x, d_y;
|
||||
logic [BW-1:0] d_beat;
|
||||
// Ch358 (Codex) — registered HIT flag, the companion to d_beat: loaded from hit_c at stage-1 accept so stage 2
|
||||
// branches on ONE registered bit instead of the BW-wide (15-bit at 640x480) d_beat==cache_beat equality that
|
||||
// directly gated the 256-bit wdata<=cache_data dirty-evict load (the Ch358 fit's WNS -0.092 family). Exact by
|
||||
// construction: accept happens only in S_RUN with the decode slot free (f_ready), and cache_beat/cache_valid
|
||||
// change only in S_FILL_C (unreachable while d_* is pending) or at clear_start (precedes all fragments; the
|
||||
// zint gates assert pre_clear_frags==0) -> hit_c cannot go stale between accept and the single stage-2 consume
|
||||
// (a missed fragment is promoted to pf_* and handed to rmw_* by S_FILL_C; it never re-enters stage 2).
|
||||
logic d_hit;
|
||||
// Ch357 (Codex) — RMW-STAGE pipeline register (stage 3). Stage 2 does the barrel READ of the target lane
|
||||
// (cache_data[d_lane] on a hit, z_rd_q[pf_lane] on a fill) into rmw_dz; stage 3 (next cycle) does GEQUAL + the
|
||||
// barrel WRITE via an explicit 16-way case. Splitting read from compare+write removes the cache_data->cache_data
|
||||
// cross-lane feedback mux (the -0.607ns 310MHz path). f_ready already spaces fragments 2 cyc apart, so an in-place
|
||||
// lane write commits one cycle before the next same-beat lane read -> same-beat forwarding preserved, no bypass.
|
||||
logic rmw_valid;
|
||||
logic [3:0] rmw_lane;
|
||||
logic [15:0] rmw_dz; // the read-out dest Z of the target lane (registered)
|
||||
logic [15:0] rmw_zq; // clamped fragment Z
|
||||
logic rmw_zmsk;
|
||||
logic [1:0] rmw_ztst;
|
||||
logic [11:0] rmw_x, rmw_y;
|
||||
// Ch367 — register GEQUAL before it enables a cache-line update. This
|
||||
// removes the compare -> 16-way cache_data write-enable cone at 310 MHz.
|
||||
logic cmp_valid;
|
||||
logic cmp_pass, cmp_write;
|
||||
logic [3:0] cmp_lane;
|
||||
logic [15:0] cmp_zq;
|
||||
logic [11:0] cmp_x, cmp_y;
|
||||
|
||||
// combinational address/lane of the INCOMING fragment
|
||||
logic [31:0] px_index_c;
|
||||
logic [BW-1:0] beat_c;
|
||||
logic [3:0] lane_c;
|
||||
always_comb begin
|
||||
px_index_c = f_y*FB_PXW + f_x;
|
||||
beat_c = px_index_c[4 +: BW];
|
||||
lane_c = px_index_c[3:0];
|
||||
end
|
||||
wire hit_c = cache_valid && (beat_c == cache_beat);
|
||||
|
||||
// ---- result register (1-deep skid; holds until p_ready) ----
|
||||
logic res_full;
|
||||
logic res_pass;
|
||||
logic [11:0] res_x, res_y;
|
||||
logic [15:0] res_zq;
|
||||
assign p_valid = res_full;
|
||||
assign p_pass = res_pass;
|
||||
assign p_x = res_x;
|
||||
assign p_y = res_y;
|
||||
assign p_zq = res_zq;
|
||||
|
||||
// ---- FSM ----
|
||||
typedef enum logic [3:0] { S_RUN, S_FLUSH_AW, S_FLUSH_B, S_FILL_AR, S_FILL_R, S_FILL_C, S_CLR, S_CLR_B,
|
||||
S_SFLUSH_AW, S_SFLUSH_B } st_t;
|
||||
st_t st;
|
||||
logic [BW-1:0] clr_beat;
|
||||
|
||||
// helper: perform the RMW on the cache line for a given lane/zq/zmsk, return pass (comb) and next cache line
|
||||
function automatic logic ztest_pass(input logic [1:0] op, input logic [15:0] s, input logic [15:0] d);
|
||||
case (op)
|
||||
2'd0: ztest_pass = 1'b0;
|
||||
2'd1: ztest_pass = 1'b1;
|
||||
2'd2: ztest_pass = (s >= d);
|
||||
2'd3: ztest_pass = (s > d);
|
||||
endcase
|
||||
endfunction
|
||||
|
||||
// Ch357 (Codex) — EXPLICIT 16-way lane write (constant slices, NOT dynamic cache_data[lane*16+:16]). Each arm writes
|
||||
// one fixed 16-bit slice; the other 15 lanes are unassigned -> HOLD. This is a per-lane write-enable, not a 256-bit
|
||||
// cross-lane feedback mux, which is what kept the compare+write out of the critical cone.
|
||||
task automatic cache_write_lane(input logic [3:0] lane, input logic [15:0] val);
|
||||
case (lane)
|
||||
4'd0: cache_data[ 15: 0] <= val; 4'd1: cache_data[ 31: 16] <= val;
|
||||
4'd2: cache_data[ 47: 32] <= val; 4'd3: cache_data[ 63: 48] <= val;
|
||||
4'd4: cache_data[ 79: 64] <= val; 4'd5: cache_data[ 95: 80] <= val;
|
||||
4'd6: cache_data[111: 96] <= val; 4'd7: cache_data[127:112] <= val;
|
||||
4'd8: cache_data[143:128] <= val; 4'd9: cache_data[159:144] <= val;
|
||||
4'd10: cache_data[175:160] <= val; 4'd11: cache_data[191:176] <= val;
|
||||
4'd12: cache_data[207:192] <= val; 4'd13: cache_data[223:208] <= val;
|
||||
4'd14: cache_data[239:224] <= val; 4'd15: cache_data[255:240] <= val;
|
||||
endcase
|
||||
endtask
|
||||
|
||||
always_ff @(posedge clk or negedge rst_n) begin
|
||||
if (!rst_n) begin
|
||||
st<=S_RUN; cache_valid<=1'b0; cache_dirty<=1'b0; cache_beat<='0; cache_data<='0; z_rd_q<='0;
|
||||
res_full<=1'b0; res_pass<=1'b0; res_x<='0; res_y<='0; res_zq<='0;
|
||||
pf_x<='0; pf_y<='0; pf_zq<='0; pf_zmsk<='0; pf_ztst<=2'd2; pf_beat<='0; pf_lane<='0;
|
||||
d_valid<=1'b0; d_lane<='0; d_zq<='0; d_zmsk<='0; d_ztst<=2'd2; d_x<='0; d_y<='0; d_beat<='0; d_hit<=1'b0;
|
||||
rmw_valid<=1'b0; rmw_lane<='0; rmw_dz<='0; rmw_zq<='0; rmw_zmsk<='0; rmw_ztst<=2'd2; rmw_x<='0; rmw_y<='0;
|
||||
cmp_valid<=1'b0; cmp_pass<=1'b0; cmp_write<=1'b0; cmp_lane<='0; cmp_zq<='0; cmp_x<='0; cmp_y<='0;
|
||||
arvalid<=1'b0; araddr<='0; rready<=1'b0;
|
||||
awvalid<=1'b0; awaddr<='0; wvalid<=1'b0; wdata<='0; wlast<=1'b0; bready<=1'b0;
|
||||
beats_read<='0; beats_written<='0; bresp_err<='0; clr_beat<='0; clear_done<=1'b0;
|
||||
end else begin
|
||||
// constant AXI framing
|
||||
arlen<=8'd0; arsize<=3'd5; arburst<=2'b01;
|
||||
awlen<=8'd0; awsize<=3'd5; awburst<=2'b01; wstrb<=32'hFFFF_FFFF;
|
||||
|
||||
// clear the result reg when the consumer takes it
|
||||
if (res_full && p_ready) res_full<=1'b0;
|
||||
|
||||
if (clear_start && st==S_RUN) begin
|
||||
cache_valid<=1'b0; cache_dirty<=1'b0; clear_done<=1'b0; clr_beat<='0;
|
||||
awaddr<=ZBASE; awvalid<=1'b1; wdata<={16{Z_CLEAR}}; wvalid<=1'b1; wlast<=1'b1; st<=S_CLR;
|
||||
end
|
||||
|
||||
case (st)
|
||||
// ---------------- normal processing ----------------
|
||||
S_RUN: begin
|
||||
// STAGE 4 — write a registered decision and publish its result. This is the only normal
|
||||
// cache_data writer, and its enable is now register-local.
|
||||
if (cmp_valid && (!res_full || p_ready)) begin
|
||||
if (cmp_write) begin cache_write_lane(cmp_lane, cmp_zq); cache_dirty<=1'b1; end
|
||||
res_full<=1'b1; res_pass<=cmp_pass; res_x<=cmp_x; res_y<=cmp_y; res_zq<=cmp_zq;
|
||||
cmp_valid<=1'b0;
|
||||
end
|
||||
|
||||
// STAGE 3 — register GEQUAL + write intent. Stage 2 waits for cmp_valid to clear, so a
|
||||
// same-beat successor never samples cache_data in the cycle its predecessor writes it.
|
||||
if (rmw_valid && !cmp_valid) begin
|
||||
cmp_valid<=1'b1; cmp_pass<=ztest_pass(rmw_ztst, rmw_zq, rmw_dz);
|
||||
cmp_write<=ztest_pass(rmw_ztst, rmw_zq, rmw_dz) && !rmw_zmsk;
|
||||
cmp_lane<=rmw_lane; cmp_zq<=rmw_zq; cmp_x<=rmw_x; cmp_y<=rmw_y;
|
||||
rmw_valid<=1'b0;
|
||||
end
|
||||
|
||||
// scene-end flush (ordered after all fragments): push the dirty line, keep it cached (persist across
|
||||
// epochs). Waits for every pipeline stage to drain so nothing is stranded.
|
||||
if (scene_flush && !d_valid && !rmw_valid && !cmp_valid && cache_valid && cache_dirty) begin
|
||||
awaddr<=ZBASE + (cache_beat<<5); awvalid<=1'b1;
|
||||
wdata<=cache_data; wvalid<=1'b1; wlast<=1'b1; st<=S_SFLUSH_AW;
|
||||
end
|
||||
// STAGE 2 — barrel READ of the target lane into the RMW register (hit), or promote+fill (miss). Fires
|
||||
// only when the RMW slot is empty (!rmw_valid): with f_ready's 2-cyc spacing the stages alternate, so
|
||||
// stage 2 never collides with a same-cycle stage-3 cache write (miss flush reads a settled cache_data).
|
||||
else if (d_valid && !rmw_valid && !cmp_valid) begin
|
||||
d_valid <= 1'b0;
|
||||
if (d_hit) begin // Ch358 — registered at accept (== cache_valid && d_beat==cache_beat there)
|
||||
// HIT: register the read-out dest Z; stage 3 compares + writes next cycle.
|
||||
rmw_valid<=1'b1; rmw_dz<=cache_data[d_lane*16 +: 16];
|
||||
rmw_lane<=d_lane; rmw_zq<=d_zq; rmw_zmsk<=d_zmsk; rmw_ztst<=d_ztst; rmw_x<=d_x; rmw_y<=d_y;
|
||||
end else begin
|
||||
// MISS: promote the decoded fragment to pf_*, backpressure, flush-if-dirty then fill
|
||||
pf_x<=d_x; pf_y<=d_y; pf_zq<=d_zq; pf_zmsk<=d_zmsk; pf_ztst<=d_ztst; pf_beat<=d_beat; pf_lane<=d_lane;
|
||||
if (cache_valid && cache_dirty) begin
|
||||
awaddr<=ZBASE + (cache_beat<<5); awvalid<=1'b1;
|
||||
wdata<=cache_data; wvalid<=1'b1; wlast<=1'b1; st<=S_FLUSH_AW;
|
||||
end else begin
|
||||
araddr<=ZBASE + (d_beat<<5); arvalid<=1'b1; st<=S_FILL_AR;
|
||||
end
|
||||
end
|
||||
end
|
||||
// ACCEPT (stage-1): register the combinational index/lane/beat + clamp16(z). f_ready gates this to
|
||||
// one decoded fragment in flight (d_valid), so accept and stage 2 never collide on d_* (2 cyc/frag).
|
||||
if (enable && f_valid && f_ready) begin
|
||||
d_valid<=1'b1; d_lane<=lane_c; d_zq<=clamp16(f_z); d_zmsk<=f_zmsk; d_ztst<=f_ztst;
|
||||
d_x<=f_x; d_y<=f_y; d_beat<=beat_c; d_hit<=hit_c; // Ch358 — pre-registered hit decision
|
||||
end
|
||||
end
|
||||
// ---------------- flush the dirty line (AW then W then B) ----------------
|
||||
S_FLUSH_AW: begin
|
||||
if (awready) awvalid<=1'b0;
|
||||
if (wready) wvalid <=1'b0;
|
||||
if ((awready||!awvalid) && (wready||!wvalid)) begin bready<=1'b1; st<=S_FLUSH_B; end
|
||||
end
|
||||
S_FLUSH_B: begin
|
||||
if (bvalid) begin
|
||||
bready<=1'b0; beats_written<=beats_written+1;
|
||||
if (bresp!=2'b00) bresp_err<=bresp_err+1;
|
||||
cache_dirty<=1'b0;
|
||||
araddr<=ZBASE + (pf_beat<<5); arvalid<=1'b1; st<=S_FILL_AR; // now fill the wanted beat
|
||||
end
|
||||
end
|
||||
// ---------------- fill the wanted beat (AR then R) ----------------
|
||||
S_FILL_AR: begin
|
||||
if (arready) begin arvalid<=1'b0; rready<=1'b1; st<=S_FILL_R; end
|
||||
end
|
||||
S_FILL_R: begin
|
||||
// Ch357 — ACCEPT: register the read beat (z_rd_q) and take the R handshake; the EMIF read-FIFO ->
|
||||
// z_rd_q path is now register->register. The cache update happens next cycle in S_FILL_C.
|
||||
if (rvalid) begin
|
||||
rready<=1'b0; beats_read<=beats_read+1;
|
||||
if (rresp!=2'b00) bresp_err<=bresp_err+1;
|
||||
z_rd_q<=rdata; st<=S_FILL_C;
|
||||
end
|
||||
end
|
||||
S_FILL_C: begin
|
||||
// Ch357 — COMMIT: fill the cache line from the registered beat, and hand the pending fragment to the
|
||||
// RMW stage (barrel READ of the target lane off the just-read beat). Stage 3 does the GEQUAL + 16-way
|
||||
// lane write next cycle in S_RUN (a guaranteed hit). Same result as the old single-cycle fill, staged.
|
||||
cache_data<=z_rd_q; cache_beat<=pf_beat; cache_valid<=1'b1; cache_dirty<=1'b0;
|
||||
rmw_valid<=1'b1; rmw_dz<=z_rd_q[pf_lane*16 +: 16];
|
||||
rmw_lane<=pf_lane; rmw_zq<=pf_zq; rmw_zmsk<=pf_zmsk; rmw_ztst<=pf_ztst; rmw_x<=pf_x; rmw_y<=pf_y;
|
||||
st<=S_RUN;
|
||||
end
|
||||
// ---------------- preclear: write Z_CLEAR to every beat (issue -> handshake -> B, loop) ----------------
|
||||
S_CLR: begin
|
||||
if (awready) awvalid<=1'b0;
|
||||
if (wready) wvalid <=1'b0;
|
||||
if ((awready||!awvalid) && (wready||!wvalid)) begin bready<=1'b1; st<=S_CLR_B; end
|
||||
end
|
||||
S_CLR_B: begin
|
||||
if (bvalid && bready) begin
|
||||
bready<=1'b0; beats_written<=beats_written+1;
|
||||
if (bresp!=2'b00) bresp_err<=bresp_err+1;
|
||||
if (clr_beat==BW'(NBEATS-1)) begin clear_done<=1'b1; st<=S_RUN; end
|
||||
else begin
|
||||
clr_beat<=clr_beat+1'b1;
|
||||
awaddr<=ZBASE + ((clr_beat+1'b1)<<5); awvalid<=1'b1; wvalid<=1'b1; wlast<=1'b1; st<=S_CLR;
|
||||
end
|
||||
end
|
||||
end
|
||||
// ---------------- scene-end flush: write the dirty line, keep cache VALID+CLEAN (Z persists) ----------------
|
||||
S_SFLUSH_AW: begin
|
||||
if (awready) awvalid<=1'b0;
|
||||
if (wready) wvalid <=1'b0;
|
||||
if ((awready||!awvalid) && (wready||!wvalid)) begin bready<=1'b1; st<=S_SFLUSH_B; end
|
||||
end
|
||||
S_SFLUSH_B: begin
|
||||
if (bvalid) begin
|
||||
bready<=1'b0; beats_written<=beats_written+1;
|
||||
if (bresp!=2'b00) bresp_err<=bresp_err+1;
|
||||
cache_dirty<=1'b0; st<=S_RUN; // cache remains VALID (persist across epochs), now clean/durable
|
||||
end
|
||||
end
|
||||
default: st<=S_RUN;
|
||||
endcase
|
||||
end
|
||||
end
|
||||
|
||||
// f_ready: accept a new fragment in S_RUN (hit => processed this cycle; miss => latched, then flush/fill), whenever the
|
||||
// result slot is free or draining this cycle. On a miss the drain empties res_full and it stays empty until S_FILL_R
|
||||
// re-fills it with the pending fragment's result, so no clobber.
|
||||
// hold off new fragments during a scene flush too (so the marker's flush isn't interleaved with a fresh RMW)
|
||||
// Ch367 pipeline — accept only when every local stage is clear. The extra compare-result stage preserves
|
||||
// same-beat forwarding without a bypass: the prior write commits before the successor can read cache_data.
|
||||
assign f_ready = enable && (st==S_RUN) && !(scene_flush && cache_valid && cache_dirty)
|
||||
&& !d_valid && !rmw_valid && !cmp_valid;
|
||||
// idle / z_drained must see all three stages drained, else a fragment would be lost at a scene boundary.
|
||||
assign idle = (st==S_RUN) && !d_valid && !rmw_valid && !cmp_valid && !res_full && !arvalid && !awvalid && !wvalid && !bready;
|
||||
// z_drained: all Z durable in LPDDR — no local work, no dirty line, no pending AXI, no result stuck.
|
||||
assign z_drained = (st==S_RUN) && !d_valid && !rmw_valid && !cmp_valid && !cache_dirty && !res_full && !arvalid && !awvalid && !wvalid && !bready;
|
||||
|
||||
endmodule : gs_lpddr_z_rmw
|
||||
@@ -0,0 +1,368 @@
|
||||
// retroDE_ps2 — gs_lpddr_zc_emit (Ch357 — Z-then-color emit integration: request FIFO + persistent-Z RMW + color writer)
|
||||
//
|
||||
// Codex Ch357 integration constraints:
|
||||
// (1) Cross gs_clk->axi_clk with ONE async request FIFO carrying the COMPLETE fragment packet {XY, clamped Z, color,
|
||||
// masks, scene marker}. No raw level-bus CDC.
|
||||
// (2) Z decision FIRST; enqueue color ONLY on pass. A rejected fragment produces NO color write.
|
||||
// (3) Ordered end-of-scene marker through the SAME FIFO: on it, flush the dirty Z line, wait Z BRESP + all accepted color
|
||||
// BRESPs, then assert combined frame_drained.
|
||||
// (4) Z cache persists across scheduler epochs (texture rebind must not clear it) — handled by gs_lpddr_z_rmw (scene_flush
|
||||
// keeps the line valid+clean).
|
||||
// (5) Preclear/invalidate Z once at frame start (clear_start).
|
||||
// (6) LPDDR map (gs_lpddr_map_pkg): color 0x000000, Z 0x140000, tex 0x200000 — disjoint.
|
||||
//
|
||||
// Structure: the request async FIFO is the ONLY CDC. Everything downstream runs in axi_clk: the Z RMW (its own Z AXI
|
||||
// read+write master), and the reused gs_lpddr_axi_master COLOR writer (its gs_clk tied to axi_clk, so its internal CDC FIFO
|
||||
// is same-clock). A tiny in-order COLOR-ALIGN FIFO carries {coladdr,color} beside the Z pipeline so the pass decision lands
|
||||
// with the right pixel. Two AXI masters are exposed (z_* and c_*); the system arbitrates them onto the shared LPDDR port.
|
||||
|
||||
module gs_lpddr_zc_emit #(
|
||||
parameter [31:0] COLBASE = 32'h0000_0000,
|
||||
parameter [31:0] ZBASE = 32'h0014_0000,
|
||||
parameter int FB_PXW = 256,
|
||||
parameter int FB_H = 210,
|
||||
parameter [15:0] Z_CLEAR = 16'h0000,
|
||||
parameter int REQ_DEPTH= 32,
|
||||
parameter int COL_DEPTH= 64
|
||||
) (
|
||||
// ---- gs_clk fragment/marker producer ----
|
||||
input logic gs_clk,
|
||||
input logic gs_rst_n,
|
||||
input logic enable,
|
||||
input logic g_valid,
|
||||
output logic g_ready,
|
||||
input logic [11:0] g_x,
|
||||
input logic [11:0] g_y,
|
||||
input logic [15:0] g_zq, // clamped Z (clamp16 done upstream)
|
||||
input logic g_zmsk,
|
||||
input logic g_ztest, // 1 = depth-test this fragment; 0 = always pass (non-Z draw)
|
||||
input logic [1:0] g_ztst, // GS TEST.ZTST: NEVER/ALWAYS/GEQUAL/GREATER
|
||||
input logic [31:0] g_color,
|
||||
input logic [16:0] g_alpha, // {ABE,A,B,C,D,FIX}, snapped at raster emit
|
||||
input logic [3:0] g_be, // FRAME.FBMSK-compressed byte enables
|
||||
input logic g_scene, // 1 = end-of-scene marker (x/y/z/color ignored)
|
||||
|
||||
// ---- axi_clk ----
|
||||
input logic axi_clk,
|
||||
input logic axi_rst_n,
|
||||
input logic clear_start, // preclear Z once at frame start
|
||||
output logic clear_done,
|
||||
output logic frame_drained, // combined: color drained AND Z drained for the scene
|
||||
|
||||
// ---- Z AXI master (read+write) ----
|
||||
output logic [31:0] z_araddr, output logic [7:0] z_arlen, output logic [2:0] z_arsize, output logic [1:0] z_arburst,
|
||||
output logic z_arvalid, input logic z_arready,
|
||||
input logic [255:0] z_rdata, input logic [1:0] z_rresp, input logic z_rlast, input logic z_rvalid, output logic z_rready,
|
||||
output logic [31:0] z_awaddr, output logic [7:0] z_awlen, output logic [2:0] z_awsize, output logic [1:0] z_awburst,
|
||||
output logic z_awvalid, input logic z_awready,
|
||||
output logic [255:0] z_wdata, output logic [31:0] z_wstrb, output logic z_wlast, output logic z_wvalid, input logic z_wready,
|
||||
input logic z_bvalid, output logic z_bready, input logic [1:0] z_bresp,
|
||||
|
||||
// ---- Color AXI master (write only) ----
|
||||
output logic [31:0] c_awaddr, output logic [7:0] c_awlen, output logic [2:0] c_awsize, output logic [1:0] c_awburst,
|
||||
output logic c_awvalid, input logic c_awready,
|
||||
output logic [255:0] c_wdata, output logic [31:0] c_wstrb, output logic c_wlast, output logic c_wvalid, input logic c_wready,
|
||||
input logic c_bvalid, output logic c_bready, input logic [1:0] c_bresp,
|
||||
|
||||
// ---- status ----
|
||||
output logic [31:0] z_beats_read, z_beats_written, c_beats_written, col_ovf, bresp_err,
|
||||
// Destination-color read AXI master. Inert for full-byte opaque writes;
|
||||
// ABE and partial FRAME.FBMSK writes both use it for destination RMW.
|
||||
output logic [31:0] d_araddr, output logic [7:0] d_arlen, output logic [2:0] d_arsize, output logic [1:0] d_arburst,
|
||||
output logic d_arvalid, input logic d_arready,
|
||||
input logic [255:0] d_rdata, input logic [1:0] d_rresp, input logic d_rlast, input logic d_rvalid, output logic d_rready,
|
||||
output logic idle
|
||||
);
|
||||
// ---------------- request async FIFO (the ONLY CDC) ----------------
|
||||
// X/Y have already been clipped to this framebuffer before reaching the
|
||||
// external ROP. Do not spend twelve FIFO bits on each coordinate: the
|
||||
// production 640x480 path needs only 10+9. Besides saving storage, this
|
||||
// keeps the deep request FIFO below the RAM-banking threshold that put a
|
||||
// bank mux on its 310 MHz registered-read path after ZTST was added.
|
||||
localparam int XW = (FB_PXW <= 1) ? 1 : $clog2(FB_PXW);
|
||||
localparam int YW = (FB_H <= 1) ? 1 : $clog2(FB_H);
|
||||
localparam int PW = 74 + XW + YW; // {scene,be,alpha,color,ztest,ztst,zmsk,zq,y[YW],x[XW]}
|
||||
localparam int Y_LSB = XW;
|
||||
localparam int ZQ_LSB = XW + YW;
|
||||
localparam int ZMSK_BIT = ZQ_LSB + 16;
|
||||
localparam int ZTST_LSB = ZMSK_BIT + 1;
|
||||
localparam int ZTEST_BIT = ZTST_LSB + 2;
|
||||
localparam int COLOR_LSB = ZTEST_BIT + 1;
|
||||
localparam int ALPHA_LSB = COLOR_LSB + 32;
|
||||
localparam int BE_LSB = ALPHA_LSB + 17;
|
||||
logic [PW-1:0] req_wdata, req_rdata; logic req_wfull, req_rempty, req_rd;
|
||||
assign req_wdata = {g_scene, g_be, g_alpha, g_color, g_ztest, g_ztst, g_zmsk,
|
||||
g_zq, g_y[YW-1:0], g_x[XW-1:0]};
|
||||
assign g_ready = enable && !req_wfull;
|
||||
// Ch439e — the measured f52 occupancy peak is 6,115, so 8K cannot be cut
|
||||
// without dropping fragments from the intentionally unthrottled producer.
|
||||
// Split that fixed capacity by both depth and width instead: each physical
|
||||
// address copy drives one quarter of the original deep/wide RAM tree.
|
||||
gs_async_fifo #(.WIDTH(PW), .DEPTH(REQ_DEPTH), .REGISTERED_READ(1'b1),
|
||||
.QUADRANT_READ(1'b1)) u_req (
|
||||
.wclk(gs_clk), .wrst_n(gs_rst_n), .wr(g_valid && g_ready), .wdata(req_wdata), .wfull(req_wfull),
|
||||
.rclk(axi_clk), .rrst_n(axi_rst_n), .rd(req_rd), .rdata(req_rdata), .rempty(req_rempty)
|
||||
);
|
||||
// REGISTERED request-HEAD stage. Ch406 makes the deep request FIFO read
|
||||
// synchronous: an accepted req_rd is followed by req_read_pending, which
|
||||
// captures the complete registered RAM output here one cycle later.
|
||||
// Ch421 keeps stage occupancy independent of packet contents: decoding the
|
||||
// RAM's scene bit directly into separate fragment/marker valid registers
|
||||
// was the complete post-Ch420 setup family. A single validity register is
|
||||
// driven only by req_read_pending; scene/type decode happens after the full
|
||||
// packet register boundary.
|
||||
logic req_stage_valid;
|
||||
logic req_read_pending;
|
||||
logic [PW-1:0] req_stage_packet;
|
||||
// Ch423 — packet DATA is intentionally unreset and lives outside the
|
||||
// async-reset control process. Keeping the lone unreset data register in
|
||||
// that process made Quartus fold axi_rst_n (the 4k-fanout EMIF-cal signal)
|
||||
// into its clock enable. Only req_stage_valid makes this payload
|
||||
// observable, so the local registered pending pulse is the complete and
|
||||
// sufficient capture enable.
|
||||
always_ff @(posedge axi_clk)
|
||||
if (req_read_pending) req_stage_packet <= req_rdata;
|
||||
wire [PW-2:0] req_stage_data = req_stage_packet[PW-2:0];
|
||||
wire req_stage_frag_valid = req_stage_valid && !req_stage_packet[PW-1];
|
||||
wire req_stage_marker_valid = req_stage_valid && req_stage_packet[PW-1];
|
||||
wire rq_scene = req_stage_marker_valid;
|
||||
wire [16:0] rq_alpha = req_stage_data[ALPHA_LSB +: 17];
|
||||
wire [3:0] rq_be = req_stage_data[BE_LSB +: 4];
|
||||
wire [31:0] rq_color = req_stage_data[COLOR_LSB +: 32];
|
||||
wire rq_ztest = req_stage_data[ZTEST_BIT];
|
||||
wire [1:0] rq_ztst = req_stage_data[ZTST_LSB +: 2];
|
||||
wire rq_zmsk = req_stage_data[ZMSK_BIT];
|
||||
wire [15:0] rq_zq = req_stage_data[ZQ_LSB +: 16];
|
||||
wire [11:0] rq_y = {{(12-YW){1'b0}}, req_stage_data[Y_LSB +: YW]};
|
||||
wire [11:0] rq_x = {{(12-XW){1'b0}}, req_stage_data[0 +: XW]};
|
||||
|
||||
`ifndef SYNTHESIS
|
||||
initial begin
|
||||
if (XW > 12 || YW > 12)
|
||||
$fatal(1, "gs_lpddr_zc_emit framebuffer dimensions exceed 12-bit fragment coordinates");
|
||||
end
|
||||
`endif
|
||||
|
||||
// ---------------- Z RMW (axi_clk) ----------------
|
||||
logic z_fvalid, z_fready, z_pvalid, z_pready, z_ppass, z_sflush, z_drained;
|
||||
logic [11:0] z_px, z_py; logic [15:0] z_pzq;
|
||||
gs_lpddr_z_rmw #(.ZBASE(ZBASE), .FB_PXW(FB_PXW), .FB_H(FB_H), .Z_CLEAR(Z_CLEAR)) u_z (
|
||||
.clk(axi_clk), .rst_n(axi_rst_n), .enable(enable), .clear_start(clear_start), .clear_done(clear_done),
|
||||
.scene_flush(z_sflush), .z_drained(z_drained),
|
||||
.f_valid(z_fvalid), .f_ready(z_fready), .f_x(rq_x), .f_y(rq_y), .f_z({16'd0, rq_zq}),
|
||||
.f_zmsk(rq_zmsk || !rq_ztest), .f_ztst(rq_ztest ? rq_ztst : 2'd1),
|
||||
.p_valid(z_pvalid), .p_ready(z_pready), .p_pass(z_ppass), .p_x(z_px), .p_y(z_py), .p_zq(z_pzq),
|
||||
.araddr(z_araddr), .arlen(z_arlen), .arsize(z_arsize), .arburst(z_arburst), .arvalid(z_arvalid), .arready(z_arready),
|
||||
.rdata(z_rdata), .rresp(z_rresp), .rlast(z_rlast), .rvalid(z_rvalid), .rready(z_rready),
|
||||
.awaddr(z_awaddr), .awlen(z_awlen), .awsize(z_awsize), .awburst(z_awburst), .awvalid(z_awvalid), .awready(z_awready),
|
||||
.wdata(z_wdata), .wstrb(z_wstrb), .wlast(z_wlast), .wvalid(z_wvalid), .wready(z_wready),
|
||||
.bvalid(z_bvalid), .bready(z_bready), .bresp(z_bresp),
|
||||
.beats_read(z_beats_read), .beats_written(z_beats_written), .bresp_err(bresp_err), .idle(z_idle)
|
||||
);
|
||||
// A fragment carries ztest: when ztest=0 it must ALWAYS pass. Feed the RMW a zmsk so it never writes Z for a
|
||||
// non-Z fragment, and force its zq to max so GEQUAL always passes. (All scheduler draws are ztest=1.)
|
||||
// (Handled at feed below via the always-pass override on the pass decision.)
|
||||
|
||||
// ---------------- color-align FIFO (in-order {coladdr,color,ztest}) ----------------
|
||||
localparam int CAW = 86; // {ztest, be[3:0], alpha[16:0], coladdr[31:0], color[31:0]}
|
||||
localparam int CAPTR_W = $clog2(COL_DEPTH);
|
||||
logic [CAW-1:0] ca_mem [0:COL_DEPTH-1];
|
||||
logic [CAPTR_W:0] ca_head, ca_tail;
|
||||
logic ca_wr_q;
|
||||
logic [CAPTR_W-1:0] ca_waddr_q;
|
||||
logic [CAW-1:0] ca_wdata_q;
|
||||
wire ca_empty = (ca_head==ca_tail);
|
||||
// Power-of-two ring full test: same low address with opposite wrap bit.
|
||||
// This is equivalent to (tail-head)==COL_DEPTH while occupancy is bounded,
|
||||
// but removes the pointer subtract/carry chain from feed_ok -> ca_mem write
|
||||
// enable (the post-scanout fit's final -0.007 ns, two-endpoint family).
|
||||
wire ca_full = (ca_tail[CAPTR_W] != ca_head[CAPTR_W])
|
||||
&& (ca_tail[CAPTR_W-1:0] == ca_head[CAPTR_W-1:0]);
|
||||
wire [31:0] frag_coladdr = COLBASE + (((rq_y*FB_PXW) + rq_x) << 2);
|
||||
// Register the color-align RAM write port. Ch423 captures address/data
|
||||
// every cycle instead of enabling these wide DATA registers with feed_ok.
|
||||
// Their contents are observable only when the separately-reset ca_wr_q is
|
||||
// high, so this removes ca_tail->ca_full->feed_ok from 82 data-register
|
||||
// enables without changing the committed address/data/write-strobe tuple.
|
||||
// Nonblocking ordering makes the RAM consume the prior cycle's captured
|
||||
// tuple exactly when the prior feed_ok raised ca_wr_q.
|
||||
always_ff @(posedge axi_clk) begin
|
||||
ca_waddr_q <= ca_tail[CAPTR_W-1:0];
|
||||
ca_wdata_q <= {rq_ztest, rq_be, rq_alpha, frag_coladdr, rq_color};
|
||||
end
|
||||
always_ff @(posedge axi_clk)
|
||||
if (ca_wr_q) ca_mem[ca_waddr_q] <= ca_wdata_q;
|
||||
|
||||
// ---------------- color writer (reused; gs_clk tied to axi_clk => internal FIFO is same-clock) ----------------
|
||||
logic col_px_emit; logic [31:0] col_px_addr; logic [31:0] col_px_pix32; logic col_flush, col_drained;
|
||||
logic col_px_ready; // Ch357 (Codex) — color-writer producer-backpressure ready (elastic stage can accept)
|
||||
// Ch357 (Codex) — ONE-ENTRY color output register. Registering the payload here breaks the ca_mem->barrel-insert
|
||||
// cone (the -0.98ns 310MHz path) AND holds the emit valid until u_c accepts it, so no pixel is dropped.
|
||||
logic col_out_valid; logic [31:0] col_out_addr; logic [31:0] col_out_color; logic [16:0] col_out_alpha; logic [3:0] col_out_be;
|
||||
logic blend_in_ready, blend_out_valid, blend_idle, col_writer_idle;
|
||||
logic blend_write_pending, blend_flush_issued, blend_drain_seen_low;
|
||||
wire blend_input_gate = !blend_write_pending;
|
||||
logic [31:0] blend_out_addr, blend_out_color;
|
||||
gs_lpddr_color_blend u_blend (
|
||||
.clk(axi_clk), .rst_n(axi_rst_n),
|
||||
.in_valid(col_out_valid && blend_input_gate), .in_ready(blend_in_ready), .in_addr(col_out_addr), .in_color(col_out_color), .in_alpha(col_out_alpha), .in_be(col_out_be),
|
||||
.out_valid(blend_out_valid), .out_ready(col_px_ready), .out_addr(blend_out_addr), .out_color(blend_out_color), .idle(blend_idle),
|
||||
.araddr(d_araddr), .arlen(d_arlen), .arsize(d_arsize), .arburst(d_arburst), .arvalid(d_arvalid), .arready(d_arready),
|
||||
.rdata(d_rdata), .rresp(d_rresp), .rlast(d_rlast), .rvalid(d_rvalid), .rready(d_rready)
|
||||
);
|
||||
logic col_commit; logic [1:0] commit_sr;
|
||||
// one-shot ctrl_commit toggle after reset to latch fb_base=COLBASE
|
||||
always_ff @(posedge axi_clk or negedge axi_rst_n)
|
||||
if (!axi_rst_n) begin commit_sr<=2'b00; col_commit<=1'b0; end
|
||||
else begin commit_sr<={commit_sr[0],1'b1}; if (commit_sr==2'b01) col_commit<=~col_commit; end
|
||||
gs_lpddr_axi_master #(.FIFO_DEPTH(COL_DEPTH), .PIX_BYTES(4), .ELASTIC_BACKPRESSURE(1'b1)) u_c (
|
||||
.gs_clk(axi_clk), .gs_rst_n(axi_rst_n), .enable(enable),
|
||||
.arm(enable), .canary(1'b0), .fb_base(COLBASE), .ctrl_commit(col_commit),
|
||||
.px_emit(col_px_emit), .px_addr(col_px_addr), .px_pix32(col_px_pix32), .px_ready(col_px_ready), .flush(col_flush),
|
||||
.axi_clk(axi_clk), .axi_rst_n(axi_rst_n),
|
||||
.awaddr(c_awaddr), .awlen(c_awlen), .awsize(c_awsize), .awburst(c_awburst), .awvalid(c_awvalid), .awready(c_awready),
|
||||
.wdata(c_wdata), .wstrb(c_wstrb), .wlast(c_wlast), .wvalid(c_wvalid), .wready(c_wready),
|
||||
.bvalid(c_bvalid), .bready(c_bready), .bresp(c_bresp),
|
||||
.beats_written(c_beats_written), .bursts_issued(), .bresp_err_count(),
|
||||
.fifo_overflow_count(col_ovf), .idle(col_writer_idle), .frame_drained(col_drained)
|
||||
);
|
||||
|
||||
// Destination RMW reads must observe the preceding blended/masked write. Merely
|
||||
// waiting for u_c.idle is insufficient: the elastic packer can hold a
|
||||
// partial beat while the AXI-side FIFO still reports empty. Flush an
|
||||
// ordered marker after every blended output and wait for col_drained to go
|
||||
// low then high. That high transition is after the data beat's BRESP, so
|
||||
// the next destination read sees the committed result. This deliberately
|
||||
// serializes the color stream only while an ABE or partial-byte write is
|
||||
// being committed.
|
||||
wire blend_rmw_in_accept = col_out_valid && blend_in_ready && blend_input_gate
|
||||
&& (col_out_alpha[16] || (col_out_be != 4'hF));
|
||||
wire blend_write_accept = blend_out_valid && col_px_ready && blend_write_pending;
|
||||
always_ff @(posedge axi_clk or negedge axi_rst_n) begin
|
||||
if (!axi_rst_n) begin
|
||||
blend_write_pending <= 1'b0;
|
||||
blend_flush_issued <= 1'b0;
|
||||
blend_drain_seen_low <= 1'b0;
|
||||
end else begin
|
||||
if (blend_rmw_in_accept) begin
|
||||
blend_write_pending <= 1'b1;
|
||||
blend_flush_issued <= 1'b0;
|
||||
blend_drain_seen_low <= 1'b0;
|
||||
end else if (blend_write_accept) begin
|
||||
blend_flush_issued <= 1'b1;
|
||||
end else if (blend_write_pending && blend_flush_issued) begin
|
||||
if (!col_drained)
|
||||
blend_drain_seen_low <= 1'b1;
|
||||
else if (blend_drain_seen_low) begin
|
||||
blend_write_pending <= 1'b0;
|
||||
blend_flush_issued <= 1'b0;
|
||||
blend_drain_seen_low <= 1'b0;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
// ---------------- control FSM (axi_clk): feed Z, emit color on pass, drain on scene marker ----------------
|
||||
typedef enum logic [1:0] { C_RUN, C_MARK_WAIT, C_DRAIN } cst_t; cst_t cst;
|
||||
|
||||
// POP only when the head stage and the synchronous-read pipeline are empty;
|
||||
// CONSUME the staged fragment via feed_ok. req_rd remains a registered
|
||||
// pulse, isolated from feed_ok and the color-align control cones.
|
||||
wire req_read_accept = req_rd && !req_rempty;
|
||||
// feed the STAGED fragment into the Z RMW (and push its color) when it is a fragment and both can accept
|
||||
wire feed_ok = (cst==C_RUN) && req_stage_frag_valid && z_fready && !ca_full;
|
||||
// consume the STAGED end-of-scene marker once all fed fragments have drained (see C_MARK_WAIT)
|
||||
wire marker_consume = (cst==C_MARK_WAIT) && req_stage_marker_valid && ca_empty && z_idle && !col_out_valid && blend_idle && !blend_write_pending;
|
||||
assign z_fvalid = feed_ok;
|
||||
// Ch357 (Codex) — PRODUCER BACKPRESSURE via a ONE-ENTRY output register (col_out_*). Consume a Z result only when the
|
||||
// output register can accept the color it would emit: z_pready = !col_out_valid || col_px_ready (empty, OR draining
|
||||
// this cycle -> simultaneous drain/refill). ca_head pops on z_pvalid && z_pready (pass OR reject; a reject consumes its
|
||||
// CA entry WITHOUT loading the output register). col_px_* are driven from the REGISTER (not combinationally from
|
||||
// ca_mem), which breaks the ca_mem->u_c barrel-insert cone (the -0.98ns 310MHz path) AND holds the payload valid until
|
||||
// u_c accepts it (px_ready), so no pixel is dropped. No comb loop: col_px_ready depends only on u_c's registered stage.
|
||||
assign z_pready = !col_out_valid || (blend_in_ready && blend_input_gate);
|
||||
wire [CAW-1:0] ca_headword = ca_mem[ca_head[CAPTR_W-1:0]];
|
||||
wire ca_head_zt = ca_headword[85];
|
||||
wire [3:0] ca_head_be = ca_headword[84:81];
|
||||
wire [16:0] ca_head_alpha= ca_headword[80:64];
|
||||
wire [31:0] ca_head_addr = ca_headword[63:32];
|
||||
wire [31:0] ca_head_col = ca_headword[31:0];
|
||||
wire z_consume = z_pvalid && z_pready; // pop the color-align head this cycle
|
||||
wire z_emit_pass = z_consume && (z_ppass || !ca_head_zt); // load the output register (Z pass or non-Z frag)
|
||||
assign col_px_emit = blend_out_valid;
|
||||
assign col_px_addr = blend_out_addr;
|
||||
assign col_px_pix32 = blend_out_color;
|
||||
always_ff @(posedge axi_clk or negedge axi_rst_n) begin
|
||||
if (!axi_rst_n) begin
|
||||
cst<=C_RUN; ca_head<=0; ca_tail<=0;
|
||||
ca_wr_q<=1'b0;
|
||||
col_flush<=0; z_sflush<=0; frame_drained<=0;
|
||||
col_out_valid<=0; col_out_addr<=0; col_out_color<=0; col_out_alpha<=0; col_out_be<=4'hF;
|
||||
req_stage_valid<=1'b0;
|
||||
req_rd<=1'b0; req_read_pending<=1'b0;
|
||||
end else begin
|
||||
col_flush<=blend_write_accept;
|
||||
ca_wr_q<=1'b0;
|
||||
|
||||
// u_req has a registered read port. Allow one accepted pop, wait
|
||||
// one cycle for its RAM output, then fill the request-head stage.
|
||||
// The pending gate prevents a second pop while that word is in
|
||||
// flight but req_stage_valid has not asserted yet.
|
||||
req_rd <= !req_stage_valid && !req_read_pending && !req_rempty && !req_rd;
|
||||
req_read_pending <= req_read_accept;
|
||||
|
||||
// Capture one cycle after the synchronous FIFO pop. Capture needs
|
||||
// an empty stage; consume needs a full one, so they remain mutually
|
||||
// exclusive.
|
||||
if (req_read_pending) begin
|
||||
req_stage_valid <= 1'b1;
|
||||
end
|
||||
if (feed_ok || marker_consume) req_stage_valid <= 1'b0;
|
||||
|
||||
// push color-align entry as we feed a fragment
|
||||
if (feed_ok) begin
|
||||
ca_wr_q <= 1'b1;
|
||||
ca_tail <= ca_tail + 1'b1;
|
||||
end
|
||||
|
||||
// ONE-ENTRY color output register. DRAIN when u_c accepts (px_ready); may be re-loaded the same cycle (refill
|
||||
// below) for simultaneous drain/refill. A consumed Z result (z_consume = z_pvalid && z_pready) pops the
|
||||
// color-align head (pass OR reject); a PASS additionally loads the register (a reject writes no color).
|
||||
if (col_out_valid && blend_in_ready && blend_input_gate) col_out_valid <= 1'b0;
|
||||
if (z_consume) begin
|
||||
ca_head <= ca_head + 1'b1;
|
||||
if (z_emit_pass) begin
|
||||
col_out_valid <= 1'b1;
|
||||
col_out_addr <= ca_head_addr;
|
||||
col_out_color <= ca_head_col;
|
||||
col_out_alpha <= ca_head_alpha;
|
||||
col_out_be <= ca_head_be;
|
||||
end
|
||||
end
|
||||
|
||||
case (cst)
|
||||
C_RUN: if (req_stage_marker_valid) cst<=C_MARK_WAIT; // staged marker: stop feeding, wait in-flight to drain
|
||||
C_MARK_WAIT: begin
|
||||
// all fed fragments have resulted (CA empty), Z idle, AND the output register drained to u_c
|
||||
if (ca_empty && z_idle && !col_out_valid && blend_idle && !blend_write_pending) begin
|
||||
col_flush<=1'b1; // push color partial beat + EOF marker
|
||||
z_sflush <=1'b1; // flush the dirty Z line (cache persists)
|
||||
// req_rd pops the marker this cycle (see assign)
|
||||
cst<=C_DRAIN;
|
||||
end
|
||||
end
|
||||
C_DRAIN: begin
|
||||
z_sflush<=1'b1; // hold until z_drained
|
||||
if (col_drained && z_drained) begin
|
||||
frame_drained<=1'b1; z_sflush<=1'b0; cst<=C_RUN;
|
||||
end
|
||||
end
|
||||
default: cst<=C_RUN;
|
||||
endcase
|
||||
// frame_drained is a per-scene ack; drop it once new fragments flow again (feed_ok => a fresh fragment consumed)
|
||||
if (feed_ok) frame_drained<=1'b0;
|
||||
end
|
||||
end
|
||||
|
||||
assign idle = (cst==C_RUN) && req_rempty && !req_stage_valid && ca_empty && z_idle && !col_out_valid && blend_idle && !blend_write_pending;
|
||||
endmodule : gs_lpddr_zc_emit
|
||||
@@ -48,7 +48,9 @@ module gs_persp_uv #(
|
||||
input logic [Q_W-1:0] q,
|
||||
output logic out_valid,
|
||||
output logic [TEXEL_W-1:0] u,
|
||||
output logic [TEXEL_W-1:0] v
|
||||
output logic [TEXEL_W-1:0] v,
|
||||
output logic [3:0] u_frac,
|
||||
output logic [3:0] v_frac
|
||||
);
|
||||
localparam int RLAT = 3; // gs_reciprocal_stub latency
|
||||
|
||||
@@ -96,13 +98,23 @@ module gs_persp_uv #(
|
||||
out_valid <= 1'b0;
|
||||
u <= '0;
|
||||
v <= '0;
|
||||
u_frac <= '0;
|
||||
v_frac <= '0;
|
||||
end else begin
|
||||
logic [PROD_W-1:0] u_prod, v_prod;
|
||||
logic [PROD_W-1:0] u_fixed4, v_fixed4;
|
||||
out_valid <= recip_valid;
|
||||
u_prod = uq_pipe[RLAT-1] * w_recip;
|
||||
v_prod = vq_pipe[RLAT-1] * w_recip;
|
||||
u <= clamp_texel(u_prod);
|
||||
v <= clamp_texel(v_prod);
|
||||
// Preserve four fractional texel bits for the palette-bilinear
|
||||
// path. The legacy integer outputs remain the exact SCALE-bit
|
||||
// truncation above; these are simply the next four product bits.
|
||||
u_fixed4 = u_prod >> (SCALE - 4);
|
||||
v_fixed4 = v_prod >> (SCALE - 4);
|
||||
u_frac <= u_fixed4[3:0];
|
||||
v_frac <= v_fixed4[3:0];
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -24,6 +24,12 @@
|
||||
// [7 + 9*i + 3..5] : tri i vtx1 RGBAQ/UV/XYZ2
|
||||
// [7 + 9*i + 6..8] : tri i vtx2 RGBAQ/UV/XYZ2
|
||||
//
|
||||
// Ch402 optional clamp header (word0[34]=1), backward-compatible:
|
||||
// [1..5] : FRAME,ALPHA,TEST,ZBUF,TEX0 as above
|
||||
// [6] : CLAMP_1 data
|
||||
// [7] : PRIM data
|
||||
// [8 + 9*i ...] : triangle records
|
||||
//
|
||||
// One `start` pulse plays the whole list; `done` pulses when it finishes.
|
||||
// Boring on purpose: 2 cycles per emitted register (present addr, then drive).
|
||||
|
||||
@@ -56,10 +62,15 @@ module gs_prim_list_feeder #(
|
||||
localparam logic [7:0] REG_RGBAQ = 8'h01;
|
||||
localparam logic [7:0] REG_ST = 8'h02; // Ch342 — perspective ST (S/T) for FST=0 tris
|
||||
localparam logic [7:0] REG_UV = 8'h03;
|
||||
// GS per-vertex FOG — XYZF2 vertex commit (reg 0x04). When the record's
|
||||
// PRIM has FGE=1, the completing vertex is tagged XYZF2 so gs_stub decodes
|
||||
// the fog byte (F=[63:56]) + 24-bit Z (Z=[55:32]); FGE=0 keeps XYZ2 (0x05).
|
||||
localparam logic [7:0] REG_XYZF2 = 8'h04;
|
||||
localparam logic [7:0] REG_XYZ2 = 8'h05;
|
||||
localparam logic [7:0] REG_TEX0_1 = 8'h06;
|
||||
localparam logic [7:0] REG_ALPHA_1 = 8'h42;
|
||||
localparam logic [7:0] REG_TEST_1 = 8'h47;
|
||||
localparam logic [7:0] REG_CLAMP_1 = 8'h48;
|
||||
localparam logic [7:0] REG_FRAME_1 = 8'h4C;
|
||||
localparam logic [7:0] REG_ZBUF_1 = 8'h4E;
|
||||
|
||||
@@ -70,7 +81,7 @@ module gs_prim_list_feeder #(
|
||||
|
||||
// Header registers (loaded once).
|
||||
logic [15:0] tri_count;
|
||||
logic [63:0] hdr_q [0:5]; // [0]=FRAME [1]=ALPHA [2]=TEST [3]=ZBUF [4]=TEX0 [5]=PRIM
|
||||
logic [63:0] hdr_q [0:6]; // legacy q[5]=PRIM; extended q[5]=CLAMP, q[6]=PRIM
|
||||
// setup-emit index -> GIF reg num (iverilog-12: no unpacked localparam array).
|
||||
function automatic logic [7:0] hdr_reg_num(input logic [2:0] i);
|
||||
unique case (i)
|
||||
@@ -78,15 +89,16 @@ module gs_prim_list_feeder #(
|
||||
3'd1: hdr_reg_num = REG_ALPHA_1;
|
||||
3'd2: hdr_reg_num = REG_TEST_1;
|
||||
3'd3: hdr_reg_num = REG_ZBUF_1;
|
||||
default: hdr_reg_num = REG_TEX0_1;
|
||||
3'd4: hdr_reg_num = REG_TEX0_1;
|
||||
default: hdr_reg_num = REG_CLAMP_1;
|
||||
endcase
|
||||
endfunction
|
||||
|
||||
typedef enum logic [3:0] {
|
||||
S_IDLE,
|
||||
S_HDR_RD, S_HDR_LD, // read words 0..6 into tri_count/rect_count + hdr_q
|
||||
S_SETUP, // emit FRAME/ALPHA/TEST/ZBUF/TEX0 from hdr_q
|
||||
S_PRIM, // emit PRIM (hdr_q[5]) for the current tri
|
||||
S_HDR_RD, S_HDR_LD, // read legacy words 0..6 or extended words 0..7
|
||||
S_SETUP, // emit FRAME/ALPHA/TEST/ZBUF/TEX0[/CLAMP] from hdr_q
|
||||
S_PRIM, // emit the selected legacy/extended PRIM word
|
||||
S_VTX_RD, S_VTX_EMIT, // walk the 9 vertex words of the current tri
|
||||
S_AFTER_TRIS, // Ch334 — tris done; start rects if any, else done
|
||||
S_RECT_RD, S_RECT_LD, // Ch334 — read a rect's 3 words (color, corner0, corner1)
|
||||
@@ -97,8 +109,8 @@ module gs_prim_list_feeder #(
|
||||
localparam int WORDS_PER_RECT = 3; // Ch334 — color + corner0(XYZ2) + corner1(XYZ2)
|
||||
|
||||
state_t state;
|
||||
logic [3:0] hdr_i; // 0..6 header-word read index
|
||||
logic [2:0] setup_i; // 0..4 setup-emit index
|
||||
logic [3:0] hdr_i; // 0..6 legacy or 0..7 extended header-word index
|
||||
logic [2:0] setup_i; // 0..4 legacy or 0..5 extended setup-emit index
|
||||
logic [15:0] tri_idx; // 0..tri_count-1
|
||||
logic [3:0] vtx_word; // 0..8 within a tri
|
||||
|
||||
@@ -121,13 +133,22 @@ module gs_prim_list_feeder #(
|
||||
// it); rects forced off. Same shared-state setup (FRAME/ALPHA/TEST/ZBUF/TEX0/PRIM). Narrow grammar:
|
||||
// PSMCT32 dest+tex, UV affine, ABE source-over, TCC texel alpha — the Ch344-proven subset.
|
||||
logic sprite_mode;
|
||||
logic clamp_header_mode;
|
||||
wire [3:0] header_last = clamp_header_mode ? 4'd7 : 4'd6;
|
||||
wire [STG_ADDR_W-1:0] tris_base = clamp_header_mode ? STG_ADDR_W'(8) : STG_ADDR_W'(OFF_TRIS);
|
||||
wire [63:0] prim_q = clamp_header_mode ? hdr_q[6] : hdr_q[5];
|
||||
wire [4:0] words_per_prim = sprite_mode ? 5'd6 : 5'd9; // staging words per primitive
|
||||
wire [3:0] last_vtx_word = sprite_mode ? 4'd5 : 4'd8; // final XYZ2 of the primitive (the kick)
|
||||
// GS per-vertex FOG — the record's PRIM.FGE (bit 5) selects the vertex
|
||||
// commit register: FGE=1 → XYZF2 (0x04, carries the per-vertex fog byte),
|
||||
// FGE=0 → XYZ2 (0x05) exactly as before. prim_q flows to gs_stub with its
|
||||
// FGE bit intact (S_PRIM emit, unmasked), so gs_stub's ras_fge tracks it.
|
||||
wire prim_fge = prim_q[5];
|
||||
logic [7:0] vtx_reg_num;
|
||||
always_comb unique case (vtx_word % 3)
|
||||
2'd0: vtx_reg_num = REG_RGBAQ;
|
||||
2'd1: vtx_reg_num = perspective_mode ? REG_ST : REG_UV;
|
||||
default: vtx_reg_num = REG_XYZ2;
|
||||
default: vtx_reg_num = prim_fge ? REG_XYZF2 : REG_XYZ2;
|
||||
endcase
|
||||
wire vtx_completing = (vtx_word == last_vtx_word); // final XYZ2 = the FIFO push / kick
|
||||
|
||||
@@ -149,7 +170,7 @@ module gs_prim_list_feeder #(
|
||||
default: rect_reg = REG_XYZ2; // 3,6,9,13,16,19
|
||||
endcase
|
||||
unique case (rect_emit)
|
||||
5'd0, 5'd10: rect_dat = hdr_q[5]; // PRIM
|
||||
5'd0, 5'd10: rect_dat = prim_q; // PRIM
|
||||
5'd1,5'd4,5'd7,5'd11,5'd14,5'd17: rect_dat = rect_color; // RGBAQ
|
||||
5'd2,5'd5,5'd8,5'd12,5'd15,5'd18: rect_dat = 64'd0; // UV (uniform texture)
|
||||
5'd3: rect_dat = mk_xyz2(rx0, ry0, rz); // tri1 v0
|
||||
@@ -166,13 +187,13 @@ module gs_prim_list_feeder #(
|
||||
|
||||
always_ff @(posedge clk or negedge rst_n) begin
|
||||
if (!rst_n) begin
|
||||
state <= S_IDLE; tri_count <= 0; hdr_i <= 0; setup_i <= 0; perspective_mode <= 1'b0; sprite_mode <= 1'b0;
|
||||
state <= S_IDLE; tri_count <= 0; hdr_i <= 0; setup_i <= 0; perspective_mode <= 1'b0; sprite_mode <= 1'b0; clamp_header_mode <= 1'b0;
|
||||
tri_idx <= 0; vtx_word <= 0; stg_rd_addr <= '0;
|
||||
gif_reg_wr_en <= 1'b0; gif_reg_num <= 8'd0; gif_reg_data <= 64'd0; done <= 1'b0;
|
||||
records_emitted <= 16'd0; fifo_wait_cycles <= 32'd0;
|
||||
rect_count <= 0; rect_idx <= 0; rect_word <= 0; rect_emit <= 0;
|
||||
rect_color <= 64'd0; rect_c0 <= 64'd0; rect_c1 <= 64'd0;
|
||||
for (int k=0;k<6;k++) hdr_q[k] <= 64'd0;
|
||||
for (int k=0;k<7;k++) hdr_q[k] <= 64'd0;
|
||||
end else begin
|
||||
gif_reg_wr_en <= 1'b0;
|
||||
done <= 1'b0;
|
||||
@@ -192,11 +213,12 @@ module gs_prim_list_feeder #(
|
||||
// (force rect_count 0 so the rect-expansion path can never run in this format).
|
||||
perspective_mode <= stg_rd_data[32];
|
||||
sprite_mode <= stg_rd_data[33]; // Ch345a
|
||||
clamp_header_mode <= stg_rd_data[34]; // Ch402 optional CLAMP_1 header
|
||||
// rects disallowed with the perspective OR sprite format.
|
||||
rect_count <= (stg_rd_data[32] || stg_rd_data[33]) ? 16'd0 : stg_rd_data[31:16];
|
||||
end
|
||||
else hdr_q[hdr_i-4'd1] <= stg_rd_data;
|
||||
if (hdr_i == 4'd6) begin // all of count + hdr_q[0..5] loaded
|
||||
if (hdr_i == header_last) begin // legacy count+6 words, or extended count+7
|
||||
setup_i <= 3'd0; state <= S_SETUP;
|
||||
end else begin
|
||||
hdr_i <= hdr_i + 4'd1;
|
||||
@@ -210,7 +232,7 @@ module gs_prim_list_feeder #(
|
||||
gif_reg_wr_en <= 1'b1;
|
||||
gif_reg_num <= hdr_reg_num(setup_i);
|
||||
gif_reg_data <= hdr_q[setup_i];
|
||||
if (setup_i == 3'd4) begin
|
||||
if (setup_i == (clamp_header_mode ? 3'd5 : 3'd4)) begin
|
||||
tri_idx <= 16'd0;
|
||||
state <= (tri_count == 16'd0) ? S_AFTER_TRIS : S_PRIM;
|
||||
end else begin
|
||||
@@ -220,9 +242,9 @@ module gs_prim_list_feeder #(
|
||||
|
||||
// ---- per triangle: PRIM, then 9 vertex words ----
|
||||
S_PRIM: begin
|
||||
gif_reg_wr_en <= 1'b1; gif_reg_num <= REG_PRIM; gif_reg_data <= hdr_q[5];
|
||||
gif_reg_wr_en <= 1'b1; gif_reg_num <= REG_PRIM; gif_reg_data <= prim_q;
|
||||
vtx_word <= 4'd0;
|
||||
stg_rd_addr <= STG_ADDR_W'(OFF_TRIS) + STG_ADDR_W'(tri_idx * words_per_prim);
|
||||
stg_rd_addr <= tris_base + STG_ADDR_W'(tri_idx * words_per_prim);
|
||||
state <= S_VTX_RD;
|
||||
end
|
||||
S_VTX_RD: state <= S_VTX_EMIT; // vert-word addr presented; data next cycle
|
||||
@@ -249,7 +271,7 @@ module gs_prim_list_feeder #(
|
||||
S_AFTER_TRIS: begin
|
||||
if (rect_count != 16'd0) begin
|
||||
rect_idx <= 16'd0; rect_word <= 2'd0;
|
||||
stg_rd_addr <= STG_ADDR_W'(OFF_TRIS) + STG_ADDR_W'(tri_count * words_per_prim);
|
||||
stg_rd_addr <= tris_base + STG_ADDR_W'(tri_count * words_per_prim);
|
||||
state <= S_RECT_RD;
|
||||
end else state <= S_DONE;
|
||||
end
|
||||
|
||||
+938
-214
File diff suppressed because it is too large
Load Diff
@@ -88,13 +88,22 @@ module gs_texture_cache #(
|
||||
(* ramstyle = "M20K" *) logic [31:0] tex_mem [0:TEX_WORDS-1];
|
||||
|
||||
// ================= fill side (axi_clk) =================
|
||||
typedef enum logic [2:0] { F_IDLE, F_AR, F_R, F_DRAIN, F_DONE } fstate_t;
|
||||
typedef enum logic [2:0] { F_IDLE, F_AR, F_R, F_DRAIN, F_WRITE, F_DONE } fstate_t;
|
||||
fstate_t fst;
|
||||
logic [$clog2(N_BEATS):0] beat; // 0..N_BEATS
|
||||
logic [255:0] fill_data_q;
|
||||
logic [2:0] fill_lane;
|
||||
logic [WIDX_BITS-1:0] fill_word_base;
|
||||
wire [WIDX_BITS-1:0] fill_word_idx = fill_word_base + WIDX_BITS'(fill_lane);
|
||||
// Ch358 (Codex) — registered RESOLVED drain word + RAM index. The 26.1 STA showed fill_lane launching into
|
||||
// tex_mem's data port: the dynamic 256->32 mux (fill_data_q[fill_lane*32+:32]) fed the RAM write directly.
|
||||
// F_DRAIN now only registers the SELECTED word/index; F_WRITE commits that register to tex_mem next cycle
|
||||
// (2 cycles/lane — harmless one-shot fill time). fill_crc accumulates the COMMITTED word, semantics unchanged.
|
||||
// These payload registers are deliberately unreset. F_WRITE is reachable only after F_DRAIN has loaded both,
|
||||
// so reset values are unobservable; keeping them out of the 4k-fanout EMIF calibration reset removes that reset
|
||||
// from the duplicated RAM-address launch registers at 310 MHz.
|
||||
logic [31:0] drain_word_q;
|
||||
logic [WIDX_BITS-1:0] drain_idx_q;
|
||||
// fill_start is an EDGE/TOGGLE (bridge toggles it on each arm), CDC-synced here so the
|
||||
// cache is RE-FILLABLE: each arm reloads the texture (lets the HPS re-stage a different
|
||||
// texture without a board reset). 3-FF sync + edge-detect, like the read/write probes.
|
||||
@@ -142,8 +151,12 @@ module gs_texture_cache #(
|
||||
end
|
||||
end
|
||||
F_DRAIN: begin
|
||||
tex_mem[fill_word_idx] <= fill_data_q[fill_lane*32 +: 32];
|
||||
fill_crc <= fill_crc + fill_data_q[fill_lane*32 +: 32]; // sum32 over the words written
|
||||
fst <= F_WRITE;
|
||||
end
|
||||
F_WRITE: begin
|
||||
// COMMIT half: registered word -> M20K; CRC over the word actually committed.
|
||||
tex_mem[drain_idx_q] <= drain_word_q;
|
||||
fill_crc <= fill_crc + drain_word_q; // sum32 over the words written
|
||||
if (fill_lane == 3'd7) begin
|
||||
fill_beats <= fill_beats + 32'd1;
|
||||
fill_bytes <= fill_bytes + 32'd32;
|
||||
@@ -158,6 +171,7 @@ module gs_texture_cache #(
|
||||
end
|
||||
end else begin
|
||||
fill_lane <= fill_lane + 3'd1;
|
||||
fst <= F_DRAIN;
|
||||
end
|
||||
end
|
||||
F_DONE: begin
|
||||
@@ -180,6 +194,14 @@ module gs_texture_cache #(
|
||||
end
|
||||
end
|
||||
|
||||
// Control-free payload boundary: observability is controlled by the reset FSM, not by reset on the data itself.
|
||||
always_ff @(posedge axi_clk) begin
|
||||
if (fst == F_DRAIN) begin
|
||||
drain_word_q <= fill_data_q[fill_lane*32 +: 32];
|
||||
drain_idx_q <= fill_word_idx;
|
||||
end
|
||||
end
|
||||
|
||||
// ================= sampler side (sample_clk) =================
|
||||
// 1-cycle REGISTERED read, identical timing to vram_bram_stub.read2:
|
||||
// present (tex_rd_addr) when tex_rd_en, data lands next cycle.
|
||||
|
||||
@@ -502,9 +502,10 @@ module gs_texture_unit #(
|
||||
// beat[k]'s 32-bit ABGR word -> capture into tap[k].
|
||||
// If k<3: k++ and -> ISSUE (next neighbor). If k==3: -> DONE.
|
||||
// DONE : combinationally lerp the 4 captured taps by u_frac/v_frac
|
||||
// per channel; assert out_valid for 1 cycle with tex_color;
|
||||
// drop busy; -> IDLE.
|
||||
// => total ~ 4*(1+L)+1 cycles per filtered sample. Throughput is NOT a
|
||||
// per channel and capture the result in tex_color_hold.
|
||||
// OUT : assert out_valid for 1 cycle with the registered color;
|
||||
// -> IDLE.
|
||||
// => total ~ 4*(1+L)+2 cycles per filtered sample. Throughput is NOT a
|
||||
// goal here (a later texture-cache pass collapses the 4 reads).
|
||||
//
|
||||
// Neighbor table (k -> du,dv): 0->(0,0) 1->(1,0) 2->(0,1) 3->(1,1).
|
||||
@@ -519,12 +520,13 @@ module gs_texture_unit #(
|
||||
// then defensively clamped to 0..255.
|
||||
generate
|
||||
if (BILINEAR_ENABLE) begin : g_bilinear
|
||||
localparam logic [1:0] BS_IDLE = 2'd0;
|
||||
localparam logic [1:0] BS_ISSUE = 2'd1;
|
||||
localparam logic [1:0] BS_WAIT = 2'd2;
|
||||
localparam logic [1:0] BS_DONE = 2'd3;
|
||||
localparam logic [2:0] BS_IDLE = 3'd0;
|
||||
localparam logic [2:0] BS_ISSUE = 3'd1;
|
||||
localparam logic [2:0] BS_WAIT = 3'd2;
|
||||
localparam logic [2:0] BS_DONE = 3'd3;
|
||||
localparam logic [2:0] BS_OUT = 3'd4;
|
||||
|
||||
logic [1:0] state;
|
||||
logic [2:0] state;
|
||||
logic [1:0] beat; // which neighbor 0..3
|
||||
logic [31:0] wait_cnt; // counts RD_LATENCY
|
||||
logic [31:0] tap [0:3]; // captured ABGR per neighbor
|
||||
@@ -641,7 +643,13 @@ module gs_texture_unit #(
|
||||
wait_cnt <= wait_cnt + 32'd1;
|
||||
end
|
||||
end
|
||||
default: begin // BS_DONE
|
||||
BS_DONE: begin
|
||||
// tex_color_hold captures the finished blend on this
|
||||
// edge. Keep a distinct OUT cycle so no consumer can
|
||||
// see the tap->two-lerp combinational cone directly.
|
||||
state <= BS_OUT;
|
||||
end
|
||||
default: begin // BS_OUT
|
||||
state <= BS_IDLE;
|
||||
end
|
||||
endcase
|
||||
@@ -703,18 +711,15 @@ module gs_texture_unit #(
|
||||
cv_a = lerp8(top_a, bot_a, lat_vf);
|
||||
end
|
||||
|
||||
// Ch310 — HOLD register for the filtered color. The combined-renderer
|
||||
// Ch310/Ch422 — HOLD register for the filtered color. The combined-renderer
|
||||
// FSM (gs_stub CB_TWAIT) may latch the result a cycle or two AFTER the
|
||||
// out_valid pulse (it steps at half-rate on z_advance beats), so the
|
||||
// blended ABGR must stay STABLE from out_valid until the next sample.
|
||||
// tex_color is the LIVE combinational blend during DONE (so an
|
||||
// out_valid-keyed caller — tb_gs_texture_bilinear — reads the fresh
|
||||
// value the SAME cycle out_valid pulses, byte-identical to before) and
|
||||
// the LATCHED copy afterward (so a caller that reads one+ cycles later,
|
||||
// like CB_TWAIT→CB_T, still sees it). The register captures the blend
|
||||
// on the clk edge that LEAVES DONE; combining "live during DONE, held
|
||||
// after" gives a value stable from out_valid until the next sample
|
||||
// overwrites it at its DONE.
|
||||
// The register captures the blend on the clk edge that LEAVES DONE.
|
||||
// Ch422 makes OUT a separate following cycle, so tex_color and
|
||||
// out_valid expose only this registered value. That is the timing
|
||||
// boundary between the two-stage bilinear arithmetic and downstream
|
||||
// TEX0 modulation; callers already wait on out_valid or !busy.
|
||||
logic [31:0] tex_color_blend;
|
||||
assign tex_color_blend = {cv_a, cv_b, cv_g, cv_r};
|
||||
logic [31:0] tex_color_hold;
|
||||
@@ -724,9 +729,9 @@ module gs_texture_unit #(
|
||||
else if (state == BS_DONE)
|
||||
tex_color_hold <= tex_color_blend; // capture the just-blended value
|
||||
end
|
||||
// live during the DONE pulse, held (last captured) otherwise
|
||||
// Registered result, held until the next sample completes.
|
||||
logic [31:0] tex_color_lin;
|
||||
assign tex_color_lin = (state == BS_DONE) ? tex_color_blend : tex_color_hold;
|
||||
assign tex_color_lin = tex_color_hold;
|
||||
|
||||
// --- output mux: bilinear FSM owns the outputs for a FILTERED PSMCT32
|
||||
// sample (do_lin). When do_lin=0 — non-PSMCT32 psm OR MMAG=0 NEAREST —
|
||||
@@ -738,7 +743,7 @@ module gs_texture_unit #(
|
||||
// tex_rd_addr is the SAME addr-gen output for both paths (the wrap
|
||||
// selects beat_u/beat_v vs port u/v); the FSM just gates rd_en.
|
||||
assign tex_rd_addr = near_rd_addr;
|
||||
assign out_valid = do_lin ? (state == BS_DONE) : near_out_valid;
|
||||
assign out_valid = do_lin ? (state == BS_OUT) : near_out_valid;
|
||||
assign tex_color = do_lin ? tex_color_lin : near_color;
|
||||
assign busy = do_lin && (state != BS_IDLE);
|
||||
end else begin : g_nearest
|
||||
|
||||
Reference in New Issue
Block a user