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:
2026-07-20 19:56:46 -04:00
parent ec82764bef
commit ba74bbd5aa
476 changed files with 696247 additions and 130119 deletions
+191 -16
View File
@@ -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
+223 -33
View File
@@ -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;
+145
View File
@@ -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
+18
View File
@@ -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
+51 -20
View File
@@ -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
+19 -10
View File
@@ -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
+610 -44
View File
@@ -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;
+13 -8
View File
@@ -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
+373
View File
@@ -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
+368
View File
@@ -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
+13 -1
View File
@@ -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
+38 -16
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+25 -3
View File
@@ -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.
+26 -21
View File
@@ -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
+119
View File
@@ -0,0 +1,119 @@
// Runtime palette staging: one CLOCK2_50 write port and one design-clock
// copy port. The host fills all 256 entries, then flips commit once. The
// design domain copies the stable bank into clut_stub and returns a sum32.
`timescale 1ns/1ps
module clut_stage_cdc (
input logic bclk,
input logic breset_n,
input logic b_we,
input logic [7:0] b_waddr,
input logic [31:0] b_wdata,
input logic b_commit_tgl,
input logic [31:0] b_expected_crc,
input logic dclk,
input logic dreset_n,
output logic clut_wr_en,
output logic [7:0] clut_wr_idx,
output logic [31:0] clut_wr_data,
output logic busy,
output logic done_tgl,
output logic [31:0] crc
);
// 256 x 32 fits in one M20K and remains a true dual-clock staging bank
// on the board instead of becoming a wide register array.
(* ramstyle = "M20K" *) logic [31:0] stage_mem [0:255];
logic [7:0] rd_addr;
logic [31:0] rd_data_q;
always_ff @(posedge bclk) begin
if (b_we)
stage_mem[b_waddr] <= b_wdata;
end
// This is the read side of the small dual-clock staging RAM. The host
// must finish writes before commit; commit is the coherency boundary.
always_ff @(posedge dclk) begin
rd_data_q <= stage_mem[rd_addr];
end
(* altera_attribute = "-name SYNCHRONIZER_IDENTIFICATION FORCED" *)
logic [2:0] commit_sync;
logic commit_seen;
logic [31:0] expected_crc_q;
logic [7:0] copy_idx;
logic [31:0] crc_acc;
typedef enum logic [1:0] { S_IDLE, S_PRIME, S_COPY } state_t;
state_t state;
always_ff @(posedge dclk or negedge dreset_n) begin
if (!dreset_n) begin
commit_sync <= 3'b000;
commit_seen <= 1'b0;
expected_crc_q <= 32'd0;
rd_addr <= 8'd0;
copy_idx <= 8'd0;
crc_acc <= 32'd0;
crc <= 32'd0;
clut_wr_en <= 1'b0;
clut_wr_idx <= 8'd0;
clut_wr_data <= 32'd0;
busy <= 1'b0;
done_tgl <= 1'b0;
state <= S_IDLE;
end else begin
commit_sync <= {commit_sync[1:0], b_commit_tgl};
clut_wr_en <= 1'b0;
// Consume any commit edge observed while a copy is live. A host
// retry belongs after done, never behind the current transaction.
if (busy && (commit_sync[2] != commit_seen))
commit_seen <= commit_sync[2];
case (state)
S_IDLE: begin
// A commit that arrives during a copy is deliberately not
// consumed. The host polls busy/done before reusing bank.
if (commit_sync[2] != commit_seen) begin
commit_seen <= commit_sync[2];
expected_crc_q <= b_expected_crc;
rd_addr <= 8'd0;
copy_idx <= 8'd0;
crc_acc <= 32'd0;
busy <= 1'b1;
state <= S_PRIME;
end
end
S_PRIME: begin
// Prime the synchronous read before writing entry zero.
rd_addr <= 8'd1;
state <= S_COPY;
end
S_COPY: begin
clut_wr_en <= 1'b1;
clut_wr_idx <= copy_idx;
clut_wr_data <= rd_data_q;
crc_acc <= crc_acc + rd_data_q;
if (copy_idx == 8'hFF) begin
crc <= crc_acc + rd_data_q;
busy <= 1'b0;
done_tgl <= ~done_tgl;
state <= S_IDLE;
end else begin
copy_idx <= copy_idx + 8'd1;
rd_addr <= rd_addr + 8'd1;
end
end
default: state <= S_IDLE;
endcase
end
end
// Keep expected_crc_q live in synthesis as an explicit documentation of
// the host/design transaction contract. The bridge returns `crc` on done;
// software compares it against its expected sum before GO.
wire _unused_expected_crc = ^expected_crc_q;
endmodule : clut_stage_cdc
+84 -2
View File
@@ -308,6 +308,13 @@ module ps2_hps_bridge (
input logic [31:0] lpddr_bresp_err_i, // (folded into 0x02C status)
input logic [31:0] lpddr_fifo_ovf_i, // (folded into 0x02C status)
input logic lpddr_idle_i, // 0x02C[0] — writer idle/done
input logic clear_done_i, // 0x02C[7] — Ch357 persistent-Z preclear complete (EMIF domain; synced).
// Host MUST wait for this before the first GO (z_rmw clear_start done).
input logic [31:0] frag_drops_i, // Ch357 — persistent-Z request-FIFO dropped-fragment count (design_clk).
// 0x0EC snapshot (latched while drained/quiescent) + 0x02C[8] sticky-nonzero.
input logic frame_drained_i, // 0x02C[6] — Ch353 ordered drain ack (EMIF domain; synced). STABLE,
// unlike transient idle: asserts only after the EOF marker's last BRESP,
// so the host confirms the FULL render drained (not a mid-batch idle).
// ---- Ch319 Brick 3: LPDDR4B read-probe (HPS reads FB content back through the
// bridge — no /dev/mem). 0x03C: WRITE sets the byte address + triggers a read;
@@ -393,6 +400,18 @@ module ps2_hps_bridge (
input logic [15:0] feeder_records_i, // 0x0E4 records_emitted (async counter)
input logic [31:0] feeder_waits_i, // 0x0E8 fifo_wait_cycles (async counter)
// Ch367 -- runtime CLUT staging. HPS writes a 1 KiB bank at
// 0x200..0x5FC, stores its expected sum32 at 0x1E4, then writes bit0
// at 0x1E0 to commit. One toggle crosses to the design-clock copier.
output logic [7:0] clut_stage_waddr_o,
output logic [31:0] clut_stage_wdata_o,
output logic clut_stage_we_o,
output logic clut_commit_tgl_o,
output logic [31:0] clut_expected_crc_o,
input logic clut_busy_i,
input logic clut_done_tgl_i,
input logic [31:0] clut_crc_i,
// ---- AXI4 slave (signature matches ps2_hps_bridge_null) ----
input logic [3:0] s_axi_awid,
input logic [37:0] s_axi_awaddr,
@@ -648,6 +667,10 @@ module ps2_hps_bridge (
// is IDLE, so latch them into clk-domain snapshots WHILE the synced idle is high (data stable -> coherent).
// The write-probe error counter latches on the synced write-done edge (count stable when a write completes).
logic [1:0] lpddr_idle_sync;
logic [1:0] frame_drained_sync; // Ch353 — 2-FF sync of frame_drained_i into the bridge clock
logic [1:0] clear_done_sync; // Ch357 — 2-FF sync of clear_done_i (persistent-Z preclear done)
logic [31:0] frag_drops_snap_q; // Ch357 — clk-domain snapshot of frag_drops_i, latched while drained (quiescent)
logic frag_drops_nz_sticky_q; // Ch357 — sticky: set once any per-scene snapshot is nonzero (fail-closed drop flag)
logic [31:0] lpddr_bytes_snap_q;
logic [31:0] lpddr_bursts_snap_q;
logic [31:0] lpddr_bresp_err_snap_q;
@@ -793,7 +816,13 @@ module ps2_hps_bridge (
// by addr[37:5]==33'h08 (= 0x100/32).
// Anything else reads 0.
reg_read = 32'd0;
if (addr[37:8] == '0) begin
if (addr == 38'h00000001E0) begin
reg_read = {30'd0, clut_done_sync[2], clut_busy_sync[1]}; // CLUT_STATUS
end else if (addr == 38'h00000001E4) begin
reg_read = clut_expected_crc_q; // CLUT_EXPECTED_CRC
end else if (addr == 38'h00000001E8) begin
reg_read = clut_crc_q; // CLUT_RESULT_CRC
end else if (addr[37:8] == '0) begin
case (addr[7:2])
6'h00: reg_read = CORE_ID; // 0x000
6'h01: reg_read = ABI_VERSION; // 0x004
@@ -808,7 +837,8 @@ module ps2_hps_bridge (
6'h0A: reg_read = raster_overflow_count; // 0x028
// 0x02C LPDDR_STATUS (R): [0]idle [1]bresp_err [2]fifo_ovf [3]rd_pending [4]scan_cache_valid [5]scan_rd_err.
// Ch352 — idle via 2-FF sync; bresp_err/fifo_ovf reduced from the clk-domain snapshots (coherent).
6'h0B: reg_read = {26'd0, lpddr_scan_err_sync[1], lpddr_scan_valid_sync[1], lpddr_rd_pending, (|lpddr_fifo_ovf_snap_q), (|lpddr_bresp_err_snap_q), lpddr_idle_sync[1]};
// Ch353 — [6]frame_drained (STABLE ordered drain ack; poll THIS, not transient [0]idle).
6'h0B: reg_read = {23'd0, frag_drops_nz_sticky_q, clear_done_sync[1], frame_drained_sync[1], lpddr_scan_err_sync[1], lpddr_scan_valid_sync[1], lpddr_rd_pending, (|lpddr_fifo_ovf_snap_q), (|lpddr_bresp_err_snap_q), lpddr_idle_sync[1]};
6'h0C: reg_read = lpddr_bytes_snap_q; // 0x030 LPDDR_BYTES (Ch352 — clk-domain snapshot)
6'h0D: reg_read = lpddr_bursts_snap_q; // 0x034 LPDDR_BURSTS (Ch352 — clk-domain snapshot)
6'h0E: reg_read = lpddr_bresp_err_snap_q; // 0x038 LPDDR_BRESP_ERRS (Ch352 — clk-domain snapshot)
@@ -859,6 +889,7 @@ module ps2_hps_bridge (
6'h37: reg_read = {20'd0, feeder_addr_q}; // 0x0DC FEEDER_STG_ADDR (current write index)
6'h39: reg_read = {16'd0, feeder_records_i}; // 0x0E4 FEEDER_RECORDS (records_emitted)
6'h3A: reg_read = feeder_waits_i; // 0x0E8 FEEDER_WAITS (fifo_wait_cycles)
6'h3B: reg_read = frag_drops_snap_q; // 0x0EC LPDDR_FRAG_DROPS (Ch357 R): persistent-Z dropped-fragment snapshot (per-scene delta must be 0)
6'h18: reg_read = video_status; // 0x060 VIDEO_STATUS (Ch225)
6'h19: reg_read = hdmi_diag; // 0x064 HDMI_DIAG (Ch225)
// Ch248 — replaced the Ch226 fake DS2 with the real
@@ -950,6 +981,11 @@ module ps2_hps_bridge (
wire write_is_feeder_hi = write_in_window && (aw_addr_q[7:2] == 6'h39); // 0x0E4 high 32 -> commit {hi,lo}, addr++
wire write_is_feeder_go = write_in_window && (aw_addr_q[7:2] == 6'h3A); // 0x0E8 bit0 -> retrigger pulse
wire write_is_clut_commit = (aw_addr_q == 38'h00000001E0);
wire write_is_clut_expected= (aw_addr_q == 38'h00000001E4);
wire write_in_clut_stage = (aw_addr_q >= 38'h0000000200)
&& (aw_addr_q <= 38'h00000005FC);
// Ch223 — OSD compatibility-sink decode at 0x100..0x11F. The OSD
// block is a 32-byte window outside the first-128-byte side-effect
// region, so it gets its own `addr[37:5] == 33'h08` guard
@@ -1009,6 +1045,9 @@ module ps2_hps_bridge (
assign core_ctrl_d = wdata_lane[2:0];
assign core_pulse_we = write_accept && write_is_pulse;
assign hdmi_clr_pulse = core_pulse_we && wdata_lane[3];
assign clut_stage_we_o = write_accept && write_in_clut_stage;
assign clut_stage_waddr_o = (aw_addr_q - 38'h0000000200) >> 2;
assign clut_stage_wdata_o = wdata_lane;
// Ch222 — input-latch register file. One always_ff covers all three
// 32-bit latches; the lane-aligned `wdata_lane` (selected by
@@ -1059,6 +1098,10 @@ module ps2_hps_bridge (
tex_rd_errs_snap_q <= 32'd0;
lpddr_ctrl_commit_q <= 1'b0;
lpddr_idle_sync <= 2'b00;
frame_drained_sync <= 2'b00;
clear_done_sync <= 2'b00;
frag_drops_snap_q <= 32'd0;
frag_drops_nz_sticky_q<= 1'b0;
lpddr_bytes_snap_q <= 32'd0;
lpddr_bursts_snap_q <= 32'd0;
lpddr_bresp_err_snap_q<= 32'd0;
@@ -1089,12 +1132,22 @@ module ps2_hps_bridge (
end
// Ch352 — remaining status CDC. idle: 2-FF sync. FB-writer counters: capture WHILE idle (stable).
lpddr_idle_sync <= {lpddr_idle_sync[0], lpddr_idle_i};
frame_drained_sync <= {frame_drained_sync[0], frame_drained_i};
if (lpddr_idle_sync[1]) begin
lpddr_bytes_snap_q <= lpddr_bytes_i;
lpddr_bursts_snap_q <= lpddr_bursts_i;
lpddr_bresp_err_snap_q <= lpddr_bresp_err_i;
lpddr_fifo_ovf_snap_q <= lpddr_fifo_ovf_i;
end
// Ch357 — persistent-Z preclear ack: 2-FF sync (single-bit, EMIF domain), same as frame_drained.
clear_done_sync <= {clear_done_sync[0], clear_done_i};
// Ch357 — drop count: latch WHILE drained (fragment flow stopped -> frag_drops_i stable, coherent multi-bit
// capture, same idle-gated idea as the FB-writer counters). The sticky flag OR-accumulates any nonzero
// snapshot so a single dropped fragment latches a fail-closed bit the host reads after every scene.
if (frame_drained_sync[1]) begin
frag_drops_snap_q <= frag_drops_i;
frag_drops_nz_sticky_q <= frag_drops_nz_sticky_q | (|frag_drops_i);
end
// write-probe error count: stable when a write completes -> latch on the synced write-done edge.
if (lpddr_wr_done_sync[2] != lpddr_wr_done_sync[1])
lpddr_wr_bresp_err_snap_q <= lpddr_wr_bresp_err_i;
@@ -1178,6 +1231,35 @@ module ps2_hps_bridge (
assign feeder_stg_wdata_o = feeder_wdata_q;
assign feeder_go_tgl_o = feeder_go_tgl_q;
// The staging write port is in this bridge clock domain. Only the
// commit toggle crosses domains, after the host has written all entries.
logic clut_commit_tgl_q;
logic [31:0] clut_expected_crc_q;
logic [1:0] clut_busy_sync;
logic [2:0] clut_done_sync;
logic [31:0] clut_crc_q;
always_ff @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
clut_commit_tgl_q <= 1'b0;
clut_expected_crc_q <= 32'd0;
clut_busy_sync <= 2'b00;
clut_done_sync <= 3'b000;
clut_crc_q <= 32'd0;
end else begin
clut_busy_sync <= {clut_busy_sync[0], clut_busy_i};
clut_done_sync <= {clut_done_sync[1:0], clut_done_tgl_i};
if (clut_done_sync[2] != clut_done_sync[1])
clut_crc_q <= clut_crc_i;
if (write_accept) begin
if (write_is_clut_expected) clut_expected_crc_q <= wdata_lane;
if (write_is_clut_commit && wdata_lane[0] && !clut_busy_sync[1])
clut_commit_tgl_q <= ~clut_commit_tgl_q;
end
end
end
assign clut_commit_tgl_o = clut_commit_tgl_q;
assign clut_expected_crc_o = clut_expected_crc_q;
// Ch223 — OSD-sink register file. Same pattern as Ch222.
// Ch245 — OSD_TRIGGER added with sibling-ABI semantics: bits are
// SET by single-cycle pulses from the menu FSM and CLEARED by HPS
+1
View File
@@ -85,6 +85,7 @@ module tile_ram_cdc (
// Three stages let us compute an edge detector against the
// already-resampled bits ([2] ^ [1]), giving the wr_pulse a full
// dclk cycle of bclk_wr_index/data stability before we sample.
(* altera_attribute = "-name SYNCHRONIZER_IDENTIFICATION FORCED" *)
logic [2:0] toggle_sync;
always_ff @(posedge dclk or negedge dreset_n) begin
if (!dreset_n)
+409 -15
View File
@@ -365,6 +365,10 @@ module de25_nano_psmct32_raster_demo_top (
// bridge into an unreadable state — only the design domain
// downstream of `core_rst_n` is held.
wire bridge_core_reset_req;
// Declared before every design-domain consumer. The runtime CLUT CDC is
// instantiated above the reset synchronizer, so an inline declaration at
// the assignment site would create an implicit, undriven early net.
wire core_rst_n;
// ---- Ch229 tile broadcast (bridge CLOCK2_50 → tile_ram_cdc) ----
// Driven by `u_hps_bridge` inside USE_QSYS_TOP; tied safe (zero +
@@ -413,6 +417,35 @@ module de25_nano_psmct32_raster_demo_top (
wire feeder_stg_we_pulse_w = fdr_we_sync[2] ^ fdr_we_sync[1]; // 1 design_clk pulse per commit
wire feeder_go_pulse_w = fdr_go_sync[2] ^ fdr_go_sync[1]; // 1 design_clk pulse per retrigger
// Ch367 -- runtime CLUT residency. The bridge writes a small palette
// bank at CLOCK2_50; one commit toggle starts the bounded design-clock
// copy into the existing CLUT writer port.
wire [7:0] clut_stage_waddr_w;
wire [31:0] clut_stage_wdata_w;
wire clut_stage_we_w, clut_commit_tgl_w;
wire [31:0] clut_expected_crc_w;
wire runtime_clut_wr_en_w, runtime_clut_busy_w, runtime_clut_done_tgl_w;
wire [7:0] runtime_clut_wr_idx_w;
wire [31:0] runtime_clut_wr_data_w, runtime_clut_crc_w;
clut_stage_cdc u_runtime_clut (
.bclk (CLOCK2_50),
.breset_n (~ninit_done),
.b_we (clut_stage_we_w),
.b_waddr (clut_stage_waddr_w),
.b_wdata (clut_stage_wdata_w),
.b_commit_tgl (clut_commit_tgl_w),
.b_expected_crc(clut_expected_crc_w),
.dclk (design_clk),
.dreset_n (core_rst_n),
.clut_wr_en (runtime_clut_wr_en_w),
.clut_wr_idx (runtime_clut_wr_idx_w),
.clut_wr_data (runtime_clut_wr_data_w),
.busy (runtime_clut_busy_w),
.done_tgl (runtime_clut_done_tgl_w),
.crc (runtime_clut_crc_w)
);
// ---- Ch248 platform DS2 wired controller --------------------------
// The shared `ds2_controller` runs on sys_clk (= CLOCK2_50 = 50 MHz),
// drives CLK/CMD/ATTN to the controller, samples DATA, and surfaces
@@ -502,7 +535,39 @@ module de25_nano_psmct32_raster_demo_top (
// Ch327a — line-buffer scanout geometry (the SCALABLE display path: O(width) BRAM, not
// O(width*height) like the frame-cache). For the Ch326 PSMCT32 spill FB: 128 px * 4 B =
// 512 B/row = 16 beats. (Default = the Ch321 128x128 PSMCT16 demo: 256 B/row = 8 beats.)
`ifdef GS_TILE_SPILL
`ifdef GS_SH3_LPDDR_FB
`ifdef GS_SH3_LPDDR_FB_Z
`ifdef GS_SH3_LPDDR_FB_640
localparam int SCANOUT_LB_STRIDE = 2560; // Ch358 — persistent-Z at NATIVE 640 px * 4 B PSMCT32 (FBW=10)
localparam int SCANOUT_LB_ROW_BEATS = 80; // 2560 / 32
localparam int SCANOUT_LB_NROWS = 480; // Ch358 — native 640x480, size 0x12C000, 38400 beats/frame
`else
localparam int SCANOUT_LB_STRIDE = 1024; // Ch357 — persistent-Z strong-reject fixture: 256 px * 4 B PSMCT32
localparam int SCANOUT_LB_ROW_BEATS = 32; // 1024 / 32
localparam int SCANOUT_LB_NROWS = 210; // Ch357 — strong-reject primary (256x210), 6720 beats/frame
`endif
`elsif GS_SH3_LPDDR_FB_640
localparam int SCANOUT_LB_STRIDE = 2560; // Ch357 — NATIVE 640 px * 4 B PSMCT32 (FBW=10)
localparam int SCANOUT_LB_ROW_BEATS = 80; // 2560 / 32
localparam int SCANOUT_LB_NROWS = 480; // Ch357 — native 640x480, size 0x12C000, 38400 beats/frame
`elsif GS_SH3_LPDDR_FB_SCHED
localparam int SCANOUT_LB_STRIDE = 1536; // Ch356 — 384 px * 4 B PSMCT32 (FBW=6, N-texture scheduler union)
localparam int SCANOUT_LB_ROW_BEATS = 48; // 1536 / 32
localparam int SCANOUT_LB_NROWS = 381; // Ch356 — 3-epoch union bbox (384x381), 18288 beats/frame
`elsif GS_SH3_LPDDR_FB_MULTITEX
localparam int SCANOUT_LB_STRIDE = 1280; // Ch355 — 320 px * 4 B PSMCT32 (FBW=5, multi-texture A+B union)
localparam int SCANOUT_LB_ROW_BEATS = 40; // 1280 / 32
localparam int SCANOUT_LB_NROWS = 381; // Ch355 — A+B union bbox (320x381)
`elsif GS_SH3_LPDDR_FB_MULTI
localparam int SCANOUT_LB_STRIDE = 1024; // 256 px * 4 B PSMCT32
localparam int SCANOUT_LB_ROW_BEATS = 32;
localparam int SCANOUT_LB_NROWS = 338; // Ch354 — multi-draw union bbox (256x338)
`else
localparam int SCANOUT_LB_STRIDE = 1024; // Ch353 — 256 px * 4 B PSMCT32
localparam int SCANOUT_LB_ROW_BEATS = 32; // 1024 / 32
localparam int SCANOUT_LB_NROWS = 334; // Ch353 — single SH3 draw bounding box (256x334)
`endif
`elsif GS_TILE_SPILL
localparam int SCANOUT_LB_STRIDE = 1024; // Ch327b — 256 px * 4 B PSMCT32
localparam int SCANOUT_LB_ROW_BEATS = 32; // 1024 / 32
localparam int SCANOUT_LB_NROWS = 256;
@@ -510,6 +575,26 @@ module de25_nano_psmct32_raster_demo_top (
localparam int SCANOUT_LB_STRIDE = 256;
localparam int SCANOUT_LB_ROW_BEATS = 8;
localparam int SCANOUT_LB_NROWS = 128;
`endif
// Ch418 — full-frame SH3 profiles preserve the dump's 512-pixel
// FBW=8 source geometry while presenting it across the board's
// 640-pixel VGA/HDMI active line. Other profiles remain 1:1.
`ifdef GS_SH3_LPDDR_FB_640
localparam bit SCANOUT_LB_H_STRETCH_5_TO_4 = 1'b1;
localparam int SCANOUT_LB_V_SOURCE_START = 32;
localparam bit SCANOUT_LB_V_STRETCH_15_TO_14 = 1'b1;
localparam bit SCANOUT_LB_V_LINEAR_FILTER = 1'b0;
localparam bit SCANOUT_LB_H_LINEAR_FILTER = 1'b0;
localparam bit SCANOUT_LB_BINOMIAL_3X3_FILTER = 1'b1;
localparam int SCANOUT_LB_H_SOURCE_PIXELS = 512;
`else
localparam bit SCANOUT_LB_H_STRETCH_5_TO_4 = 1'b0;
localparam int SCANOUT_LB_V_SOURCE_START = 0;
localparam bit SCANOUT_LB_V_STRETCH_15_TO_14 = 1'b0;
localparam bit SCANOUT_LB_V_LINEAR_FILTER = 1'b0;
localparam bit SCANOUT_LB_H_LINEAR_FILTER = 1'b0;
localparam bit SCANOUT_LB_BINOMIAL_3X3_FILTER = 1'b0;
localparam int SCANOUT_LB_H_SOURCE_PIXELS = 0;
`endif
// Initialize at declaration so iverilog doesn't bring up the
// sim with an X on hps_reset_sync[1] — that X would AND into
@@ -529,7 +614,7 @@ module de25_nano_psmct32_raster_demo_top (
end
wire hps_core_reset = hps_reset_sync[1];
wire core_rst_n = rst_sync[1] & ~hps_core_reset;
assign core_rst_n = rst_sync[1] & ~hps_core_reset;
// -------------------------------------------------------------
// core_go pulse — wait GO_DELAY cycles after core_rst_n
@@ -592,6 +677,7 @@ module de25_nano_psmct32_raster_demo_top (
wire demo_flush_emit;
wire [31:0] demo_flush_addr;
wire [15:0] demo_flush_pix16;
wire [3:0] demo_flush_be;
wire [5:0] demo_flush_psm;
wire [31:0] demo_vram_raddr; // Ch320 — PCRTC scanout VRAM byte address
wire demo_pix_window; // Ch320 — high inside the displayed frame window
@@ -622,6 +708,86 @@ module de25_nano_psmct32_raster_demo_top (
wire [31:0] demo_tile_reload_color; // reloaded color for raddr (1-cyc)
wire [31:0] demo_tile_reload_z; // reloaded Z for raddr (1-cyc)
wire [2:0] demo_tile_phase; // Ch323 diag — current tile phase (event-counter source)
// Ch357 — persistent-Z ROP fragment stream (perspective path). u_demo emits per-fragment
// {x,y,z} aligned to the flush color; gs_lpddr_zc_emit (under GS_SH3_LPDDR_FB_Z) does the
// PSMZ16S clamp16 read/compare/update and emits color ONLY on Z-pass. Inert otherwise.
wire [11:0] demo_flush_x;
wire [11:0] demo_flush_y;
wire [31:0] demo_flush_z;
wire demo_flush_abe;
wire [16:0] demo_flush_alpha;
wire demo_flush_ztest;
wire [1:0] demo_flush_ztst;
wire demo_flush_zmsk;
// ============================ Ch353 render epoch (GS_SH3_LPDDR_FB) ============================
// Codex round 5: with FEEDER_AUTOSTART=0 the boot setup ITSELF transitions into C_READY, so a flush gated on a
// feeder_ready RISE alone would enqueue a FALSE end-of-scene EOF at setup — which would assert frame_drained the
// instant the host later arms the writer. Gate the flush on an in-flight render instead:
// sh3_render_inflight = set by an ACCEPTED host GO (feeder_go pulse WHILE ready)
// sh3_fb_flush = sh3_render_inflight && feeder_ready RISE (render+drain complete) -> ONE EOF per render
// sh3_render_inflight = cleared when the flush is issued
// The writer's ordered drain ack (sh3_frame_drained, EMIF domain) then HARD-gates the scanout (no timeout).
// Proven end-to-end by tb_top_psmct32_sh3_lpddr_fb_seq. Inert (flush tied 0) for every other profile.
`ifdef GS_SH3_LPDDR_FB
wire sh3_frame_drained; // driven by u_lpddr_axi .frame_drained (EMIF/axi domain)
logic sh3_fb_flush, sh3_render_inflight, sh3_ready_q;
wire sh3_ready_rise = feeder_ready_w && !sh3_ready_q;
always_ff @(posedge design_clk or negedge core_rst_n) begin
if (!core_rst_n) begin sh3_fb_flush<=1'b0; sh3_render_inflight<=1'b0; sh3_ready_q<=1'b0; end
else begin
sh3_ready_q <= feeder_ready_w;
sh3_fb_flush <= 1'b0;
if (feeder_go_pulse_w && feeder_ready_w) sh3_render_inflight <= 1'b1; // accepted GO opens the epoch
if (sh3_render_inflight && sh3_ready_rise) begin
sh3_fb_flush <= 1'b1; // exactly one EOF per render
sh3_render_inflight <= 1'b0;
end
end
end
`else
wire sh3_fb_flush = 1'b0;
wire sh3_frame_drained = 1'b0; // non-SH3 profiles: tie off (fed to the bridge diagnostic, unused there)
`endif
`ifdef GS_SH3_LPDDR_FB_Z
// ---- Ch357 persistent-Z ROP feed (design_clk) ----
// The raster flush is the fragment producer; sh3_fb_flush (proven one-per-render ordered
// scene-completion marker) rides the SAME request FIFO as the end-of-scene marker. clamp16 is
// the vendored PCSX2 PSMZ16S source-Z saturation: min(z, 0xFFFF).
wire [15:0] zc_g_zq_w = (|demo_flush_z[31:16]) ? 16'hFFFF : demo_flush_z[15:0];
wire zc_frag_v = demo_flush_emit && (demo_flush_psm == 6'h00); // PSMCT32 raster fragment
wire zc_g_valid = zc_frag_v || sh3_fb_flush;
wire zc_g_scene = sh3_fb_flush; // ordered end-of-scene marker
wire [11:0] zc_g_x = demo_flush_x;
wire [11:0] zc_g_y = demo_flush_y;
wire [15:0] zc_g_zq = zc_g_zq_w;
wire zc_g_zmsk = demo_flush_zmsk;
wire zc_g_ztest = demo_flush_ztest;
wire [1:0] zc_g_ztst = demo_flush_ztst;
wire [31:0] zc_g_color = demo_flush_color32;
wire [16:0] zc_g_alpha = demo_flush_alpha;
wire [3:0] zc_g_be = demo_flush_be;
wire zc_g_ready; // driven by zc_emit
// Fragment-drop guard: the request async FIFO SILENTLY drops when g_ready is low and the raster
// cannot stall. Count every dropped request; the bridge snapshots this (0x0EC) + a sticky-nonzero bit
// (0x02C[8]) so the HOST proves zero drops per scene (fail-closed), not just the randomized sim.
reg [31:0] zc_g_drops_q;
always_ff @(posedge design_clk or negedge core_rst_n) begin
if (!core_rst_n) zc_g_drops_q <= 32'd0;
else if (zc_g_valid && !zc_g_ready) zc_g_drops_q <= zc_g_drops_q + 32'd1;
end
wire [31:0] zc_g_drops = zc_g_drops_q;
// Z-cache invalidate once at the initial host ARM (after the host preclears color+Z, before GO 1).
// arm rises exactly once per session; the pulse invalidates gs_lpddr_z_rmw's write-back line so Z
// then persists across every scheduler epoch (only clear_start invalidates; scene markers keep it).
wire zc_clear_done;
reg arm_e1, arm_e2, arm_e3;
always_ff @(posedge emif_clk or negedge emif_reset_n) begin
if (!emif_reset_n) begin arm_e1<=1'b0; arm_e2<=1'b0; arm_e3<=1'b0; end
else begin arm_e1<=lpddr_arm_w; arm_e2<=arm_e1; arm_e3<=arm_e2; end
end
wire zc_clear_start = arm_e2 && !arm_e3; // emif-domain rising edge of arm
`endif
// Ch352 (Codex guardrail #1) — LPDDR texture-cache geometry as PROFILE PARAMETERS, not hardcoded over the
// Ch322 tritex demo. SH3 real-draw crop: full 512x512 PSMT8 (262 KiB) in LPDDR, sampled at TBP=1024
// (0x40000). Every other GS_LPDDR_TEX profile keeps the tritex 8x8 (2 KiB) geometry.
@@ -629,6 +795,10 @@ module de25_nano_psmct32_raster_demo_top (
localparam int TEXC_VRAM_BASE = 32'h0004_0000; // crop TEX_VRAM_BASE = NEW_TBP(1024)*256
localparam int TEXC_BYTES = 262144; // 512x512 PSMT8
localparam int TEXC_NBEATS = 8192; // 262144 / 32 (single-beat 32 B reads)
`elsif GS_SH3_LPDDR_FB
localparam int TEXC_VRAM_BASE = 32'h0004_0000; // Ch353 — same 512x512 PSMT8 texture as the crop profile
localparam int TEXC_BYTES = 262144;
localparam int TEXC_NBEATS = 8192;
`else
localparam int TEXC_VRAM_BASE = 2048; // Ch322 tritex 8x8 PSMCT32
localparam int TEXC_BYTES = 2048;
@@ -642,12 +812,24 @@ module de25_nano_psmct32_raster_demo_top (
// this build path. Removing it retires the Ch159+ critical
// path and frees the timing budget for a faster clock.
.STRIP_HW_DIVIDER (1'b1),
// Ch352 — SEQUENTIAL gradient divider (registered iterations, no combinational cone). This REPLACES the
// combinational divide + the multicycle/false_path SDC approaches, which all made the Quartus fitter
// grind on the divider cone for hours. The sequential divider closes timing with NO SDC exception.
// Bit-exact to `/` (tb_gs_grad_divider) so the SH3 oracle stays 96.2%. GRAD_DIV_CYCLES below is ignored.
.GRAD_SEQ_DIVIDER (1'b1),
.GRAD_DIV_CYCLES (4),
// Ch357 (Codex) — SEQUENTIAL gradient divider (registered iterations, NO combinational cone), now the board
// default. The Ch352 seq-divider "prep trap" (>18 min, never reached Place) was caused by the combinational
// signed `/` remaining UNCONDITIONALLY in the RTL alongside the sequential divider — Quartus inferred BOTH,
// and the giant LPM divide cone is what ground Place/Prep for hours (twice: the SDC multicycle on the ~1157-
// source combinational cone, then the numerator-register variant, both ground). Ch357 makes the two dividers
// MUTUALLY EXCLUSIVE via generate (gs_stub g_combdiv/g_no_combdiv): at GRAD_SEQ_DIVIDER=1 the combinational `/`
// does not exist in the netlist at all, so there is no divider cone and NO gradient SDC exception is applied.
// Ch357 FINAL (Codex, 2026-07-05, after the isolation experiment ROUTED): keep the registered-numerator
// COMBINATIONAL divide (grad_num_sel_q/grad_det_q -> grad_result_q). Ch417 widens the divide from 56/32 to
// 64/35 bits, so use GRAD_DIV_CYCLES=5: design_clk = 40 ns (25 MHz) gives a 200 ns functional window instead
// of relying on the old 160 ns window measured against the former 108 ns routed cone. The setup=5/hold=4
// multicycle is applied STA-ONLY in the SDC (never
// during quartus_fit), so the fitter keeps its known-CONVERGENT "hopeless single-cycle" behavior (places in
// seconds) while final quartus_sta fail-closes at the 200 ns window. SEQUENTIAL divider (=1) is NOT used — it
// traps this Agilex fitter even with the combinational `/` generate-stripped. Bit-exact to `/`; SH3 oracle
// unchanged (tb_gs_grad_divider + the persp board-path equivalence 252/252).
.GRAD_SEQ_DIVIDER (1'b0),
.GRAD_DIV_CYCLES (5),
// Ch163 — strip the PCRTC magnification divider. The demo
// locks MAGH=MAGV=0 so the divisor is constant 1 and the
// `hwin_rel / hmag_factor` / `vwin_rel / vmag_factor` math
@@ -901,6 +1083,52 @@ module de25_nano_psmct32_raster_demo_top (
.CLUT_CSM1_ENABLE (1'b1),
.FEEDER_ENABLE (1'b1),
.FEEDER_STG_WORDS (768) // 68 clipped tris -> ~655 staging words
`elsif GS_SH3_LPDDR_FB
// Ch353 — the SH3 real draw on an LPDDR-ONLY direct PSMCT32 framebuffer (the FULL 256x334 bounding box, NOT
// the Ch352 256x120 BRAM crop). Same feeder-driven SH3 config as GS_SH3_REAL_DRAW_DEMO (real CSM1 CLUT +
// Ch351 widened perspective + GS_LPDDR_TEX cache, GRAD_SEQ_DIVIDER=0 from the shared prefix = the known-
// fitting combinational divide), but: FB_LPDDR_ONLY suppresses the BRAM raster mirror (the flush stream
// drives the PSMCT32 LPDDR writer instead of BRAM), and FEEDER_AUTOSTART=0 so boot setup reaches C_READY
// WITHOUT a render — the host arms the writer THEN pulses GO (0x0E8), and the render-epoch flush emits exactly
// one EOF -> one frame_drained, which HARD-gates the line-buffer scanout (no timeout). VRAM stays 128 KiB /
// CBP=480 (Codex option A — the known-fitting Ch352 config; the CLUT->CBP0 BRAM-shrink is deferred). Proven
// end-to-end by tb_top_psmct32_sh3_lpddr_fb_seq (the mandatory fit gate). GS_LPDDR4B_FB + GS_LPDDR_TEX must
// ALSO be defined (the build profile sets all three).
.VRAM_BYTES (128 * 1024),
.RAM_SIZE_BYTES (32 * 1024),
.VRAM_ENABLE_READ2 (1'b0),
.PSMCT32_SWIZZLE (1'b0),
.COMBINED_TAZ (1'b0),
.TILE_LOCAL (1'b0),
.TILE_COLS (1),
.TILE_ROWS (1),
.TILE_MULTIPRIM (1'b0),
.TILE_PRIM_COUNT (1),
.TILE_FIFO_DEPTH (8),
.BIN_BUFFER_ENABLE (1'b0),
.HEARTBEAT_SPLICE_ENABLE (1'b0),
.PERSPECTIVE_CORRECT(1'b1),
.SUBPIXEL_XY (1'b1), // Ch416 — preserve native XYZ 12.4 and sample pixel centers
.PERSP_RECIP_IDX_BITS(11),
.BILINEAR_ENABLE (1'b1),
.PALETTE_BILINEAR (1'b1),
.CLUT_CSM1_ENABLE (1'b1),
.FEEDER_ENABLE (1'b1),
`ifdef GS_SH3_LPDDR_FB_Z
.FEEDER_STG_WORDS (2048), // Ch402 — character draws split into staging-safe single-draw epochs
`elsif GS_SH3_LPDDR_FB_640
.FEEDER_STG_WORDS (2048), // Ch357 — native 640x480, same runtime-staged per-epoch list (619 words)
`elsif GS_SH3_LPDDR_FB_SCHED
.FEEDER_STG_WORDS (2048), // Ch356 — runtime-staged per-epoch list (68 tris / 619 words) + headroom
`elsif GS_SH3_LPDDR_FB_MULTITEX
.FEEDER_STG_WORDS (2048), // Ch355 — runtime-staged per-scene list (68 tris / 619 words) + headroom
`elsif GS_SH3_LPDDR_FB_MULTI
.FEEDER_STG_WORDS (2048), // Ch354 — 3-draw combined list = 204 tris / 1843 words (Codex-approved)
`else
.FEEDER_STG_WORDS (768), // Ch353 — single draw = 68 tris / ~655 words
`endif
.FEEDER_AUTOSTART (1'b0), // Ch353 — no boot render; host arms writer then GOes
.FB_LPDDR_ONLY (1'b1) // Ch353 — flush -> LPDDR writer, no BRAM FB mirror
`elsif GS_TILE_LATE_DEMO
// Ch316 — LEADING-EMPTY-TILE traversal fix. ONE prim, ONLY in the LAST tile
// t15 of the 4x4 grid; tiles t0..t14 are empty and precede it. Proves the
@@ -1282,6 +1510,7 @@ module de25_nano_psmct32_raster_demo_top (
.flush_emit_o (demo_flush_emit),
.flush_addr_o (demo_flush_addr),
.flush_pix16_o(demo_flush_pix16),
.flush_be_o (demo_flush_be),
.flush_psm_o (demo_flush_psm),
.vram_read_addr_o(demo_vram_raddr), // Ch320 — PCRTC scanout addr for LPDDR4B scanout
.pix_window_o (demo_pix_window), // Ch320 — displayed-frame window gate
@@ -1293,6 +1522,15 @@ module de25_nano_psmct32_raster_demo_top (
.tex_cache_ready_i(demo_tex_cache_ready),
.tex_cache_hits_o(demo_tex_cache_hits),
.tex_bram_hits_o (demo_tex_bram_hits),
// Ch357 — persistent-Z ROP fragment coords + Z (perspective path), aligned to the flush color.
.flush_x_o (demo_flush_x),
.flush_y_o (demo_flush_y),
.flush_z_o (demo_flush_z),
.flush_abe_o (demo_flush_abe),
.flush_alpha_o (demo_flush_alpha),
.flush_ztest_o (demo_flush_ztest),
.flush_ztst_o (demo_flush_ztst),
.flush_zmsk_o (demo_flush_zmsk),
// Ch323 — tile color+Z spill/reload (inert outputs when TILE_SPILL_ENABLE=0).
.flush_color32_o (demo_flush_color32),
.z_flush_emit_o (demo_z_flush_emit),
@@ -1317,7 +1555,11 @@ module de25_nano_psmct32_raster_demo_top (
.feeder_go_i (feeder_go_pulse_w),
.feeder_ready_o (feeder_ready_w),
.feeder_records_o (feeder_records_w),
.feeder_waits_o (feeder_waits_w)
.feeder_waits_o (feeder_waits_w),
.runtime_clut_wr_en_i (runtime_clut_wr_en_w),
.runtime_clut_wr_idx_i (runtime_clut_wr_idx_w),
.runtime_clut_wr_data_i(runtime_clut_wr_data_w),
.runtime_clut_busy_i (runtime_clut_busy_w)
);
`ifndef GS_LPDDR_TEX
// Ch322 — no texture cache: the texel tap returns BRAM (bram-top mux constant-folds
@@ -1592,8 +1834,21 @@ module de25_nano_psmct32_raster_demo_top (
end
end
wire video_src_eff = video_src_dclk;
`ifdef GS_SH3_LPDDR_FB
// Ch353 — HARD frame_drained gate (Codex round 5, NO timeout): blank BLACK until the ordered drain ack, THEN the
// line-buffer scanout serves the LPDDR FB. frame_ready_emif gates the EMIF prefetch enable; frame_ready_r (design)
// gates the HDMI source mux so unfilled line buffers can never show before the frame is drained. Failure (no ack)
// stays black forever — no timeout fallback (a partial FB is exactly the bug this handshake prevents).
logic [2:0] sh3_frd_dsync;
always_ff @(posedge design_clk or posedge async_rst_assert) begin
if (async_rst_assert) sh3_frd_dsync <= 3'd0; else sh3_frd_dsync <= {sh3_frd_dsync[1:0], sh3_frame_drained};
end
wire frame_ready_r = sh3_frd_dsync[2];
wire frame_ready_emif = sh3_frame_drained;
`else
wire frame_ready_r = 1'b1; // other profiles: BRAM mirror is a stable fallback, no gate
wire frame_ready_emif = 1'b1;
`endif
// emif-domain coherent bundled capture (LPDDR scanout enable + reader routing below).
logic [2:0] commit_e_sync; logic video_src_emif, scanout_lb_emif;
always_ff @(posedge emif_clk or negedge emif_reset_n) begin
@@ -2052,7 +2307,10 @@ module de25_nano_psmct32_raster_demo_top (
// Ch324 Brick 3 — LPDDR scanout config: the spill profile scans the 64x64 PSMCT32 color
// FB straight out of LPDDR at COLOR_SPILL_BASE (PCRTC addresses it BRAM-relative so
// VRAM_BASE stays 0). Other LPDDR profiles keep the FB-at-0 PSMCT16 mirror.
`ifdef GS_TILE_SPILL
`ifdef GS_SH3_LPDDR_FB
localparam [29:0] SCANOUT_FB_BASE = 30'd0; // Ch353 — FB at LPDDR base 0 (host sets 0x01C=0)
localparam bit SCANOUT_PSMCT32 = 1'b1; // full-frame PSMCT32
`elsif GS_TILE_SPILL
localparam [29:0] SCANOUT_FB_BASE = COLOR_SPILL_BASE;
localparam bit SCANOUT_PSMCT32 = 1'b1;
`else
@@ -2072,11 +2330,90 @@ module de25_nano_psmct32_raster_demo_top (
wire [7:0] fbw_awlen; wire [2:0] fbw_awsize; wire fbw_awvalid, fbw_awready;
wire [255:0] fbw_wdata; wire [31:0] fbw_wstrb; wire fbw_wlast, fbw_wvalid, fbw_wready;
wire [1:0] fbw_bresp; wire fbw_bvalid, fbw_bready;
gs_lpddr_axi_master #(.FIFO_DEPTH(16)) u_lpddr_axi (
`ifdef GS_SH3_LPDDR_FB_Z
// ================= Ch357 — persistent-Z emit (gs_lpddr_zc_emit) =================
// Replaces the plain color writer with a Z-then-color emitter: Z RMW (read/clamp16-compare/
// update) FIRST, color enqueued ONLY on pass. Three AXI streams, routed onto the shared LPDDR
// through the SAME de25 arbiters proven in tb_top_psmct32_sh3_zarb:
// * color writes (c_*) -> fbw_* == gs_lpddr_wr_arb s0 (FB-writer, priority)
// * Z writes (zw_*) -> gs_lpddr_wr_arb s2 (repurposed spill port; inert in this profile)
// * Z reads (zr_*) -> gs_lpddr_rd_arb s3 (reload port; inert in this profile)
// Fixture geometry: 256x210 strong-reject (Ch357) or NATIVE 640x480 (Ch358, +GS_SH3_LPDDR_FB_640).
// Z base 0x140000 (disjoint from color: 640x480x4=0x12C000 < 0x140000; and tex: Z top 0x1D6000 < 0x200000).
`ifdef GS_SH3_LPDDR_FB_640
localparam int ZFB_PXW = 640, ZFB_H = 480;
`else
localparam int ZFB_PXW = 256, ZFB_H = 210;
`endif
wire [31:0] zw_awaddr; wire [7:0] zw_awlen; wire [2:0] zw_awsize; wire [1:0] zw_awburst;
wire zw_awvalid, zw_awready;
wire [255:0] zw_wdata; wire [31:0] zw_wstrb; wire zw_wlast, zw_wvalid, zw_wready;
wire zw_bvalid, zw_bready; wire [1:0] zw_bresp;
wire [31:0] zr_araddr; wire [7:0] zr_arlen; wire [2:0] zr_arsize; wire [1:0] zr_arburst;
wire zr_arvalid, zr_arready;
wire [255:0] zr_rdata; wire [1:0] zr_rresp; wire zr_rlast, zr_rvalid, zr_rready;
wire [31:0] dr_araddr; wire [7:0] dr_arlen; wire [2:0] dr_arsize; wire [1:0] dr_arburst;
wire dr_arvalid, dr_arready;
wire [255:0] dr_rdata; wire [1:0] dr_rresp; wire dr_rlast, dr_rvalid, dr_rready;
wire [31:0] c_awaddr; wire [7:0] c_awlen; wire [2:0] c_awsize; wire [1:0] c_awburst;
wire c_awvalid;
wire [255:0] c_wdata; wire [31:0] c_wstrb; wire c_wlast, c_wvalid;
wire c_bready;
// Request-FIFO sizing history: Ch368's adjacent-frame runtime-CLUT test needed 3610 entries (4096 passed;
// 2048 clipped). The much larger Ch439 f52 production replay peaks at 6115 entries, establishing the 8192
// production size below. zc_g_drops remains the fail-closed hardware guard.
// (History: Ch357's tb_top_psmct32_sh3_zint showed a depth-64 request FIFO drops a small transient burst
// (2 frags) when the Z-RMW stalls on a run of cache-line misses while the raster — which cannot honor
// g_ready — keeps emitting; 256 sufficed for the zsched/zs640 scenes.)
gs_lpddr_zc_emit #(.COLBASE(32'h0000_0000), .ZBASE(32'h0014_0000),
// Ch439: the complete 311-epoch f52 replay peaks at 6,115
// queued requests. 8K retains 2,077 entries (34%) of
// measured headroom while halving the deep 93-bit FIFO's
// M20K/address-tree footprint at 310 MHz.
.FB_PXW(ZFB_PXW), .FB_H(ZFB_H), .REQ_DEPTH(8192), .COL_DEPTH(128)) u_zc_emit (
.gs_clk(design_clk), .gs_rst_n(core_rst_n), .enable(1'b1),
.g_valid(zc_g_valid), .g_ready(zc_g_ready), .g_x(zc_g_x), .g_y(zc_g_y), .g_zq(zc_g_zq),
.g_zmsk(zc_g_zmsk), .g_ztest(zc_g_ztest), .g_ztst(zc_g_ztst), .g_color(zc_g_color), .g_alpha(zc_g_alpha), .g_be(zc_g_be), .g_scene(zc_g_scene),
.axi_clk(emif_clk), .axi_rst_n(emif_reset_n),
.clear_start(zc_clear_start), .clear_done(zc_clear_done), .frame_drained(sh3_frame_drained),
.z_araddr(zr_araddr), .z_arlen(zr_arlen), .z_arsize(zr_arsize), .z_arburst(zr_arburst),
.z_arvalid(zr_arvalid), .z_arready(zr_arready),
.z_rdata(zr_rdata), .z_rresp(zr_rresp), .z_rlast(zr_rlast), .z_rvalid(zr_rvalid), .z_rready(zr_rready),
.d_araddr(dr_araddr), .d_arlen(dr_arlen), .d_arsize(dr_arsize), .d_arburst(dr_arburst),
.d_arvalid(dr_arvalid), .d_arready(dr_arready),
.d_rdata(dr_rdata), .d_rresp(dr_rresp), .d_rlast(dr_rlast), .d_rvalid(dr_rvalid), .d_rready(dr_rready),
.z_awaddr(zw_awaddr), .z_awlen(zw_awlen), .z_awsize(zw_awsize), .z_awburst(zw_awburst),
.z_awvalid(zw_awvalid), .z_awready(zw_awready),
.z_wdata(zw_wdata), .z_wstrb(zw_wstrb), .z_wlast(zw_wlast), .z_wvalid(zw_wvalid), .z_wready(zw_wready),
.z_bvalid(zw_bvalid), .z_bready(zw_bready), .z_bresp(zw_bresp),
.c_awaddr(c_awaddr), .c_awlen(c_awlen), .c_awsize(c_awsize), .c_awburst(c_awburst),
.c_awvalid(c_awvalid), .c_awready(fbw_awready),
.c_wdata(c_wdata), .c_wstrb(c_wstrb), .c_wlast(c_wlast), .c_wvalid(c_wvalid), .c_wready(fbw_wready),
.c_bvalid(fbw_bvalid), .c_bready(c_bready), .c_bresp(fbw_bresp),
.z_beats_read(), .z_beats_written(lpddr_bursts_w), .c_beats_written(lpddr_bytes_w),
.col_ovf(lpddr_fifo_ovf_w), .bresp_err(lpddr_bresp_err_w), .idle(lpddr_idle_w)
);
// Color master -> fbw_* (gs_lpddr_wr_arb s0). c_* carries no awid; the arb tags s0 internally.
assign fbw_awaddr = c_awaddr[29:0]; assign fbw_awburst = c_awburst; assign fbw_awid = 7'd0;
assign fbw_awlen = c_awlen; assign fbw_awsize = c_awsize; assign fbw_awvalid = c_awvalid;
assign fbw_wdata = c_wdata; assign fbw_wstrb = c_wstrb; assign fbw_wlast = c_wlast;
assign fbw_wvalid = c_wvalid; assign fbw_bready = c_bready;
assign emif_m_awaddr = 32'd0; assign emif_m_awid = 5'd0; // unused under _Z
`else
gs_lpddr_axi_master #(.FIFO_DEPTH(16)
`ifdef GS_SH3_LPDDR_FB
, .PIX_BYTES(4) // Ch353 — PSMCT32 = 8 px / 256-bit beat
`endif
) u_lpddr_axi (
.gs_clk(design_clk), .gs_rst_n(core_rst_n), .enable(1'b1),
.arm(lpddr_arm_w), .canary(lpddr_canary_w), .fb_base(lpddr_fb_base_w), .ctrl_commit(lpddr_ctrl_commit_w),
`ifdef GS_SH3_LPDDR_FB
.px_emit(demo_flush_emit && (demo_flush_psm == 6'h00)), // Ch353 — PSMCT32 raster emits only
.px_addr(demo_flush_addr), .px_pix32(demo_flush_color32), .flush(sh3_fb_flush),
`else
.px_emit(demo_flush_emit && (demo_flush_psm == 6'h02)),
.px_addr(demo_flush_addr), .px_pix16(demo_flush_pix16),
.px_addr(demo_flush_addr), .px_pix32({16'd0, demo_flush_pix16}), .flush(1'b0),
`endif
.axi_clk(emif_clk), .axi_rst_n(emif_reset_n),
.awaddr(emif_m_awaddr), .awlen(fbw_awlen), .awsize(fbw_awsize),
.awburst(fbw_awburst), .awid(emif_m_awid), .awvalid(fbw_awvalid),
@@ -2087,9 +2424,13 @@ module de25_nano_psmct32_raster_demo_top (
.beats_written(lpddr_bytes_w), .bursts_issued(lpddr_bursts_w),
.bresp_err_count(lpddr_bresp_err_w), .fifo_overflow_count(lpddr_fifo_ovf_w),
.idle(lpddr_idle_w)
`ifdef GS_SH3_LPDDR_FB
, .frame_drained(sh3_frame_drained) // Ch353 — ordered drain ack -> hard scanout gate
`endif
);
assign fbw_awaddr = emif_m_awaddr[29:0]; // EMIF addr is 30-bit (base 0)
assign fbw_awid = {2'b00, emif_m_awid}; // EMIF awid is 7-bit
`endif
`ifdef GS_LPDDR_TEX
// Ch322 — HPS write-probe (stage texture words) + 2:1 write arbiter (FB writer = priority).
wire [29:0] wp_awaddr; wire [1:0] wp_awburst; wire [6:0] wp_awid;
@@ -2116,10 +2457,17 @@ module de25_nano_psmct32_raster_demo_top (
.s1_awsize(wp_awsize), .s1_awvalid(wp_awvalid), .s1_awready(wp_awready),
.s1_wdata(wp_wdata), .s1_wstrb(wp_wstrb), .s1_wlast(wp_wlast), .s1_wvalid(wp_wvalid), .s1_wready(wp_wready),
.s1_bresp(wp_bresp), .s1_bvalid(wp_bvalid), .s1_bready(wp_bready),
`ifdef GS_SH3_LPDDR_FB_Z
// Ch357 — s2 = persistent-Z write stream from gs_lpddr_zc_emit (zw_*).
.s2_awaddr(zw_awaddr[29:0]), .s2_awburst(zw_awburst), .s2_awid(7'd6), .s2_awlen(zw_awlen), .s2_awsize(zw_awsize),
.s2_awvalid(zw_awvalid), .s2_awready(zw_awready), .s2_wdata(zw_wdata), .s2_wstrb(zw_wstrb), .s2_wlast(zw_wlast),
.s2_wvalid(zw_wvalid), .s2_wready(zw_wready), .s2_bresp(zw_bresp), .s2_bvalid(zw_bvalid), .s2_bready(zw_bready),
`else
// Ch323 — ports 2/3 (Z spill / HPS write-probe) unused in the GS_LPDDR_TEX build; tie inert.
.s2_awaddr(30'd0), .s2_awburst(2'b01), .s2_awid(7'd6), .s2_awlen(8'd0), .s2_awsize(3'b101),
.s2_awvalid(1'b0), .s2_awready(), .s2_wdata(256'd0), .s2_wstrb(32'd0), .s2_wlast(1'b0),
.s2_wvalid(1'b0), .s2_wready(), .s2_bresp(), .s2_bvalid(), .s2_bready(1'b1),
`endif
.s3_awaddr(30'd0), .s3_awburst(2'b01), .s3_awid(7'd5), .s3_awlen(8'd0), .s3_awsize(3'b101),
.s3_awvalid(1'b0), .s3_awready(), .s3_wdata(256'd0), .s3_wstrb(32'd0), .s3_wlast(1'b0),
.s3_wvalid(1'b0), .s3_wready(), .s3_bresp(), .s3_bvalid(), .s3_bready(1'b1),
@@ -2284,6 +2632,8 @@ module de25_nano_psmct32_raster_demo_top (
// line-buffer; other profiles keep the runtime bridge bit (default frame-cache).
`ifdef GS_TILE_SPILL
wire scanout_lb_eff = 1'b1;
`elsif GS_SH3_LPDDR_FB
wire scanout_lb_eff = 1'b1; // Ch353 — always line-buffer (a 256x334 frame-cache = 334 KiB)
`else
wire scanout_lb_eff = scanout_lb_emif; // Ch352 — emif-domain coherent capture (not raw bridge bit)
`endif
@@ -2303,7 +2653,14 @@ module de25_nano_psmct32_raster_demo_top (
);
gs_lpddr_scanout_lb #(.FB_BASE(SCANOUT_FB_BASE), .STRIDE_BYTES(SCANOUT_LB_STRIDE),
.ROW_BEATS(SCANOUT_LB_ROW_BEATS), .N_ROWS(SCANOUT_LB_NROWS),
.PSMCT32(SCANOUT_PSMCT32)) u_lpddr_scan_lb (
.PSMCT32(SCANOUT_PSMCT32),
.H_STRETCH_5_TO_4(SCANOUT_LB_H_STRETCH_5_TO_4),
.V_SOURCE_START(SCANOUT_LB_V_SOURCE_START),
.V_STRETCH_15_TO_14(SCANOUT_LB_V_STRETCH_15_TO_14),
.V_LINEAR_FILTER(SCANOUT_LB_V_LINEAR_FILTER),
.H_LINEAR_FILTER(SCANOUT_LB_H_LINEAR_FILTER),
.H_SOURCE_PIXELS(SCANOUT_LB_H_SOURCE_PIXELS),
.BINOMIAL_3X3_FILTER(SCANOUT_LB_BINOMIAL_3X3_FILTER)) u_lpddr_scan_lb (
.axi_clk(emif_clk), .axi_rst_n(emif_reset_n),
.enable(video_src_emif & scanout_lb_eff & frame_ready_emif),
.video_clk(design_clk), .frame_start(demo_video_vsync),
@@ -2354,6 +2711,9 @@ module de25_nano_psmct32_raster_demo_top (
.s3_arready(reload_ar_arready),
.s3_rdata(reload_r_rdata), .s3_rresp(reload_r_rresp), .s3_rlast(reload_r_rlast),
.s3_rvalid(reload_r_rvalid), .s3_rready(reload_r_rready),
.s4_araddr(dr_araddr[29:0]), .s4_arburst(dr_arburst), .s4_arid(7'd7),
.s4_arlen(dr_arlen), .s4_arsize(dr_arsize), .s4_arvalid(dr_arvalid), .s4_arready(dr_arready),
.s4_rdata(dr_rdata), .s4_rresp(dr_rresp), .s4_rlast(dr_rlast), .s4_rvalid(dr_rvalid), .s4_rready(dr_rready),
.m_araddr(emif_ar_araddr), .m_arburst(emif_ar_arburst), .m_arid(emif_ar_arid),
.m_arlen(emif_ar_arlen), .m_arsize(emif_ar_arsize), .m_arvalid(emif_ar_arvalid),
.m_arready(emif_ar_arready),
@@ -2413,6 +2773,14 @@ module de25_nano_psmct32_raster_demo_top (
.color_o(demo_tile_reload_color), .z_o(demo_tile_reload_z),
.reload_ready(demo_tile_reload_ready)
);
`elsif GS_SH3_LPDDR_FB_Z
// Ch357 — read-port-3 carries the persistent-Z READ stream from gs_lpddr_zc_emit (zr_*).
assign reload_ar_araddr=zr_araddr[29:0]; assign reload_ar_arburst=zr_arburst; assign reload_ar_arid=7'd6;
assign reload_ar_arlen=zr_arlen; assign reload_ar_arsize=zr_arsize; assign reload_ar_arvalid=zr_arvalid;
assign zr_arready=reload_ar_arready;
assign zr_rdata=reload_r_rdata; assign zr_rresp=reload_r_rresp; assign zr_rlast=reload_r_rlast;
assign zr_rvalid=reload_r_rvalid; assign reload_r_rready=zr_rready;
assign reload_color_beats_w=32'd0; assign reload_z_beats_w=32'd0; assign reload_rd_errs_w=32'd0;
`else
// no reload engine — tie read-port-3 inert (arvalid=0, rready=1 drains) + counters 0.
assign reload_ar_araddr=30'd0; assign reload_ar_arburst=2'b01; assign reload_ar_arid=7'd6;
@@ -2434,7 +2802,7 @@ module de25_nano_psmct32_raster_demo_top (
.gs_clk(design_clk), .gs_rst_n(core_rst_n), .enable(1'b1),
.arm(lpddr_arm_w), .canary(lpddr_canary_w), .fb_base(lpddr_fb_base_w), .ctrl_commit(lpddr_ctrl_commit_w),
.px_emit(demo_flush_emit && (demo_flush_psm == 6'h02)),
.px_addr(demo_flush_addr), .px_pix16(demo_flush_pix16),
.px_addr(demo_flush_addr), .px_pix32({16'd0, demo_flush_pix16}), .flush(1'b0),
.axi_clk(CLOCK2_50), .axi_rst_n(~ninit_done),
.awaddr(f2s_awaddr_w), .awlen(f2s_awlen_w), .awsize(f2s_awsize_w),
.awburst(f2s_awburst_w), .awid(f2s_awid_w), .awvalid(f2s_awvalid_w),
@@ -2682,6 +3050,15 @@ module de25_nano_psmct32_raster_demo_top (
.emif_hps_emif_ref_clk_0_clk (LPDDR4A_REFCLK_p)
);
// Ch357 — persistent-Z observability into the bridge (host fail-closed gates). Under GS_SH3_LPDDR_FB_Z the
// zc_emit preclear ack + drop counter; tied 0 for every other profile (the status bits read 0 / clear_done 0).
`ifdef GS_SH3_LPDDR_FB_Z
wire bridge_clear_done = zc_clear_done; // emif domain (from gs_lpddr_zc_emit) -> 0x02C[7]
wire [31:0] bridge_frag_drops = zc_g_drops; // design_clk domain -> 0x0EC snapshot + 0x02C[8] sticky
`else
wire bridge_clear_done = 1'b0;
wire [31:0] bridge_frag_drops = 32'd0;
`endif
// The minimal null AXI4 slave for the hps2fpga bridge. Same port
// signature as `splash_hps_bridge` so a future "real" ps2 bridge
// can be dropped in without re-plumbing the top.
@@ -2714,6 +3091,9 @@ module de25_nano_psmct32_raster_demo_top (
.lpddr_bresp_err_i(lpddr_bresp_err_w),
.lpddr_fifo_ovf_i (lpddr_fifo_ovf_w),
.lpddr_idle_i (lpddr_idle_w),
.frame_drained_i (sh3_frame_drained), // Ch353 — 0x02C[6] host-visible ordered drain ack (0 for non-SH3)
.clear_done_i (bridge_clear_done), // Ch357 — 0x02C[7] persistent-Z preclear done (0 for non-Z)
.frag_drops_i (bridge_frag_drops), // Ch357 — 0x0EC snapshot + 0x02C[8] sticky drop flag (0 for non-Z)
.lpddr_rd_addr_o (lpddr_rd_addr_w),
.lpddr_rd_pulse_o (lpddr_rd_pulse_w),
.lpddr_rd_data_i (lpddr_rd_data_w),
@@ -2849,7 +3229,16 @@ module de25_nano_psmct32_raster_demo_top (
.feeder_go_tgl_o (feeder_go_tgl_w),
.feeder_ready_i (feeder_ready_w),
.feeder_records_i (feeder_records_w),
.feeder_waits_i (feeder_waits_w)
.feeder_waits_i (feeder_waits_w),
.clut_stage_waddr_o (clut_stage_waddr_w),
.clut_stage_wdata_o (clut_stage_wdata_w),
.clut_stage_we_o (clut_stage_we_w),
.clut_commit_tgl_o (clut_commit_tgl_w),
.clut_expected_crc_o (clut_expected_crc_w),
.clut_busy_i (runtime_clut_busy_w),
.clut_done_tgl_i (runtime_clut_done_tgl_w),
.clut_crc_i (runtime_clut_crc_w)
);
`else
@@ -2886,6 +3275,11 @@ module de25_nano_psmct32_raster_demo_top (
assign feeder_go_tgl_w = 1'b0;
assign feeder_stg_waddr_w = 12'd0;
assign feeder_stg_wdata_w = 64'd0;
assign clut_stage_waddr_w = 8'd0;
assign clut_stage_wdata_w = 32'd0;
assign clut_stage_we_w = 1'b0;
assign clut_commit_tgl_w = 1'b0;
assign clut_expected_crc_w = 32'd0;
`endif // USE_QSYS_TOP
// Suppress unused-input warnings on lint (CLOCK0_50 / CLOCK1_50
+84 -9
View File
@@ -195,12 +195,13 @@ module top_psmct32_raster_demo_bram
// Default 0 generate-guards all perspective logic out (zero cost); only the
// GS_PERSP_DEMO board profile sets it 1.
parameter bit PERSPECTIVE_CORRECT = 1'b0,
parameter bit SUBPIXEL_XY = 1'b0,
parameter int PERSP_RECIP_IDX_BITS = 8, // Ch351 — perspective reciprocal LUT width (far-W -> 11)
parameter int GRAD_DIV_CYCLES = 1, // Ch352 — triangle-setup divide settle cycles (board fits -> 4)
parameter bit GRAD_SEQ_DIVIDER = 1'b0, // Ch352 — sequential gradient divider (board fits -> 1)
// Ch344 — TEXTURED + source-over ALPHA SPRITE path (forwarded to gs_stub). Default 0 -> byte-identical.
parameter bit SPRITE_TEX_ALPHA = 1'b0,
parameter bit SPRITE_TEX_ALPHA_CLUT = 1'b0, // Ch347 — admit PSMT8 (CLUT) textures into the alpha-sprite path
parameter bit SPRITE_TEX_ALPHA_CLUT = 1'b0, // Ch408 — admit PSMT8/PSMT4 CLUT textures into alpha sprites
parameter bit CLUT_CSM1_ENABLE = 1'b0, // Ch350 — CSM1 16x16 CT32 grid CLUT load (SH3 indexed env path)
// Ch302 — COMBINED textured+alpha+depth probe (forwarded to gs_stub). When 1,
// a TME+ABE+ZTE triangle runs the multi-beat per-pixel FSM (Zread->Ztest->
@@ -237,6 +238,11 @@ module top_psmct32_raster_demo_bram
// dead logic, byte-identical for every existing profile.
parameter bit FEEDER_ENABLE = 1'b0,
parameter int FEEDER_STG_WORDS = 256,
// Ch353 — auto-start the FIRST feeder render at C_SETUP (default, byte-identical). Set 0 ONLY for
// the GS_SH3_LPDDR_FB profile: the disarmed boot render would otherwise leave a pending EOF that
// falsely asserts frame_drained the instant the host later arms the writer. With AUTOSTART=0,
// C_SETUP transitions straight to C_READY (no render) and every render waits for an explicit GO.
parameter bit FEEDER_AUTOSTART = 1'b1,
// Ch315 — primitive FIFO / per-tile bin depth (capacity). Power-of-2, default
// 4 = byte-identical. GS_TILE_CAP_DEMO sets 8 to scale capacity past the old 4.
parameter int TILE_FIFO_DEPTH = 4,
@@ -366,7 +372,16 @@ module top_psmct32_raster_demo_bram
output logic [31:0] flush_addr_o, // linear FB byte address
output logic [15:0] flush_pix16_o,
output logic [31:0] flush_color32_o, // Ch323 — full 32-bit flushed color (for spill round-trip capture)
output logic [3:0] flush_be_o, // FRAME.FBMSK-compressed byte enables
output logic [5:0] flush_psm_o,
output logic [11:0] flush_x_o, // Ch357 — fragment x/y/z for the external LPDDR-Z ROP (perspective path)
output logic [11:0] flush_y_o,
output logic [31:0] flush_z_o,
output logic flush_abe_o,
output logic [16:0] flush_alpha_o,
output logic flush_ztest_o,
output logic [1:0] flush_ztst_o,
output logic flush_zmsk_o,
// Ch323 — tile Z-FLUSH stream (TILE_SPILL_ENABLE; the de25 routes it to an LPDDR Z-backing
// writer). z_flush_addr_o is Z-backing-relative (pixel_index*4, 32-bit Z). Inert/0 unless
// spilling; unused (left open) by every existing consumer.
@@ -416,7 +431,15 @@ module top_psmct32_raster_demo_bram
input logic feeder_go_i, // retrigger pulse (honoured only in C_READY)
output logic feeder_ready_o, // control FSM is in C_READY (a new list may start)
output logic [15:0] feeder_records_o, // primitives emitted by the current list
output logic [31:0] feeder_waits_o // cycles the feeder paused under fifo_full
output logic [31:0] feeder_waits_o, // cycles the feeder paused under fifo_full
// Ch367 -- runtime palette residency. The design-clock staging copier
// owns this writer while busy; the legacy TEX0/VRAM loader remains the
// sole writer at every other time.
input logic runtime_clut_wr_en_i,
input logic [7:0] runtime_clut_wr_idx_i,
input logic [31:0] runtime_clut_wr_data_i,
input logic runtime_clut_busy_i
);
localparam int RAM_ADDR_W = $clog2(RAM_SIZE_BYTES);
@@ -805,7 +828,12 @@ module top_psmct32_raster_demo_bram
fdr_start <= 1'b0;
unique case (cst)
C_SETUP: if (dma_done_seen && !xfer_busy) begin // bootlet setup + TRX/IMAGE done
fdr_start <= 1'b1; grid_ran <= 1'b0; cst <= C_RUN; end
if (FEEDER_AUTOSTART) begin
fdr_start <= 1'b1; grid_ran <= 1'b0; cst <= C_RUN; // auto boot render
end else begin
grid_ran <= 1'b0; cst <= C_READY; // Ch353: no boot render — await explicit GO
end
end
C_RUN: begin if (gs_scene_busy) grid_ran <= 1'b1;
if (fdr_done) cst <= C_DRAIN; end
// Ch337 — gate on gs_scene_busy (NOT gs_raster_active): a >FIFO_DEPTH scene
@@ -883,15 +911,39 @@ module top_psmct32_raster_demo_bram
logic raster_pixel_emit;
logic [63:0] raster_pixel_color_q;
logic [31:0] raster_pixel_fb_addr_q;
logic [11:0] raster_pixel_x_q, raster_pixel_y_q;
logic [31:0] raster_pixel_z_q;
logic [3:0] raster_pixel_be_q;
logic [31:0] raster_pixel_mask_q;
logic [63:0] gs_frame_1_q;
logic [5:0] raster_pixel_psm_q;
logic raster_pixel_abe_q;
logic [16:0] raster_pixel_alpha_q;
logic raster_pixel_ztest_q;
logic [1:0] raster_pixel_ztst_q;
logic raster_pixel_zmsk_q;
// Ch318 — expose the flush stream for an external LPDDR AXI writer (de25 top).
assign flush_emit_o = raster_pixel_emit;
assign flush_addr_o = raster_pixel_fb_addr_q;
assign flush_pix16_o = raster_pixel_color_q[15:0];
assign flush_color32_o = raster_pixel_color_q[31:0]; // Ch323 — full color for spill capture
// The production LPDDR ROP supports the byte-granular FBMSK subset used
// by the captured SH3 frame. A set mask byte preserves that destination
// byte; partial-bit mask bytes are rejected by fixture generation.
wire [3:0] frame_byte_be = {gs_frame_1_q[63:56] == 8'h00,
gs_frame_1_q[55:48] == 8'h00,
gs_frame_1_q[47:40] == 8'h00,
gs_frame_1_q[39:32] == 8'h00};
assign flush_be_o = raster_pixel_be_q & frame_byte_be;
assign flush_x_o = raster_pixel_x_q; // Ch357 — fragment coords + Z for the LPDDR-Z ROP
assign flush_y_o = raster_pixel_y_q;
assign flush_z_o = raster_pixel_z_q;
assign flush_abe_o = raster_pixel_abe_q;
assign flush_alpha_o = raster_pixel_alpha_q;
assign flush_ztest_o = raster_pixel_ztest_q;
assign flush_ztst_o = raster_pixel_ztst_q;
assign flush_zmsk_o = raster_pixel_zmsk_q;
assign flush_psm_o = raster_pixel_psm_q;
// Ch295 — texture-sampler read port out of gs_stub. Wired to
@@ -921,6 +973,11 @@ module top_psmct32_raster_demo_bram
// that consumes it as clut_load_busy to hold the FIFO pop while the
// VRAM->CLUT load runs). Driven by clut_loader_stub.load_busy below.
logic clut_ld_busy;
// Existing unit tops leave the new runtime ports open. Treat only a
// driven-one as active so those legacy fixtures stay byte-identical.
wire runtime_clut_busy = (runtime_clut_busy_i === 1'b1);
wire runtime_clut_wr_en = (runtime_clut_wr_en_i === 1'b1);
wire clut_busy = clut_ld_busy | runtime_clut_busy;
// Ch296 — gs_stub TEX0_1 decode taps that feed clut_loader_stub: the
// 1-cycle commit pulse + CBP/CPSM/CSM/CSA/CLD that decide when (and
@@ -960,6 +1017,7 @@ module top_psmct32_raster_demo_bram
.FB_RD_REGISTERED (1'b1),
.Z_RD_REGISTERED (1'b1),
.PERSPECTIVE_CORRECT(PERSPECTIVE_CORRECT), // Ch301 — forwarded board param (default 0)
.SUBPIXEL_XY (SUBPIXEL_XY), // Ch416 — native GS 12.4 coverage (default 0)
.PERSP_RECIP_IDX_BITS(PERSP_RECIP_IDX_BITS), // Ch351 — perspective reciprocal LUT width (far-W -> 11)
.GRAD_DIV_CYCLES(GRAD_DIV_CYCLES), // Ch352 — triangle-setup divide settle cycles (board -> 4)
.GRAD_SEQ_DIVIDER(GRAD_SEQ_DIVIDER), // Ch352 — sequential gradient divider (board -> 1)
@@ -997,7 +1055,7 @@ module top_psmct32_raster_demo_bram
.pmode_q(pmode_q), .dispfb1_q(dispfb1_q), .display1_q(display1_q),
.prim_q(), .rgbaq_q(),
.xyz2_q(), .xyzf2_q(),
.frame_1_q(), .zbuf_1_q(),
.frame_1_q(gs_frame_1_q), .zbuf_1_q(),
.tex0_1_q(), .tex0_1_cbp_q(gs_tex0_cbp), .tex0_1_cpsm_q(gs_tex0_cpsm),
.tex0_1_csm_q(gs_tex0_csm), .tex0_1_csa_q(gs_tex0_csa),
.tex0_1_cld_q(gs_tex0_cld), .tex0_1_wr_q(gs_tex0_wr),
@@ -1018,12 +1076,18 @@ module top_psmct32_raster_demo_bram
.pixel_fbp_q(), .pixel_fbw_q(), .pixel_psm_q(), .pixel_fb_addr_q(),
.raster_pixel_emit(raster_pixel_emit),
.raster_pixel_emit_count(),
.raster_pixel_x_q(), .raster_pixel_y_q(),
.raster_pixel_x_q(raster_pixel_x_q), .raster_pixel_y_q(raster_pixel_y_q),
.raster_pixel_color_q(raster_pixel_color_q),
.raster_pixel_fb_addr_q(raster_pixel_fb_addr_q),
.raster_pixel_z_q(raster_pixel_z_q),
.raster_pixel_be_q(raster_pixel_be_q),
.raster_pixel_mask_q(raster_pixel_mask_q),
.raster_pixel_psm_q(raster_pixel_psm_q),
.raster_pixel_abe_q(raster_pixel_abe_q),
.raster_pixel_alpha_q(raster_pixel_alpha_q),
.raster_pixel_ztest_q(raster_pixel_ztest_q),
.raster_pixel_ztst_q(raster_pixel_ztst_q),
.raster_pixel_zmsk_q(raster_pixel_zmsk_q),
.z_flush_emit_o(z_flush_emit_o), // Ch323 — tile Z-flush stream (de25 Z-writer)
.z_flush_addr_o(z_flush_addr_o),
.z_flush_data_o(z_flush_data_o),
@@ -1047,7 +1111,7 @@ module top_psmct32_raster_demo_bram
.tex_rd_data(gs_tex_rd_data),
.clut_rd_idx (gs_clut_rd_idx),
.clut_rd_data(gs_clut_rd_data),
.clut_load_busy(clut_ld_busy), // hold pop while VRAM->CLUT load runs
.clut_load_busy(clut_busy), // hold pop while either CLUT writer runs
.fb_rd_en (gs_fb_rd_en),
.fb_rd_addr(gs_fb_rd_addr),
.fb_rd_data(gs_fb_rd_data),
@@ -1422,11 +1486,22 @@ module top_psmct32_raster_demo_bram
end
end
wire clut_wr_en = runtime_clut_wr_en ? 1'b1 : clut_ld_wr_en_q;
wire [7:0] clut_wr_idx = runtime_clut_wr_en ? runtime_clut_wr_idx_i : clut_ld_wr_idx_q;
wire [31:0] clut_wr_data = runtime_clut_wr_en ? runtime_clut_wr_data_i : clut_ld_wr_data_c;
// synthesis translate_off
always_ff @(posedge clk) begin
if (rst_n && runtime_clut_wr_en && clut_ld_wr_en_q)
$error("runtime CLUT copy collided with TEX0 CLUT loader");
end
// synthesis translate_on
clut_stub u_clut (
.clk(clk), .rst_n(rst_n),
.write_en (clut_ld_wr_en_q),
.write_idx(clut_ld_wr_idx_q),
.write_data(clut_ld_wr_data_c), // LIVE registered-read data
.write_en (clut_wr_en),
.write_idx(clut_wr_idx),
.write_data(clut_wr_data),
// pcrtc scanout read port — unused in this textured-board top
// (PCRTC clut_enable=0). Tie its index to 0.
.read_idx (8'd0),