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
+119
View File
@@ -0,0 +1,119 @@
// retroDE_ps2 — tb_gs_async_fifo (Ch357, Codex)
//
// Scoreboard for gs_async_fifo after the REGISTERED-empty change (rempty <= rempty_nxt, the read-side twin of the
// registered wfull). Two ASYNCHRONOUS clocks. The writer pushes a strictly increasing sequence; the reader pops and
// asserts each rdata equals the next expected value -> catches ANY duplicate (same value twice) or drop (skipped value)
// and guarantees in-order delivery. Covers: continuous reads, final-entry empty assertion, asynchronous write arrival,
// wrap/full backpressure, and randomized read/write gaps under async clocks.
`timescale 1ns/1ps
module tb_gs_async_fifo #(
parameter bit TEST_BANKED = 1'b0,
parameter bit TEST_QUADRANT = 1'b0,
parameter bit TEST_REGISTERED = TEST_BANKED || TEST_QUADRANT
);
localparam int WIDTH = 32;
localparam int DEPTH = 8;
// Production request-FIFO corner: 40 MHz raster producer into the
// ~310 MHz EMIF consumer. A slower-reader test cannot detect publishing
// the write pointer before a staged RAM commit.
logic wclk=0; always #12.5 wclk=~wclk; // 40 MHz
logic rclk=0; always #1.6 rclk=~rclk; // 312.5 MHz, async to wclk
logic wrst_n, rrst_n;
logic wr, wfull;
logic [WIDTH-1:0] wdata;
logic rd, dut_rd, rempty;
logic [WIDTH-1:0] rdata;
logic registered_pending;
gs_async_fifo #(.WIDTH(WIDTH), .DEPTH(DEPTH),
.REGISTERED_READ(TEST_REGISTERED), .BANKED_READ(TEST_BANKED),
.QUADRANT_READ(TEST_QUADRANT)) dut (
.wclk(wclk), .wrst_n(wrst_n), .wr(wr), .wdata(wdata), .wfull(wfull),
.rclk(rclk), .rrst_n(rrst_n), .rd(dut_rd), .rdata(rdata), .rempty(rempty)
);
// independent LFSR backpressure on each clock
logic [15:0] wl=16'hBEEF; always_ff @(posedge wclk) wl<={wl[14:0], wl[15]^wl[13]^wl[12]^wl[10]};
logic [15:0] rl=16'h1234; always_ff @(posedge rclk) rl<={rl[14:0], rl[15]^rl[13]^rl[12]^rl[10]};
logic want_write, force_read_all, stop_write;
assign want_write = wl[0] | wl[3]; // ~75% offered writes
assign wr = want_write && !stop_write; // FIFO gates internally with !wfull; stop_write freezes the producer
assign rd = force_read_all ? 1'b1 : (rl[1] | rl[4]); // continuous-read phase forces rd=1
// gs_async_fifo's rd input is an accepted-read handshake. Keep the
// randomized read request separate so the test explicitly enforces that
// interface contract, exactly as every production wrapper does.
assign dut_rd = rd && !rempty && (!TEST_REGISTERED || !registered_pending);
always_ff @(posedge rclk or negedge rrst_n) begin
if (!rrst_n) registered_pending <= 1'b0;
else registered_pending <= TEST_REGISTERED && dut_rd;
end
logic [WIDTH-1:0] wr_seq, rd_seq; // next value to write / next value expected to read
assign wdata = wr_seq;
int errors; initial errors=0;
int sb_err; // scoreboard-only error counter (reset + written solely by the reader always_ff)
// writer: count accepted writes, advance the sequence
always_ff @(posedge wclk or negedge wrst_n) begin
if (!wrst_n) wr_seq <= '0;
else if (wr && !wfull) wr_seq <= wr_seq + 1;
end
// reader scoreboard: every accepted read must equal the next expected sequence value (in order, no dup/drop)
always_ff @(posedge rclk or negedge rrst_n) begin
if (!rrst_n) begin rd_seq <= '0; sb_err <= 0; end
else if (TEST_REGISTERED ? registered_pending : dut_rd) begin
if (rdata !== rd_seq) begin
if (sb_err < 20) $error("[afifo] out-of-order/dup/drop: got %0d expected %0d", rdata, rd_seq);
sb_err <= sb_err + 1;
end
rd_seq <= rd_seq + 1;
end
end
task automatic run_cycles(input int n_r); repeat (n_r) @(posedge rclk); endtask
initial begin
wrst_n=0; rrst_n=0; force_read_all=0; stop_write=0;
repeat (6) @(posedge wclk); wrst_n=1;
repeat (6) @(posedge rclk); rrst_n=1;
// reset check: FIFO must come up EMPTY
@(posedge rclk);
if (rempty !== 1'b1) begin $error("[afifo] rempty not asserted after reset"); errors++; end
// ---- Phase 1: randomized async read/write (wrap/full exercised many times) ----
run_cycles(30000);
// ---- Phase 2: continuous reads -> drain fully; assert final-entry empty ----
force_read_all = 1'b1;
run_cycles(4000);
force_read_all = 1'b0;
// ---- Phase 3: FREEZE the writer, drain fully, assert EMPTY + counts equal (final-entry empty assertion) ----
stop_write = 1'b1;
force_read_all = 1'b1;
begin int g; g=0; while ((wr_seq !== rd_seq) && g<40000) begin @(posedge rclk); g++; end end
run_cycles(20);
// ---- checks ----
if (rempty !== 1'b1) begin $error("[afifo] FIFO not EMPTY after full drain (rempty=%0b)", rempty); errors++; end
if (wr_seq !== rd_seq) begin $error("[afifo] count mismatch: wrote %0d read %0d (drop/dup)", wr_seq, rd_seq); errors++; end
if (wr_seq < 32'd1000) begin $error("[afifo] too few transfers (%0d) — test not meaningful", wr_seq); errors++; end
errors = errors + sb_err;
$display("[tb_gs_async_fifo] wrote=%0d read=%0d sb_err=%0d rempty=%0b errors=%0d",
wr_seq, rd_seq, sb_err, rempty, errors);
if (errors==0) $display("[tb_gs_async_fifo] PASS");
else $display("[tb_gs_async_fifo] FAIL");
$finish;
end
initial begin #4000000; $error("[tb_gs_async_fifo] TIMEOUT"); $finish; end
endmodule : tb_gs_async_fifo
+164
View File
@@ -0,0 +1,164 @@
// retroDE_ps2 — tb_gs_feeder_fog_e2e
//
// END-TO-END GS per-vertex FOG through the runtime feeder: a staging list with
// PRIM.FGE=1 drives gs_prim_list_feeder, whose gif_reg_* stream feeds gs_stub.
// Proves the full data path — the feeder tags the vertex commit XYZF2 (reg 0x04)
// carrying the per-vertex fog byte, and gs_stub applies the fog blend at emit.
//
// One untextured flat Gouraud triangle, flat per-vertex F, FOGCOL left at the
// RTL default 0 (black) — the SH3 scene's value. So the emitted RGB must equal
// (C * F) >> 8 per channel (fog toward black), alpha unchanged, at interior px.
`timescale 1ns/1ps
module tb_gs_feeder_fog_e2e;
localparam int STG_ADDR_W = 12;
logic clk; logic rst_n;
initial clk = 1'b0; always #5 clk = ~clk;
// feeder <-> staging + gs_stub
logic start, busy, done;
logic [15:0] records_emitted;
logic [31:0] fifo_wait_cycles;
logic [STG_ADDR_W-1:0] stg_rd_addr;
logic [63:0] stg_rd_data;
logic gif_reg_wr_en;
logic [7:0] gif_reg_num;
logic [63:0] gif_reg_data;
// staging RAM
logic [63:0] stg [0:127];
always_ff @(posedge clk) stg_rd_data <= stg[stg_rd_addr[6:0]];
// gs_stub outputs we watch
logic [7:0] bg_r, bg_g, bg_b;
logic [63:0] prim_q, rgbaq_q, xyz2_q, xyzf2_q, frame_1_q, zbuf_1_q;
logic prim_complete; logic [31:0] prim_complete_count;
logic [63:0] prim_v0_q, prim_v1_q, prim_v2_q, prim_color_q;
logic [63:0] prim_color_v0_q, prim_color_v1_q, prim_color_v2_q;
trace_pkg::vertex_t prim_v0_decoded_q, prim_v1_decoded_q, prim_v2_decoded_q;
trace_pkg::color_t prim_v0_color_decoded_q, prim_v1_color_decoded_q, prim_v2_color_decoded_q;
logic pixel_emit; logic [31:0] pixel_emit_count;
logic [11:0] pixel_x_q, pixel_y_q; logic [63:0] pixel_color_q;
logic [8:0] pixel_fbp_q; logic [5:0] pixel_fbw_q, pixel_psm_q; logic [31:0] pixel_fb_addr_q;
logic raster_pixel_emit; logic [31:0] raster_pixel_emit_count;
logic [11:0] raster_pixel_x_q, raster_pixel_y_q; logic [63:0] raster_pixel_color_q;
logic [31:0] raster_pixel_fb_addr_q; logic [3:0] raster_pixel_be_q;
logic [31:0] raster_pixel_mask_q; logic [5:0] raster_pixel_psm_q;
logic raster_active, raster_overflow, raster_fifo_full, raster_degenerate;
logic tex_rd_en; logic [31:0] tex_rd_addr;
logic fb_rd_en; logic [31:0] fb_rd_addr;
logic z_rd_en; logic [31:0] z_rd_addr;
logic ev_valid; trace_pkg::subsys_e ev_subsys; trace_pkg::event_e ev_event;
logic [63:0] ev_arg0, ev_arg1, ev_arg2, ev_arg3; logic [31:0] ev_flags;
gs_prim_list_feeder #(.STG_ADDR_W(STG_ADDR_W)) u_feeder (
.clk(clk), .rst_n(rst_n), .start(start), .busy(busy), .done(done),
.records_emitted(records_emitted), .fifo_wait_cycles(fifo_wait_cycles),
.stg_rd_addr(stg_rd_addr), .stg_rd_data(stg_rd_data),
.fifo_full(raster_fifo_full),
.gif_reg_wr_en(gif_reg_wr_en), .gif_reg_num(gif_reg_num), .gif_reg_data(gif_reg_data));
gs_stub u_gs (
.clk(clk), .rst_n(rst_n),
.reg_wr_en(1'b0), .reg_wr_addr(16'd0), .reg_wr_data(64'd0),
.gif_reg_wr_en(gif_reg_wr_en), .gif_reg_num(gif_reg_num), .gif_reg_data(gif_reg_data),
.bg_r(bg_r), .bg_g(bg_g), .bg_b(bg_b),
.prim_q(prim_q), .rgbaq_q(rgbaq_q), .xyz2_q(xyz2_q), .xyzf2_q(xyzf2_q),
.frame_1_q(frame_1_q), .zbuf_1_q(zbuf_1_q),
.prim_complete(prim_complete), .prim_complete_count(prim_complete_count),
.prim_v0_q(prim_v0_q), .prim_v1_q(prim_v1_q), .prim_v2_q(prim_v2_q),
.prim_color_q(prim_color_q),
.prim_color_v0_q(prim_color_v0_q), .prim_color_v1_q(prim_color_v1_q), .prim_color_v2_q(prim_color_v2_q),
.prim_v0_decoded_q(prim_v0_decoded_q), .prim_v1_decoded_q(prim_v1_decoded_q), .prim_v2_decoded_q(prim_v2_decoded_q),
.prim_v0_color_decoded_q(prim_v0_color_decoded_q), .prim_v1_color_decoded_q(prim_v1_color_decoded_q), .prim_v2_color_decoded_q(prim_v2_color_decoded_q),
.pixel_emit(pixel_emit), .pixel_emit_count(pixel_emit_count),
.pixel_x_q(pixel_x_q), .pixel_y_q(pixel_y_q), .pixel_color_q(pixel_color_q),
.pixel_fbp_q(pixel_fbp_q), .pixel_fbw_q(pixel_fbw_q), .pixel_psm_q(pixel_psm_q), .pixel_fb_addr_q(pixel_fb_addr_q),
.raster_pixel_emit(raster_pixel_emit), .raster_pixel_emit_count(raster_pixel_emit_count),
.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_be_q(raster_pixel_be_q),
.raster_pixel_mask_q(raster_pixel_mask_q), .raster_pixel_psm_q(raster_pixel_psm_q),
.raster_active(raster_active), .raster_overflow(raster_overflow),
.raster_fifo_full(raster_fifo_full), .raster_degenerate(raster_degenerate),
.tex_rd_en(tex_rd_en), .tex_rd_addr(tex_rd_addr), .tex_rd_data(32'd0),
.fb_rd_en(fb_rd_en), .fb_rd_addr(fb_rd_addr), .fb_rd_data(32'd0),
.z_rd_en(z_rd_en), .z_rd_addr(z_rd_addr), .z_rd_data(32'd0),
.ev_valid(ev_valid), .ev_subsys(ev_subsys), .ev_event(ev_event),
.ev_arg0(ev_arg0), .ev_arg1(ev_arg1), .ev_arg2(ev_arg2), .ev_arg3(ev_arg3), .ev_flags(ev_flags));
// flat triangle color + fog
localparam int CR=8'hC0, CG=8'h80, CB=8'h40, CA=8'hFF, FF=8'h80;
function automatic logic [63:0] xyzf2(input int x, input int y, input int f);
return {8'(f), 24'd0, 12'(y), 4'd0, 12'(x), 4'd0};
endfunction
// capture emitted pixels
bit covered [0:15][0:15];
logic [31:0] cap_c [0:15][0:15];
bit cap_armed;
always_ff @(posedge clk) begin
if (rst_n && cap_armed && raster_pixel_emit
&& raster_pixel_x_q < 16 && raster_pixel_y_q < 16) begin
covered[raster_pixel_y_q][raster_pixel_x_q] <= 1'b1;
cap_c [raster_pixel_y_q][raster_pixel_x_q] <= raster_pixel_color_q[31:0];
end
end
int errors, checks;
function automatic int fogblack(input int c, input int f); return (c*f) >> 8; endfunction
task automatic chk(input int x, input int y);
int er, eg, eb;
er=fogblack(CR,FF); eg=fogblack(CG,FF); eb=fogblack(CB,FF);
if (!covered[y][x]) begin $error("[e2e] (%0d,%0d) not covered", x, y); errors++; end
else if (cap_c[y][x][7:0]!==er[7:0] || cap_c[y][x][15:8]!==eg[7:0] ||
cap_c[y][x][23:16]!==eb[7:0] || cap_c[y][x][31:24]!==CA[7:0]) begin
$error("[e2e] (%0d,%0d) got %08x expected fog-black (R=%0d G=%0d B=%0d A=%0d)",
x, y, cap_c[y][x], er, eg, eb, CA); errors++;
end else begin
$display("[e2e] (%0d,%0d) got (%0d,%0d,%0d,a=%0d) fog-black EXACT OK",
x, y, cap_c[y][x][7:0], cap_c[y][x][15:8], cap_c[y][x][23:16], cap_c[y][x][31:24]);
checks++;
end
endtask
initial begin
errors=0; checks=0; cap_armed=0; start=0;
for (int y=0;y<16;y++) for (int x=0;x<16;x++) begin covered[y][x]=0; cap_c[y][x]=0; end
for (int i=0;i<128;i++) stg[i]=64'd0;
// staging: 1 tri, shared state, PRIM+FGE, 3 vertices (RGBAQ/UV/XYZF2)
stg[0]=64'd1;
stg[1]=64'h0000_0000_0001_0000; // FRAME FBW=1 PSMCT32
stg[2]=64'd0; stg[3]=64'd0; stg[4]=64'd0; stg[5]=64'd0; // ALPHA/TEST/ZBUF/TEX0
stg[6]=64'd3 | (64'h1<<5); // PRIM: TRI + FGE (TME=0)
// v0
stg[7]={32'd0, 8'(CA), 8'(CB), 8'(CG), 8'(CR)}; stg[8]=64'd0; stg[9]=xyzf2(2,2,FF);
// v1
stg[10]={32'd0, 8'(CA), 8'(CB), 8'(CG), 8'(CR)}; stg[11]=64'd0; stg[12]=xyzf2(12,3,FF);
// v2 (closes)
stg[13]={32'd0, 8'(CA), 8'(CB), 8'(CG), 8'(CR)}; stg[14]=64'd0; stg[15]=xyzf2(4,9,FF);
rst_n=0; repeat(4) @(posedge clk); rst_n=1; repeat(2) @(posedge clk);
cap_armed=1;
@(negedge clk); start=1; @(negedge clk); start=0;
wait (done==1'b1);
repeat (400) @(posedge clk);
cap_armed=0; @(posedge clk);
if (raster_pixel_emit_count == 0) begin
$error("[e2e] feeder->gs_stub rendered 0 pixels"); errors++;
end
// interior pixels of the triangle
chk(5,4); chk(4,5); chk(6,4);
if (checks == 0) begin $error("[e2e] no interior pixels verified"); errors++; end
$display("[tb_gs_feeder_fog_e2e] emit_count=%0d checks=%0d errors=%0d",
raster_pixel_emit_count, checks, errors);
if (errors==0) $display("[tb_gs_feeder_fog_e2e] PASS");
else $display("[tb_gs_feeder_fog_e2e] FAIL");
$finish;
end
initial begin #3000000; $error("[tb_gs_feeder_fog_e2e] timeout"); $finish; end
endmodule : tb_gs_feeder_fog_e2e
+445
View File
@@ -0,0 +1,445 @@
// retroDE_ps2 — tb_gs_fog
//
// Focused white-box TB for GS per-vertex FOG in the reduced rasterizer.
//
// PS2 GS fog: final_color_channel = (C * F + FOGCOL * (255 - F)) >> 8, per
// RGB channel, where F is the 8-bit per-vertex fog coefficient (XYZF2 bits
// [63:56]) interpolated affinely across the primitive (flat for a SPRITE),
// gated on PRIM.FGE (bit 5). FOGCOL is GIF reg 0x3D (low 24 bits = 0xBBGGRR).
// Alpha is NOT fogged. When FGE=0 the emit is byte-identical to the no-fog
// path — the hard invariant.
//
// Tests:
// T1 — FGE=1 triangle, FLAT color, FLAT F. Assert the emitted RGB equals the
// EXACT hand-computed fog blend at several interior pixels, and the
// alpha is preserved unchanged.
// T2 — FGE=1 triangle, FLAT color, DISTINCT per-vertex F. Probe the white-box
// s2_fog_f at interior S2 pixels and assert it matches the barycentric
// (ground-truth) interpolated F within 1 LSB — proving F rides the
// shared affine gradient engine like the colour/Z attributes.
// T3 — FGE=0 triangle, SAME geometry/color as T1 with FOGCOL still set.
// Assert the emitted color is BYTE-IDENTICAL to the raw vertex color
// (no fog applied) — the non-negotiable invariant.
// T4 — FGE=1 SPRITE, flat color, flat F. Assert the exact fog blend on the
// flat-sprite emit path (s2_sprite_color64 chokepoint).
`timescale 1ns/1ps
module tb_gs_fog;
logic clk;
logic rst_n;
initial clk = 1'b0;
always #5 clk = ~clk;
logic gif_reg_wr_en;
logic [7:0] gif_reg_num;
logic [63:0] gif_reg_data;
logic [7:0] bg_r, bg_g, bg_b;
logic [63:0] prim_q, rgbaq_q, xyz2_q, xyzf2_q, frame_1_q, zbuf_1_q;
logic prim_complete;
logic [31:0] prim_complete_count;
logic [63:0] prim_v0_q, prim_v1_q, prim_v2_q;
logic [63:0] prim_color_q;
logic [63:0] prim_color_v0_q, prim_color_v1_q, prim_color_v2_q;
trace_pkg::vertex_t prim_v0_decoded_q, prim_v1_decoded_q, prim_v2_decoded_q;
trace_pkg::color_t prim_v0_color_decoded_q, prim_v1_color_decoded_q, prim_v2_color_decoded_q;
logic pixel_emit;
logic [31:0] pixel_emit_count;
logic [11:0] pixel_x_q, pixel_y_q;
logic [63:0] pixel_color_q;
logic [8:0] pixel_fbp_q;
logic [5:0] pixel_fbw_q, pixel_psm_q;
logic [31:0] pixel_fb_addr_q;
logic raster_pixel_emit;
logic [31:0] raster_pixel_emit_count;
logic [11:0] raster_pixel_x_q, raster_pixel_y_q;
logic [63:0] raster_pixel_color_q;
logic [31:0] raster_pixel_fb_addr_q;
logic [3:0] raster_pixel_be_q;
logic [31:0] raster_pixel_mask_q;
logic [5:0] raster_pixel_psm_q;
logic raster_active;
logic raster_overflow;
logic raster_fifo_full;
logic raster_degenerate;
logic tex_rd_en; logic [31:0] tex_rd_addr;
logic fb_rd_en; logic [31:0] fb_rd_addr;
logic z_rd_en; logic [31:0] z_rd_addr;
logic ev_valid;
trace_pkg::subsys_e ev_subsys;
trace_pkg::event_e ev_event;
logic [63:0] ev_arg0, ev_arg1, ev_arg2, ev_arg3;
logic [31:0] ev_flags;
gs_stub u_gs (
.clk(clk), .rst_n(rst_n),
.reg_wr_en(1'b0), .reg_wr_addr(16'd0), .reg_wr_data(64'd0),
.gif_reg_wr_en(gif_reg_wr_en),
.gif_reg_num(gif_reg_num),
.gif_reg_data(gif_reg_data),
.bg_r(bg_r), .bg_g(bg_g), .bg_b(bg_b),
.prim_q(prim_q), .rgbaq_q(rgbaq_q),
.xyz2_q(xyz2_q), .xyzf2_q(xyzf2_q),
.frame_1_q(frame_1_q), .zbuf_1_q(zbuf_1_q),
.prim_complete(prim_complete),
.prim_complete_count(prim_complete_count),
.prim_v0_q(prim_v0_q), .prim_v1_q(prim_v1_q), .prim_v2_q(prim_v2_q),
.prim_color_q(prim_color_q),
.prim_color_v0_q(prim_color_v0_q),
.prim_color_v1_q(prim_color_v1_q),
.prim_color_v2_q(prim_color_v2_q),
.prim_v0_decoded_q(prim_v0_decoded_q),
.prim_v1_decoded_q(prim_v1_decoded_q),
.prim_v2_decoded_q(prim_v2_decoded_q),
.prim_v0_color_decoded_q(prim_v0_color_decoded_q),
.prim_v1_color_decoded_q(prim_v1_color_decoded_q),
.prim_v2_color_decoded_q(prim_v2_color_decoded_q),
.pixel_emit(pixel_emit),
.pixel_emit_count(pixel_emit_count),
.pixel_x_q(pixel_x_q), .pixel_y_q(pixel_y_q),
.pixel_color_q(pixel_color_q),
.pixel_fbp_q(pixel_fbp_q),
.pixel_fbw_q(pixel_fbw_q),
.pixel_psm_q(pixel_psm_q),
.pixel_fb_addr_q(pixel_fb_addr_q),
.raster_pixel_emit(raster_pixel_emit),
.raster_pixel_emit_count(raster_pixel_emit_count),
.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_be_q(raster_pixel_be_q),
.raster_pixel_mask_q(raster_pixel_mask_q),
.raster_pixel_psm_q(raster_pixel_psm_q),
.raster_active(raster_active),
.raster_overflow(raster_overflow),
.raster_fifo_full(raster_fifo_full),
.raster_degenerate(raster_degenerate),
.tex_rd_en(tex_rd_en), .tex_rd_addr(tex_rd_addr), .tex_rd_data(32'd0),
.fb_rd_en(fb_rd_en), .fb_rd_addr(fb_rd_addr), .fb_rd_data(32'd0),
.z_rd_en(z_rd_en), .z_rd_addr(z_rd_addr), .z_rd_data(32'd0),
.ev_valid(ev_valid), .ev_subsys(ev_subsys), .ev_event(ev_event),
.ev_arg0(ev_arg0), .ev_arg1(ev_arg1),
.ev_arg2(ev_arg2), .ev_arg3(ev_arg3),
.ev_flags(ev_flags)
);
// ----- Drive helpers -----
task automatic drive_reg(input logic [7:0] num, input logic [63:0] data);
@(negedge clk);
gif_reg_wr_en = 1'b1; gif_reg_num = num; gif_reg_data = data;
@(posedge clk);
endtask
task automatic drive_idle();
@(negedge clk);
gif_reg_wr_en = 1'b0; gif_reg_num = 8'd0; gif_reg_data = 64'd0;
@(posedge clk);
endtask
// XYZF2 (reg 0x04): X=[15:0] (12.4), Y=[31:16] (12.4), Z=[55:32] (24b),
// F=[63:56] (8b fog). Screen coords carry no fractional bits here.
function automatic logic [63:0] xyzf2(input int x, input int y,
input int z, input int f);
return {8'(f), 24'(z), 12'(y), 4'd0, 12'(x), 4'd0};
endfunction
function automatic logic [63:0] rgbaq(input int r, input int g, input int b, input int a);
return {32'd0, 8'(a), 8'(b), 8'(g), 8'(r)};
endfunction
localparam logic [7:0] R_PRIM = 8'h00;
localparam logic [7:0] R_RGBAQ = 8'h01;
localparam logic [7:0] R_XYZF2 = 8'h04;
localparam logic [7:0] R_FOGCOL = 8'h3D;
localparam logic [7:0] R_FRAME_1 = 8'h4C;
localparam logic [63:0] PRIM_TRI = 64'd3; // TRI
localparam logic [63:0] PRIM_TRI_FGE = 64'd3 | (64'd1 << 5); // TRI + FGE
localparam logic [63:0] PRIM_SPRITE_FGE = 64'd6 | (64'd1 << 5); // SPRITE + FGE
localparam logic [63:0] FRAME_1_VAL = 64'h0000_0000_0001_0000; // FBW=1, PSMCT32
// FOGCOL = 0xBBGGRR
localparam int FOG_R = 8'h20;
localparam int FOG_G = 8'h40;
localparam int FOG_B = 8'h60;
localparam logic [63:0] FOGCOL_VAL = {40'd0, 8'(FOG_B), 8'(FOG_G), 8'(FOG_R)};
// Reference fog blend (matches RTL fog_blend_abgr exactly).
function automatic int fog_ch(input int c, input int f, input int fc);
return (c * f + fc * (255 - f)) >> 8;
endfunction
function automatic int absdiff(input int a, input int b);
return (a > b) ? (a - b) : (b - a);
endfunction
// ----- Independent triangle reference (edge fn, fill rule, barycentric) -----
int vx0, vy0, vx1, vy1, vx2, vy2;
int vf0, vf1, vf2;
int ref_det;
int sx1, sy1, sx2, sy2, sf1, sf2;
function automatic int edge_f(input int px, input int py,
input int ax, input int ay,
input int bx, input int by);
return (px - ax) * (by - ay) - (py - ay) * (bx - ax);
endfunction
task automatic setup_ref();
int sa;
sa = (vx1 - vx0) * (vy2 - vy0) - (vy1 - vy0) * (vx2 - vx0);
if (sa < 0) begin
sx1 = vx2; sy1 = vy2; sf1 = vf2;
sx2 = vx1; sy2 = vy1; sf2 = vf1;
ref_det = -sa;
end else begin
sx1 = vx1; sy1 = vy1; sf1 = vf1;
sx2 = vx2; sy2 = vy2; sf2 = vf2;
ref_det = sa;
end
endtask
function automatic bit tol(input int ax, input int ay, input int bx, input int by);
int dx, dy;
dx = bx - ax; dy = by - ay;
return (dy > 0) || ((dy == 0) && (dx > 0));
endfunction
function automatic bit ref_inside(input int px, input int py);
int e0, e1, e2, b0, b1, b2;
e0 = edge_f(px, py, vx0, vy0, sx1, sy1);
e1 = edge_f(px, py, sx1, sy1, sx2, sy2);
e2 = edge_f(px, py, sx2, sy2, vx0, vy0);
b0 = tol(vx0, vy0, sx1, sy1) ? 0 : 1;
b1 = tol(sx1, sy1, sx2, sy2) ? 0 : 1;
b2 = tol(sx2, sy2, vx0, vy0) ? 0 : 1;
return ((e0 + b0) <= 0) && ((e1 + b1) <= 0) && ((e2 + b2) <= 0);
endfunction
function automatic int ref_attr(input int px, input int py,
input int a0, input int a1, input int a2);
int L0, L1, L2, num;
L0 = -edge_f(px, py, sx1, sy1, sx2, sy2);
L1 = -edge_f(px, py, sx2, sy2, vx0, vy0);
L2 = -edge_f(px, py, vx0, vy0, sx1, sy1);
num = L0 * a0 + L1 * a1 + L2 * a2;
if (ref_det == 0) return 0;
return num / ref_det;
endfunction
int errors;
// ----- Emit capture (full 32-bit ABGR) -----
bit covered [0:15][0:15];
logic [31:0] cap_c [0:15][0:15];
bit cap_armed;
task automatic clear_cov();
for (int y = 0; y < 16; y++)
for (int x = 0; x < 16; x++) begin
covered[y][x] = 1'b0; cap_c[y][x] = 32'd0;
end
endtask
always_ff @(posedge clk) begin
if (rst_n && cap_armed && raster_pixel_emit
&& raster_pixel_x_q < 16 && raster_pixel_y_q < 16) begin
covered[raster_pixel_y_q][raster_pixel_x_q] <= 1'b1;
cap_c [raster_pixel_y_q][raster_pixel_x_q] <= raster_pixel_color_q[31:0];
end
end
// ----- T2 F-interpolation probe: white-box s2_fog_f at interior pixels -----
int f_probe_checks;
bit f_probe_armed;
always_ff @(posedge clk) begin
if (rst_n && f_probe_armed
&& u_gs.s2_valid_q && u_gs.s2_inside_q && u_gs.ras_tri_active) begin
int px, py, ef;
px = int'(u_gs.s2_x_q);
py = int'(u_gs.s2_y_q);
if (px < 16 && py < 16 && ref_inside(px, py)) begin
ef = ref_attr(px, py, vf0, sf1, sf2);
if (absdiff(int'(u_gs.s2_fog_f), ef) > 1) begin
$error("[T2 F] (%0d,%0d) s2_fog_f=%0d expected ~%0d",
px, py, u_gs.s2_fog_f, ef);
errors = errors + 1;
end else begin
f_probe_checks = f_probe_checks + 1;
end
end
end
end
// Exact fog-blend spot check on a captured emitted pixel.
task automatic chk_fog(input int x, input int y,
input int cr, input int cg, input int cb, input int ca,
input int f);
int er, eg, eb;
er = fog_ch(cr, f, FOG_R);
eg = fog_ch(cg, f, FOG_G);
eb = fog_ch(cb, f, FOG_B);
if (!covered[y][x]) begin
$error("[fog] (%0d,%0d) expected covered but was not", x, y);
errors = errors + 1;
end else if (cap_c[y][x][7:0] !== er[7:0] ||
cap_c[y][x][15:8] !== eg[7:0] ||
cap_c[y][x][23:16] !== eb[7:0]) begin
$error("[fog] (%0d,%0d) got (%0d,%0d,%0d) expected EXACT (%0d,%0d,%0d)",
x, y, cap_c[y][x][7:0], cap_c[y][x][15:8], cap_c[y][x][23:16], er, eg, eb);
errors = errors + 1;
end else if (cap_c[y][x][31:24] !== ca[7:0]) begin
$error("[fog] (%0d,%0d) alpha fogged: got %0d expected %0d (unchanged)",
x, y, cap_c[y][x][31:24], ca);
errors = errors + 1;
end else begin
$display("[fog] (%0d,%0d) got (%0d,%0d,%0d,a=%0d) EXACT OK",
x, y, cap_c[y][x][7:0], cap_c[y][x][15:8], cap_c[y][x][23:16],
cap_c[y][x][31:24]);
end
endtask
// Exact no-fog (FGE=0 invariant): emitted RGB == raw color, alpha == raw.
task automatic chk_nofog(input int x, input int y,
input int cr, input int cg, input int cb, input int ca);
if (!covered[y][x]) begin
$error("[nofog] (%0d,%0d) expected covered but was not", x, y);
errors = errors + 1;
end else if (cap_c[y][x][7:0] !== cr[7:0] ||
cap_c[y][x][15:8] !== cg[7:0] ||
cap_c[y][x][23:16] !== cb[7:0] ||
cap_c[y][x][31:24] !== ca[7:0]) begin
$error("[nofog] (%0d,%0d) FGE=0 NOT byte-identical: got %08x expected raw (%0d,%0d,%0d,a=%0d)",
x, y, cap_c[y][x], cr, cg, cb, ca);
errors = errors + 1;
end else begin
$display("[nofog] (%0d,%0d) FGE=0 byte-identical raw color OK", x, y);
end
endtask
// Flat triangle constants.
localparam int TC_R = 8'hC0, TC_G = 8'h80, TC_B = 8'h30, TC_A = 8'hFF;
initial begin
errors = 0; f_probe_checks = 0; cap_armed = 1'b0; f_probe_armed = 1'b0;
rst_n = 1'b0; gif_reg_wr_en = 1'b0; gif_reg_num = 8'd0; gif_reg_data = 64'd0;
clear_cov();
repeat (4) @(posedge clk);
rst_n = 1'b1;
repeat (2) @(posedge clk);
// ================================================================
// T1 — FGE=1 triangle, FLAT color, FLAT F. Exact fog blend.
// ================================================================
vx0=2; vy0=1; vf0=8'h40;
vx1=13;vy1=2; vf1=8'h40;
vx2=5; vy2=7; vf2=8'h40;
setup_ref();
clear_cov(); cap_armed = 1'b1;
drive_reg(R_FOGCOL, FOGCOL_VAL);
drive_reg(R_PRIM, PRIM_TRI_FGE);
drive_reg(R_FRAME_1, FRAME_1_VAL);
drive_reg(R_RGBAQ, rgbaq(TC_R,TC_G,TC_B,TC_A));
drive_reg(R_XYZF2, xyzf2(vx0,vy0,0,vf0));
drive_reg(R_RGBAQ, rgbaq(TC_R,TC_G,TC_B,TC_A));
drive_reg(R_XYZF2, xyzf2(vx1,vy1,0,vf1));
drive_reg(R_RGBAQ, rgbaq(TC_R,TC_G,TC_B,TC_A));
drive_reg(R_XYZF2, xyzf2(vx2,vy2,0,vf2)); // closes
drive_idle();
repeat (300) @(posedge clk);
cap_armed = 1'b0;
@(posedge clk);
chk_fog(7, 3, TC_R, TC_G, TC_B, TC_A, 8'h40);
chk_fog(5, 4, TC_R, TC_G, TC_B, TC_A, 8'h40);
chk_fog(6, 2, TC_R, TC_G, TC_B, TC_A, 8'h40);
if (raster_overflow || raster_degenerate) begin
$error("[T1] raster anomaly"); errors = errors + 1;
end
// ================================================================
// T2 — FGE=1 triangle, FLAT color, DISTINCT per-vertex F. Probe the
// white-box interpolated s2_fog_f vs barycentric ground truth.
// ================================================================
vx0=2; vy0=1; vf0=8'h10;
vx1=13;vy1=2; vf1=8'hF0;
vx2=5; vy2=7; vf2=8'h80;
setup_ref();
f_probe_armed = 1'b1;
drive_reg(R_PRIM, PRIM_TRI_FGE);
drive_reg(R_FRAME_1, FRAME_1_VAL);
drive_reg(R_RGBAQ, rgbaq(TC_R,TC_G,TC_B,TC_A));
drive_reg(R_XYZF2, xyzf2(vx0,vy0,0,vf0));
drive_reg(R_RGBAQ, rgbaq(TC_R,TC_G,TC_B,TC_A));
drive_reg(R_XYZF2, xyzf2(vx1,vy1,0,vf1));
drive_reg(R_RGBAQ, rgbaq(TC_R,TC_G,TC_B,TC_A));
drive_reg(R_XYZF2, xyzf2(vx2,vy2,0,vf2)); // closes
drive_idle();
repeat (320) @(posedge clk);
f_probe_armed = 1'b0;
@(posedge clk);
if (f_probe_checks == 0) begin
$error("[T2] no interior F pixels observed"); errors = errors + 1;
end
// ================================================================
// T3 — FGE=0 triangle, SAME geometry/color as T1, FOGCOL still set.
// Emitted color MUST be byte-identical to the raw vertex color.
// ================================================================
vx0=2; vy0=1; vf0=8'h40;
vx1=13;vy1=2; vf1=8'h40;
vx2=5; vy2=7; vf2=8'h40;
setup_ref();
clear_cov(); cap_armed = 1'b1;
drive_reg(R_PRIM, PRIM_TRI); // FGE = 0
drive_reg(R_FRAME_1, FRAME_1_VAL);
drive_reg(R_RGBAQ, rgbaq(TC_R,TC_G,TC_B,TC_A));
drive_reg(R_XYZF2, xyzf2(vx0,vy0,0,vf0));
drive_reg(R_RGBAQ, rgbaq(TC_R,TC_G,TC_B,TC_A));
drive_reg(R_XYZF2, xyzf2(vx1,vy1,0,vf1));
drive_reg(R_RGBAQ, rgbaq(TC_R,TC_G,TC_B,TC_A));
drive_reg(R_XYZF2, xyzf2(vx2,vy2,0,vf2)); // closes
drive_idle();
repeat (300) @(posedge clk);
cap_armed = 1'b0;
@(posedge clk);
chk_nofog(7, 3, TC_R, TC_G, TC_B, TC_A);
chk_nofog(5, 4, TC_R, TC_G, TC_B, TC_A);
chk_nofog(6, 2, TC_R, TC_G, TC_B, TC_A);
// ================================================================
// T4 — FGE=1 SPRITE, flat color, flat F. Exact fog blend on the
// sprite emit chokepoint (s2_sprite_color64).
// ================================================================
clear_cov(); cap_armed = 1'b1;
drive_reg(R_PRIM, PRIM_SPRITE_FGE);
drive_reg(R_FRAME_1, FRAME_1_VAL);
drive_reg(R_RGBAQ, rgbaq(TC_R,TC_G,TC_B,TC_A));
drive_reg(R_XYZF2, xyzf2(1, 1, 0, 8'hA0)); // sprite origin vertex
drive_reg(R_XYZF2, xyzf2(5, 5, 0, 8'hA0)); // closing vertex -> flat F=0xA0
drive_idle();
repeat (200) @(posedge clk);
cap_armed = 1'b0;
@(posedge clk);
// Interior sprite pixels (rect [1,5)x[1,5)).
chk_fog(2, 2, TC_R, TC_G, TC_B, TC_A, 8'hA0);
chk_fog(3, 4, TC_R, TC_G, TC_B, TC_A, 8'hA0);
chk_fog(4, 1, TC_R, TC_G, TC_B, TC_A, 8'hA0);
$display("[tb_gs_fog] T2 f_checks=%0d errors=%0d", f_probe_checks, errors);
if (errors == 0) $display("[tb_gs_fog] PASS");
else $display("[tb_gs_fog] FAIL");
$finish;
end
initial begin
#5000000;
$error("[tb_gs_fog] timeout");
$finish;
end
endmodule : tb_gs_fog
+346
View File
@@ -0,0 +1,346 @@
// retroDE_ps2 — tb_gs_fog_persp
//
// Focused white-box TB for GS per-vertex FOG on the PERSPECTIVE-textured emit
// path (persp_emit5). Forces PERSPECTIVE_CORRECT=1. The SH3 board scene runs
// this build (PERSPECTIVE_CORRECT=1, COMBINED_TAZ=0, TILE_LOCAL=0), so its
// geometry emits through persp_emit5 — the path round-1 left unfogged.
//
// A perspective-textured DECAL triangle (ST/Q supplied so ras_persp activates)
// samples a SOLID texture (tex_rd_data tied constant → s1_tex_color == TEXEL
// for every pixel), so the emitted color isolates the fog blend and its stage
// alignment without needing to model the perspective UV walk. F is affine /
// screen-linear per vertex (XYZF2[63:56]).
//
// Checks:
// T1p — FGE=1, flat color, DISTINCT per-vertex F. At every persp_emit5:
// (a) ALIGNMENT — the white-box +5-aligned s2_fog_f delay tap
// (u_gs.persp_fog_f5) matches the barycentric (screen-linear) F at
// the emitted pixel (persp_x5,persp_y5) within 1 LSB. A wrong delay
// depth would 1-pixel-shift F and fail here.
// (b) BLEND — the captured emitted RGB equals fog_blend(TEXEL,
// persp_fog_f5) EXACTLY, alpha unchanged.
// T2p — FGE=0, same geometry. Emitted color is BYTE-IDENTICAL to TEXEL (no
// fog) — the non-negotiable invariant on the perspective path.
`timescale 1ns/1ps
module tb_gs_fog_persp;
logic clk;
logic rst_n;
initial clk = 1'b0;
always #5 clk = ~clk;
logic gif_reg_wr_en;
logic [7:0] gif_reg_num;
logic [63:0] gif_reg_data;
logic [7:0] bg_r, bg_g, bg_b;
logic [63:0] prim_q, rgbaq_q, xyz2_q, xyzf2_q, frame_1_q, zbuf_1_q;
logic prim_complete;
logic [31:0] prim_complete_count;
logic [63:0] prim_v0_q, prim_v1_q, prim_v2_q;
logic [63:0] prim_color_q;
logic [63:0] prim_color_v0_q, prim_color_v1_q, prim_color_v2_q;
trace_pkg::vertex_t prim_v0_decoded_q, prim_v1_decoded_q, prim_v2_decoded_q;
trace_pkg::color_t prim_v0_color_decoded_q, prim_v1_color_decoded_q, prim_v2_color_decoded_q;
logic pixel_emit;
logic [31:0] pixel_emit_count;
logic [11:0] pixel_x_q, pixel_y_q;
logic [63:0] pixel_color_q;
logic [8:0] pixel_fbp_q;
logic [5:0] pixel_fbw_q, pixel_psm_q;
logic [31:0] pixel_fb_addr_q;
logic raster_pixel_emit;
logic [31:0] raster_pixel_emit_count;
logic [11:0] raster_pixel_x_q, raster_pixel_y_q;
logic [63:0] raster_pixel_color_q;
logic [31:0] raster_pixel_fb_addr_q;
logic [3:0] raster_pixel_be_q;
logic [31:0] raster_pixel_mask_q;
logic [5:0] raster_pixel_psm_q;
logic raster_active;
logic raster_overflow;
logic raster_fifo_full;
logic raster_degenerate;
logic tex_rd_en; logic [31:0] tex_rd_addr;
logic fb_rd_en; logic [31:0] fb_rd_addr;
logic z_rd_en; logic [31:0] z_rd_addr;
logic ev_valid;
trace_pkg::subsys_e ev_subsys;
trace_pkg::event_e ev_event;
logic [63:0] ev_arg0, ev_arg1, ev_arg2, ev_arg3;
logic [31:0] ev_flags;
// Solid texture: any texel address returns this ABGR word.
localparam logic [31:0] TEXEL = 32'hFF_B0_60_20; // A=FF B=B0 G=60 R=20
logic [31:0] tex_texel;
assign tex_texel = TEXEL;
gs_stub #(.PERSPECTIVE_CORRECT(1'b1)) u_gs (
.clk(clk), .rst_n(rst_n),
.reg_wr_en(1'b0), .reg_wr_addr(16'd0), .reg_wr_data(64'd0),
.gif_reg_wr_en(gif_reg_wr_en),
.gif_reg_num(gif_reg_num),
.gif_reg_data(gif_reg_data),
.bg_r(bg_r), .bg_g(bg_g), .bg_b(bg_b),
.prim_q(prim_q), .rgbaq_q(rgbaq_q),
.xyz2_q(xyz2_q), .xyzf2_q(xyzf2_q),
.frame_1_q(frame_1_q), .zbuf_1_q(zbuf_1_q),
.prim_complete(prim_complete),
.prim_complete_count(prim_complete_count),
.prim_v0_q(prim_v0_q), .prim_v1_q(prim_v1_q), .prim_v2_q(prim_v2_q),
.prim_color_q(prim_color_q),
.prim_color_v0_q(prim_color_v0_q),
.prim_color_v1_q(prim_color_v1_q),
.prim_color_v2_q(prim_color_v2_q),
.prim_v0_decoded_q(prim_v0_decoded_q),
.prim_v1_decoded_q(prim_v1_decoded_q),
.prim_v2_decoded_q(prim_v2_decoded_q),
.prim_v0_color_decoded_q(prim_v0_color_decoded_q),
.prim_v1_color_decoded_q(prim_v1_color_decoded_q),
.prim_v2_color_decoded_q(prim_v2_color_decoded_q),
.pixel_emit(pixel_emit),
.pixel_emit_count(pixel_emit_count),
.pixel_x_q(pixel_x_q), .pixel_y_q(pixel_y_q),
.pixel_color_q(pixel_color_q),
.pixel_fbp_q(pixel_fbp_q),
.pixel_fbw_q(pixel_fbw_q),
.pixel_psm_q(pixel_psm_q),
.pixel_fb_addr_q(pixel_fb_addr_q),
.raster_pixel_emit(raster_pixel_emit),
.raster_pixel_emit_count(raster_pixel_emit_count),
.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_be_q(raster_pixel_be_q),
.raster_pixel_mask_q(raster_pixel_mask_q),
.raster_pixel_psm_q(raster_pixel_psm_q),
.raster_active(raster_active),
.raster_overflow(raster_overflow),
.raster_fifo_full(raster_fifo_full),
.raster_degenerate(raster_degenerate),
.tex_rd_en(tex_rd_en), .tex_rd_addr(tex_rd_addr), .tex_rd_data(tex_texel),
.fb_rd_en(fb_rd_en), .fb_rd_addr(fb_rd_addr), .fb_rd_data(32'd0),
.z_rd_en(z_rd_en), .z_rd_addr(z_rd_addr), .z_rd_data(32'd0),
.ev_valid(ev_valid), .ev_subsys(ev_subsys), .ev_event(ev_event),
.ev_arg0(ev_arg0), .ev_arg1(ev_arg1),
.ev_arg2(ev_arg2), .ev_arg3(ev_arg3),
.ev_flags(ev_flags)
);
// ----- Drive helpers -----
task automatic drive_reg(input logic [7:0] num, input logic [63:0] data);
@(negedge clk);
gif_reg_wr_en = 1'b1; gif_reg_num = num; gif_reg_data = data;
@(posedge clk);
endtask
task automatic drive_idle();
@(negedge clk);
gif_reg_wr_en = 1'b0; gif_reg_num = 8'd0; gif_reg_data = 64'd0;
@(posedge clk);
endtask
// XYZF2 (reg 0x04): X=[15:0] (12.4), Y=[31:16] (12.4), Z=[55:32], F=[63:56].
function automatic logic [63:0] xyzf2(input int x, input int y,
input int z, input int f);
return {8'(f), 24'(z), 12'(y), 4'd0, 12'(x), 4'd0};
endfunction
// RGBAQ with Q in [55:32] (24-bit, FRAC=12).
function automatic logic [63:0] rgbaq_q_val(input int r, input int g, input int b,
input int a, input int q_fp);
return {8'd0, 24'(q_fp), 8'(a), 8'(b), 8'(g), 8'(r)};
endfunction
// ST (reg 0x02): S in [23:0], T in [55:32] (24-bit, FRAC=12).
function automatic logic [63:0] st_val(input int s_fp, input int t_fp);
return {8'd0, 24'(t_fp), 8'd0, 24'(s_fp)};
endfunction
localparam logic [7:0] R_PRIM = 8'h00;
localparam logic [7:0] R_RGBAQ = 8'h01;
localparam logic [7:0] R_ST = 8'h02;
localparam logic [7:0] R_XYZF2 = 8'h04;
localparam logic [7:0] R_TEX0_1 = 8'h06;
localparam logic [7:0] R_FOGCOL = 8'h3D;
localparam logic [7:0] R_FRAME_1 = 8'h4C;
// PRIM: TRIANGLE(3) + TME(bit4) + FGE(bit5).
localparam logic [63:0] PRIM_TRI_TEX_FGE = 64'd3 | (64'd1 << 4) | (64'd1 << 5);
localparam logic [63:0] PRIM_TRI_TEX = 64'd3 | (64'd1 << 4); // FGE=0
localparam logic [63:0] FRAME_1_VAL = 64'h0000_0000_0001_0000; // FBW=1, PSMCT32
// TEX0_1: TBP0=8, TBW=1, PSM=PSMCT32(0), TW=TH=0, TCC=0, TFX=DECAL(1)@[36:35].
localparam logic [63:0] TEX0_VAL = 64'd8 | (64'd1 << 14) | (64'd1 << 35);
localparam int Q_ONE = 24'h001000; // 1.0 in FRAC=12
localparam int FOG_R = 8'h20, FOG_G = 8'h40, FOG_B = 8'h60;
localparam logic [63:0] FOGCOL_VAL = {40'd0, 8'(FOG_B), 8'(FOG_G), 8'(FOG_R)};
function automatic int fog_ch(input int c, input int f, input int fc);
return (c * f + fc * (255 - f)) >> 8;
endfunction
function automatic int absdiff(input int a, input int b);
return (a > b) ? (a - b) : (b - a);
endfunction
// ----- Triangle reference (edge fn + fill rule + barycentric for F) -----
int vx0, vy0, vx1, vy1, vx2, vy2, vf0, vf1, vf2;
int ref_det, sx1, sy1, sx2, sy2, sf1, sf2;
function automatic int edge_f(input int px, input int py,
input int ax, input int ay,
input int bx, input int by);
return (px - ax) * (by - ay) - (py - ay) * (bx - ax);
endfunction
task automatic setup_ref();
int sa;
sa = (vx1 - vx0) * (vy2 - vy0) - (vy1 - vy0) * (vx2 - vx0);
if (sa < 0) begin
sx1 = vx2; sy1 = vy2; sf1 = vf2;
sx2 = vx1; sy2 = vy1; sf2 = vf1;
ref_det = -sa;
end else begin
sx1 = vx1; sy1 = vy1; sf1 = vf1;
sx2 = vx2; sy2 = vy2; sf2 = vf2;
ref_det = sa;
end
endtask
function automatic int ref_f(input int px, input int py);
int L0, L1, L2, num;
L0 = -edge_f(px, py, sx1, sy1, sx2, sy2);
L1 = -edge_f(px, py, sx2, sy2, vx0, vy0);
L2 = -edge_f(px, py, vx0, vy0, sx1, sy1);
num = L0 * vf0 + L1 * sf1 + L2 * sf2;
if (ref_det == 0) return 0;
return num / ref_det;
endfunction
int errors, align_checks, blend_checks;
// Emit capture: expected color computed at persp_emit5 from the RTL's own
// +5-aligned F (persp_fog_f5), plus the alignment check vs barycentric F.
logic [31:0] exp_c [0:31][0:31];
bit exp_set [0:31][0:31];
bit fge_on;
task automatic clear_exp();
for (int y = 0; y < 32; y++)
for (int x = 0; x < 32; x++) begin
exp_c[y][x] = 32'd0; exp_set[y][x] = 1'b0;
end
endtask
// persp_emit5 monitor: alignment + record expected fogged color.
always_ff @(posedge clk) begin
if (rst_n && u_gs.ras_persp && u_gs.persp_emit5) begin
int px, py, ef, af;
logic [31:0] want;
px = int'(u_gs.persp_x5);
py = int'(u_gs.persp_y5);
af = int'(u_gs.persp_fog_f5);
if (px < 32 && py < 32) begin
// (a) alignment: RTL +5-aligned F vs barycentric F at this pixel.
ef = ref_f(px, py);
if (absdiff(af, ef) > 1) begin
$error("[persp align] (%0d,%0d) persp_fog_f5=%0d expected ~%0d", px, py, af, ef);
errors = errors + 1;
end else begin
align_checks = align_checks + 1;
end
// (b) expected emitted color = fog(TEXEL, af) if FGE, else TEXEL.
if (fge_on)
want = {TEXEL[31:24],
8'(fog_ch(int'(TEXEL[23:16]), af, FOG_B)),
8'(fog_ch(int'(TEXEL[15:8]), af, FOG_G)),
8'(fog_ch(int'(TEXEL[7:0]), af, FOG_R))};
else
want = TEXEL;
exp_c[py][px] <= want;
exp_set[py][px] <= 1'b1;
end
end
end
// Capture the actual emitted pixel and compare to the expected recorded above.
always_ff @(posedge clk) begin
if (rst_n && raster_pixel_emit
&& raster_pixel_x_q < 32 && raster_pixel_y_q < 32) begin
if (!exp_set[raster_pixel_y_q][raster_pixel_x_q]) begin
$error("[persp emit] (%0d,%0d) emitted but no persp_emit5 expected color recorded",
raster_pixel_x_q, raster_pixel_y_q);
errors = errors + 1;
end else if (raster_pixel_color_q[31:0] !== exp_c[raster_pixel_y_q][raster_pixel_x_q]) begin
$error("[persp emit] (%0d,%0d) got %08x expected %08x",
raster_pixel_x_q, raster_pixel_y_q,
raster_pixel_color_q[31:0], exp_c[raster_pixel_y_q][raster_pixel_x_q]);
errors = errors + 1;
end else begin
blend_checks = blend_checks + 1;
end
end
end
task automatic drive_persp_tri(input logic [63:0] prim_word);
drive_reg(R_FOGCOL, FOGCOL_VAL);
drive_reg(R_PRIM, prim_word);
drive_reg(R_FRAME_1, FRAME_1_VAL);
drive_reg(R_TEX0_1, TEX0_VAL);
drive_reg(R_RGBAQ, rgbaq_q_val(8'hFF, 8'hFF, 8'hFF, 8'hFF, Q_ONE));
drive_reg(R_ST, st_val(0, 0));
drive_reg(R_XYZF2, xyzf2(vx0, vy0, 0, vf0));
drive_reg(R_RGBAQ, rgbaq_q_val(8'hFF, 8'hFF, 8'hFF, 8'hFF, Q_ONE));
drive_reg(R_ST, st_val(0, 0));
drive_reg(R_XYZF2, xyzf2(vx1, vy1, 0, vf1));
drive_reg(R_RGBAQ, rgbaq_q_val(8'hFF, 8'hFF, 8'hFF, 8'hFF, Q_ONE));
drive_reg(R_ST, st_val(0, 0));
drive_reg(R_XYZF2, xyzf2(vx2, vy2, 0, vf2)); // closes
drive_idle();
repeat (900) @(posedge clk);
endtask
initial begin
errors = 0; align_checks = 0; blend_checks = 0;
rst_n = 1'b0; gif_reg_wr_en = 1'b0; gif_reg_num = 8'd0; gif_reg_data = 64'd0;
clear_exp();
repeat (4) @(posedge clk);
rst_n = 1'b1;
repeat (2) @(posedge clk);
// ============================================================
// T1p — FGE=1, distinct per-vertex F, solid texel. Alignment + blend.
// ============================================================
vx0=2; vy0=2; vf0=8'h20;
vx1=12; vy1=3; vf1=8'hE0;
vx2=4; vy2=9; vf2=8'h80;
setup_ref();
clear_exp(); fge_on = 1'b1;
drive_persp_tri(PRIM_TRI_TEX_FGE);
if (align_checks == 0) begin
$error("[T1p] no persp_emit5 pixels observed (persp path did not fire)");
errors = errors + 1;
end
if (raster_overflow || raster_degenerate) begin
$error("[T1p] raster anomaly"); errors = errors + 1;
end
// ============================================================
// T2p — FGE=0, same geometry. Emitted MUST be byte-identical to TEXEL.
// ============================================================
clear_exp(); fge_on = 1'b0;
drive_persp_tri(PRIM_TRI_TEX);
$display("[tb_gs_fog_persp] align_checks=%0d blend_checks=%0d errors=%0d",
align_checks, blend_checks, errors);
if (errors == 0) $display("[tb_gs_fog_persp] PASS");
else $display("[tb_gs_fog_persp] FAIL");
$finish;
end
initial begin
#8000000;
$error("[tb_gs_fog_persp] timeout");
$finish;
end
endmodule : tb_gs_fog_persp
+118 -2
View File
@@ -42,7 +42,7 @@ module tb_gs_lpddr_axi_master;
gs_lpddr_axi_master #(.FIFO_DEPTH(16)) dut (
.gs_clk(gs_clk), .gs_rst_n(gs_rst_n), .enable(enable),
.arm(arm), .canary(1'b0), .fb_base(fb_base_dut), .ctrl_commit(ctrl_commit),
.px_emit(px_emit), .px_addr(px_addr), .px_pix16(px_pix16),
.px_emit(px_emit), .px_addr(px_addr), .px_pix32({16'd0, px_pix16}), .flush(1'b0),
.axi_clk(axi_clk), .axi_rst_n(axi_rst_n),
.awaddr(awaddr), .awlen(awlen), .awsize(awsize), .awburst(awburst), .awid(awid),
.awvalid(awvalid), .awready(awready),
@@ -59,7 +59,7 @@ module tb_gs_lpddr_axi_master;
gs_lpddr_axi_master #(.FIFO_DEPTH(16)) dut_canary (
.gs_clk(gs_clk), .gs_rst_n(gs_rst_n), .enable(enable),
.arm(arm), .canary(1'b1), .fb_base(32'h0), .ctrl_commit(ctrl_commit),
.px_emit(px_emit), .px_addr(px_addr), .px_pix16(px_pix16),
.px_emit(px_emit), .px_addr(px_addr), .px_pix32({16'd0, px_pix16}), .flush(1'b0),
.axi_clk(axi_clk), .axi_rst_n(axi_rst_n),
.awaddr(), .awlen(), .awsize(), .awburst(), .awid(), .awvalid(can_awvalid), .awready(1'b1),
.wdata(), .wstrb(), .wlast(), .wvalid(can_wvalid), .wready(1'b1),
@@ -68,6 +68,45 @@ module tb_gs_lpddr_axi_master;
.bresp_err_count(), .fifo_overflow_count(), .idle()
);
// ---- Ch353 PSMCT32 DUT (PIX_BYTES=4) + BACKPRESSURED capturing slave (Codex #3) ----
logic px32_emit=0, px32_flush=0;
logic [31:0] px32_addr=0, px32_data=0;
logic [255:0] w32_data; logic [31:0] w32_strb, w32_awaddr; logic w32_awvalid, w32_wvalid, w32_bready, w32_drained;
logic [31:0] beats32, bursts32, ovf32, bresp_err32;
logic force_stall = 1'b0; // Codex saturation test — hold AXI fully off to fill the FIFO
wire aw32_rdy = !force_stall && lfsr[1]; // intermittent AW backpressure (off entirely while stalled)
wire w32_rdy = !force_stall && (lfsr[3] | lfsr[5]);
logic b32_valid; logic [2:0] b32_pending; // delayed B response
always_ff @(posedge axi_clk or negedge axi_rst_n) begin
if (!axi_rst_n) begin b32_valid<=0; b32_pending<=0; end
else begin
if (w32_wvalid && w32_rdy) b32_pending <= 3'd3;
else if (b32_pending != 0) b32_pending <= b32_pending - 3'd1;
if (b32_pending == 3'd1) b32_valid <= 1;
else if (b32_valid && w32_bready) b32_valid <= 0;
end
end
gs_lpddr_axi_master #(.FIFO_DEPTH(16), .PIX_BYTES(4)) dut32 (
.gs_clk(gs_clk), .gs_rst_n(gs_rst_n), .enable(enable),
.arm(1'b1), .canary(1'b0), .fb_base(32'h0), .ctrl_commit(ctrl_commit),
.px_emit(px32_emit), .px_addr(px32_addr), .px_pix32(px32_data), .flush(px32_flush),
.axi_clk(axi_clk), .axi_rst_n(axi_rst_n),
.awaddr(w32_awaddr), .awlen(), .awsize(), .awburst(), .awid(),
.awvalid(w32_awvalid), .awready(aw32_rdy),
.wdata(w32_data), .wstrb(w32_strb), .wlast(), .wvalid(w32_wvalid), .wready(w32_rdy),
.bvalid(b32_valid), .bready(w32_bready), .bresp(2'b00),
.beats_written(beats32), .bursts_issued(bursts32),
.bresp_err_count(bresp_err32), .fifo_overflow_count(ovf32), .idle(), .frame_drained(w32_drained)
);
logic [7:0] smem32 [0:1023];
logic [31:0] aw32_latch;
always_ff @(posedge axi_clk) begin
if (w32_awvalid && aw32_rdy) aw32_latch <= w32_awaddr;
if (w32_wvalid && w32_rdy) for (int i=0;i<32;i++) if (w32_strb[i]) begin
int a32; a32 = aw32_latch + i; if (a32>=0 && a32<1024) smem32[a32] <= w32_data[i*8 +: 8];
end
end
// ---------------- AXI4 slave model (byte memory) + backpressure ----------------
logic [7:0] smem [0:MEM_BYTES-1];
int errors; initial errors=0;
@@ -123,6 +162,9 @@ module tb_gs_lpddr_axi_master;
task automatic emit1(input logic [31:0] a, input logic [15:0] p);
@(negedge gs_clk); px_emit=1'b1; px_addr=a; px_pix16=p;
endtask
task automatic emit32(input logic [31:0] a, input logic [31:0] d);
@(negedge gs_clk); px32_emit=1'b1; px32_addr=a; px32_data=d; px32_flush=1'b0;
endtask
// disarm witness: latch any AXI write activity (must stay 0 while arm=0)
always_ff @(posedge axi_clk) begin
@@ -219,6 +261,80 @@ module tb_gs_lpddr_axi_master;
else $display("[axi] STABILITY ok: %0d beats completed under arm/base perturbation, no AMBA-hold violations", beats_written-beats0);
end
// ===== Ch353 PSMCT32 TEST: 8 px -> one FULL beat; 5 px + flush -> one PARTIAL beat =====
begin
for (int i=0;i<256;i++) smem32[i]=8'h00;
// 8 contiguous PSMCT32 px -> lanes 0..7 of beat 0 -> completes naturally (all 32 strb bits)
for (int k=0;k<8;k++) emit32(32'(k*4), 32'hA000_0000 | 32'(k));
@(negedge gs_clk); px32_emit=1'b0;
begin int d=0; while(beats32<1 && d<4000) begin @(posedge axi_clk); d++; end end // backpressure-tolerant
if (beats32 !== 32'd1) begin $error("[axi32] 8px: beats=%0d (want 1 full beat)", beats32); errors++; end
for (int k=0;k<8;k++) begin
logic [31:0] g; g = {smem32[k*4+3],smem32[k*4+2],smem32[k*4+1],smem32[k*4]};
if (g !== (32'hA000_0000 | 32'(k))) begin $error("[axi32] beat0 lane%0d got=%08x", k, g); errors++; end
end
// 5 px at beat 1 (addr 32..48) -> PARTIAL (lanes 0..4); no auto-flush -> pulse flush
for (int k=0;k<5;k++) emit32(32'(32 + k*4), 32'hB000_0000 | 32'(k));
@(negedge gs_clk); px32_emit=1'b0;
@(negedge gs_clk); px32_flush=1'b1; // end-of-scene partial-beat flush + EOF marker
@(negedge gs_clk); px32_flush=1'b0;
// wait on the ORDERED drain ack (robust under backpressure — set after the last beat's BRESP)
begin int d=0; while(!w32_drained && d<8000) begin @(posedge axi_clk); d++; end end
if (!w32_drained) begin $error("[axi32] frame_drained never asserted under backpressure"); errors++; end
if (beats32 !== 32'd2) begin $error("[axi32] partial+flush: beats=%0d (want 2)", beats32); errors++; end
for (int k=0;k<5;k++) begin
logic [31:0] g; g = {smem32[32+k*4+3],smem32[32+k*4+2],smem32[32+k*4+1],smem32[32+k*4]};
if (g !== (32'hB000_0000 | 32'(k))) begin $error("[axi32] beat1 lane%0d got=%08x", k, g); errors++; end
end
for (int k=5;k<8;k++) begin // unstrobed lanes must be UNTOUCHED (still 0)
logic [31:0] g; g = {smem32[32+k*4+3],smem32[32+k*4+2],smem32[32+k*4+1],smem32[32+k*4]};
if (g !== 32'd0) begin $error("[axi32] beat1 unstrobed lane%0d got=%08x (want 0)", k, g); errors++; end
end
if (ovf32 !== 0) begin $error("[axi32] FIFO overflow=%0d under backpressure", ovf32); errors++; end
if (bresp_err32 !== 0) begin $error("[axi32] bresp_err=%0d", bresp_err32); errors++; end
if (errors==0) $display("[axi32] PSMCT32+backpressure ok: full beat + partial-flush survive AW/W/B stalls; ovf=0 bresp_err=0; ordered drain ack");
end
// ===== Ch353 (Codex) SATURATION: hold AXI off until the FIFO is FULL, then flush. The partial beat and the
// EOF marker must NOT be dropped (their state is retained, gated on !fifo_wfull); on release they push in
// order and frame_drained asserts only after the last BRESP. =====
begin
for (int i=0;i<1024;i++) smem32[i]=8'h00;
force_stall = 1'b1; // AXI fully off -> the FIFO saturates as we emit
for (int bt=0;bt<24;bt++) for (int k=0;k<8;k++) emit32(32'(bt*32 + k*4), 32'hC000_0000 | 32'(bt*8+k));
@(negedge gs_clk); px32_emit=1'b0;
for (int k=0;k<5;k++) emit32(32'(30*32 + k*4), 32'hD000_0000 | 32'(k)); // a PARTIAL beat at a distinct line
@(negedge gs_clk); px32_emit=1'b0;
@(negedge gs_clk); px32_flush=1'b1; @(negedge gs_clk); px32_flush=1'b0; // flush while the FIFO is FULL
repeat(80) @(posedge axi_clk);
if (w32_drained) begin $error("[axi32-sat] frame_drained asserted while AXI stalled (EOF popped early)"); errors++; end
force_stall = 1'b0; // release -> drain -> the GATED partial + marker finally push
begin int d=0; while(!w32_drained && d<60000) begin @(posedge axi_clk); d++; end end
if (!w32_drained) begin $error("[axi32-sat] frame_drained never asserted after release (partial/EOF DROPPED under full FIFO)"); errors++; end
for (int k=0;k<5;k++) begin // the PARTIAL beat must SURVIVE the full-FIFO flush
logic [31:0] g; g = {smem32[30*32+k*4+3],smem32[30*32+k*4+2],smem32[30*32+k*4+1],smem32[30*32+k*4]};
if (g !== (32'hD000_0000 | 32'(k))) begin $error("[axi32-sat] partial lane%0d got=%08x (DROPPED under saturation)", k, g); errors++; end
end
if (errors==0) $display("[axi32-sat] SATURATION ok: FIFO full at flush; partial+EOF retained until accepted; frame_drained after BRESP");
end
// ===== Ch353 (Codex round 3) NEAR-FULL: scene ENDS ON A FULL BEAT, NO partial, flush immediately after the
// final normal beat. The EOF-marker enqueue must not race the prior normal fifo_wr (registered fifo_wfull) —
// gated on !fifo_wr && !fifo_wfull. =====
begin
for (int i=0;i<1024;i++) smem32[i]=8'h00;
force_stall = 1'b1; // AXI off -> FIFO saturates as we emit
for (int bt=0;bt<20;bt++) for (int k=0;k<8;k++) emit32(32'(bt*32 + k*4), 32'hE000_0000 | 32'(bt*8+k));
// flush IMMEDIATELY after the final FULL beat — no partial (has_data=0 once the last beat completes)
@(negedge gs_clk); px32_emit=1'b0; px32_flush=1'b1; @(negedge gs_clk); px32_flush=1'b0;
repeat(40) @(posedge axi_clk);
if (w32_drained) begin $error("[axi32-nf] frame_drained asserted while AXI stalled"); errors++; end
force_stall = 1'b0; // release -> drain -> the EOF marker pushes + pops
begin int d=0; while(!w32_drained && d<60000) begin @(posedge axi_clk); d++; end end
if (!w32_drained) begin $error("[axi32-nf] frame_drained never asserted (EOF DROPPED when scene ended on a full beat)"); errors++; end
if (errors==0) $display("[axi32-nf] NEAR-FULL ok: scene ends on full beat, no partial; EOF survives, frame_drained after BRESP");
end
$display("[tb_gs_lpddr_axi_master] beats=%0d bursts=%0d bresp_err=%0d fifo_ovf=%0d prot_err=%0d idle=%0b errors=%0d",
beats_written, bursts_issued, bresp_err_count, fifo_overflow_count, prot_err, idle, errors);
if (errors==0) $display("[tb_gs_lpddr_axi_master] PASS");
@@ -0,0 +1,168 @@
// retroDE_ps2 — tb_gs_lpddr_axi_master_elastic (Ch357, Codex)
//
// Saturation / producer-backpressure test for gs_lpddr_axi_master with ELASTIC_BACKPRESSURE=1
// (the u_zc_emit|u_c configuration). Proves the elastic (skid) stage + px_ready handshake:
// * a backpressure-aware producer HOLDS px_emit until px_ready and NEVER loses a pixel;
// * FIFO saturation (force_stall fills the FIFO -> px_ready drops -> producer stalls) drops nothing;
// * simultaneous stage drain+refill (intermittent AXI wready) preserves every beat;
// * a partial beat + ordered EOF marker asserted WHILE the FIFO is full retry until accepted;
// * randomized AW/W/B stalls; exact slave-memory contents == replayed reference; col_ovf == 0;
// * frame_drained eventually asserts (no hang).
`timescale 1ns/1ps
module tb_gs_lpddr_axi_master_elastic;
localparam int MEM_WORDS = 4096; // 16 KiB byte memory
localparam int NPIX = 600; // pixels driven (multiple beats + a trailing partial)
logic gs_clk=0; always #5 gs_clk =~gs_clk; // 100 MHz
logic axi_clk=0; always #7 axi_clk=~axi_clk; // ~71 MHz, async
logic gs_rst_n, axi_rst_n;
logic ctrl_commit=0; always #13 ctrl_commit=~ctrl_commit;
// ---- producer (backpressure-aware) ----
logic px_emit;
logic [31:0] px_addr, px_pix;
logic px_ready;
logic flush;
// ---- AXI ----
logic [31:0] awaddr; logic [7:0] awlen; logic [2:0] awsize; logic [1:0] awburst; logic [4:0] awid;
logic awvalid, awready;
logic [255:0] wdata; logic [31:0] wstrb; logic wlast, wvalid, wready;
logic bvalid, bready; logic [1:0] bresp;
logic [31:0] beats_written, bursts_issued, bresp_err_count, col_ovf;
logic idle, frame_drained;
logic force_stall = 1'b0; // hard AXI-off window to fill the FIFO
logic [15:0] lfsr;
always_ff @(posedge axi_clk or negedge axi_rst_n)
if (!axi_rst_n) lfsr<=16'hACE1;
else lfsr<={lfsr[14:0], lfsr[15]^lfsr[13]^lfsr[12]^lfsr[10]};
assign awready = !force_stall && lfsr[0];
assign wready = !force_stall && (lfsr[2] | lfsr[5]);
// delayed B response
logic [2:0] b_pending;
always_ff @(posedge axi_clk or negedge axi_rst_n) begin
if (!axi_rst_n) begin bvalid<=0; b_pending<=0; end
else begin
if (wvalid && wready) b_pending <= 3'd3;
else if (b_pending != 0) b_pending <= b_pending - 3'd1;
if (b_pending == 3'd1) bvalid <= 1;
else if (bvalid && bready) bvalid <= 0;
end
end
assign bresp = 2'b00;
gs_lpddr_axi_master #(.FIFO_DEPTH(16), .PIX_BYTES(4), .ELASTIC_BACKPRESSURE(1'b1)) dut (
.gs_clk(gs_clk), .gs_rst_n(gs_rst_n), .enable(1'b1),
.arm(1'b1), .canary(1'b0), .fb_base(32'h0), .ctrl_commit(ctrl_commit),
.px_emit(px_emit), .px_addr(px_addr), .px_pix32(px_pix), .px_ready(px_ready), .flush(flush),
.axi_clk(axi_clk), .axi_rst_n(axi_rst_n),
.awaddr(awaddr), .awlen(awlen), .awsize(awsize), .awburst(awburst), .awid(awid),
.awvalid(awvalid), .awready(awready),
.wdata(wdata), .wstrb(wstrb), .wlast(wlast), .wvalid(wvalid), .wready(wready),
.bvalid(bvalid), .bready(bready), .bresp(bresp),
.beats_written(beats_written), .bursts_issued(bursts_issued),
.bresp_err_count(bresp_err_count), .fifo_overflow_count(col_ovf), .idle(idle), .frame_drained(frame_drained)
);
// pixel stream: 40 px/line (not a beat multiple -> partials + periodic line-change flushes), 1 KiB line stride.
function automatic [31:0] pix_addr(input int i);
pix_addr = ((i/40) * 32'h400) + ((i%40) * 4);
endfunction
function automatic [31:0] pix_data(input int i);
pix_data = 32'hC0DE0000 | (i & 32'h0000FFFF);
endfunction
// ---- capturing AXI slave (byte memory) — smem is written ONLY here ----
logic [7:0] smem [0:MEM_WORDS-1];
logic [31:0] aw_latch;
always @(posedge axi_clk) begin
if (awvalid && awready) aw_latch <= awaddr;
if (wvalid && wready) for (int i=0;i<32;i++) if (wstrb[i]) begin
int a; a = int'(aw_latch) + i; if (a>=0 && a<MEM_WORDS) smem[a] <= wdata[i*8 +: 8];
end
end
// ---- producer state ----
int pidx; // next pixel to present
int accepted; // pixels accepted (px_emit && px_ready)
logic px_ready_dropped; // witness: backpressure engaged at least once
logic stream_en = 1'b0; // hold the stream until arm_gs (the DUT's 2-FF-synced arm) is high, else px0/px1 would
// be handshake-accepted while the packer (gated on arm_gs) still ignores them.
assign px_emit = stream_en && (pidx < NPIX);
assign px_addr = pix_addr(pidx);
assign px_pix = pix_data(pidx);
always_ff @(posedge gs_clk or negedge gs_rst_n) begin
if (!gs_rst_n) begin pidx<=0; accepted<=0; px_ready_dropped<=0; end
else begin
if (px_emit && !px_ready) px_ready_dropped <= 1'b1; // stalled: proves backpressure
if (px_emit && px_ready) begin pidx <= pidx + 1; accepted <= accepted + 1; end
end
end
// ---- stimulus / checks ----
int errors; initial errors=0;
initial begin
gs_rst_n=0; axi_rst_n=0; flush=0; force_stall=0;
repeat (6) @(posedge axi_clk); axi_rst_n=1;
repeat (4) @(posedge gs_clk); gs_rst_n=1;
repeat (4) @(posedge gs_clk); stream_en=1; // let arm_gs settle before feeding pixels
// Saturation window: after ~120 pixels, hard-stall AXI so the FIFO fills and px_ready must drop.
fork
begin
while (accepted < 120) @(posedge gs_clk);
force_stall = 1'b1; // FIFO fills -> px_ready drops -> producer stalls
repeat (200) @(posedge axi_clk);
force_stall = 1'b0; // release; drain resumes, producer un-stalls
end
join_none
// wait for all pixels accepted (must complete despite the stall)
begin int guard; guard=0;
while (accepted < NPIX && guard <= 400000) begin @(posedge gs_clk); guard++; end
if (accepted < NPIX) begin $error("[elastic] producer HUNG (accepted=%0d/%0d)", accepted, NPIX); errors++; end
end
// partial-beat + EOF marker WHILE momentarily stalling again (partial->marker while full)
force_stall = 1'b1; repeat (30) @(posedge axi_clk);
@(posedge gs_clk); flush = 1'b1; @(posedge gs_clk); flush = 1'b0; // end-of-scene flush requested during stall
repeat (40) @(posedge axi_clk); force_stall = 1'b0;
// wait for the frame to drain (partial + marker accepted, all BRESPs in)
begin int guard; guard=0;
while (!frame_drained && guard <= 400000) begin @(posedge axi_clk); guard++; end
if (!frame_drained) begin $error("[elastic] frame_drained never asserted (drain HUNG)"); errors++; end
end
repeat (20) @(posedge axi_clk);
// ---- checks ----
if (accepted !== NPIX) begin $error("[elastic] accepted=%0d != NPIX=%0d", accepted, NPIX); errors++; end
if (col_ovf !== 0) begin $error("[elastic] col_ovf=%0d (overflow witness must be 0)", col_ovf); errors++; end
if (bresp_err_count !== 0) begin $error("[elastic] bresp_err=%0d", bresp_err_count); errors++; end
if (!px_ready_dropped) begin $error("[elastic] backpressure never engaged (px_ready stayed high under stall)"); errors++; end
// exact contents: replay every accepted pixel and compare its 4 bytes against the slave memory.
for (int i=0;i<accepted;i++) for (int b=0;b<4;b++) begin
int a; logic [31:0] wd; logic [7:0] want;
a = int'(pix_addr(i)) + b; wd = pix_data(i); want = wd[b*8 +: 8];
if (a>=0 && a<MEM_WORDS && smem[a] !== want) begin
if (errors < 20) $error("[elastic] px%0d byte%0d @%0d: smem=%02x != %02x", i, b, a, smem[a], want);
errors++;
end
end
$display("[tb_gs_lpddr_axi_master_elastic] accepted=%0d beats=%0d bursts=%0d col_ovf=%0d bp_seen=%0b errors=%0d",
accepted, beats_written, bursts_issued, col_ovf, px_ready_dropped, errors);
if (errors==0) $display("[tb_gs_lpddr_axi_master_elastic] PASS");
else $display("[tb_gs_lpddr_axi_master_elastic] FAIL");
$finish;
end
initial begin #5000000; $error("[tb_gs_lpddr_axi_master_elastic] TIMEOUT"); $finish; end
endmodule : tb_gs_lpddr_axi_master_elastic
+34
View File
@@ -0,0 +1,34 @@
`timescale 1ns/1ps
module tb_gs_lpddr_color_blend;
logic clk=0,rst_n=0; always #5 clk=~clk;
logic iv,ir; logic [31:0] ia,ic; logic [16:0] ialpha; logic [3:0] ibe;
logic ov,or_; logic [31:0] oa,oc; logic idle;
logic [31:0] araddr; logic [7:0] arlen; logic [2:0] arsize; logic [1:0] arburst; logic arvalid,arready;
logic [255:0] rdata; logic [1:0] rresp; logic rlast,rvalid,rready;
gs_lpddr_color_blend dut(.*,.in_valid(iv),.in_ready(ir),.in_addr(ia),.in_color(ic),.in_alpha(ialpha),.in_be(ibe),.out_valid(ov),.out_ready(or_),.out_addr(oa),.out_color(oc));
initial begin
iv=0; ia=0; ic=0; ialpha=0; ibe=4'hF; or_=1; arready=0; rdata=0; rresp=0; rlast=0; rvalid=0;
repeat(2) @(posedge clk); rst_n=1;
// A=0 B=2(0) C=2(FIX=0x80) D=1(Cd): result Cd-Cs, saturating.
@(negedge clk); iv=1; ia=32'h14; ic=32'h80402010; ialpha={1'b1,2'd2,2'd0,2'd2,2'd1,8'h80};
@(negedge clk); iv=0;
wait(arvalid); @(negedge clk); arready=1; @(negedge clk); arready=0;
wait(rready); rdata[191:160]=32'h40203020; rlast=1; rvalid=1; @(negedge clk); rvalid=0; rlast=0;
wait(ov); if(oa!==32'h14 || oc!==32'h80001010) $fatal(1,"blend got addr=%h color=%h",oa,oc);
repeat(2) @(posedge clk); if(!idle) $fatal(1,"stage did not return idle");
// Authentic SH3 destination-darken equation (ALPHA=0x46):
// (ZERO-Cd)*As/128 + Cd. Black source RGB is intentional; only
// source alpha supplies the coefficient. Cd=0x80604020, As=0x50
// gives floor(Cd*48/128) = 0x24180c in BGR byte order.
@(negedge clk); iv=1; ia=32'h08; ic=32'h50000000; ibe=4'b0111;
ialpha={1'b1,2'd2,2'd1,2'd0,2'd1,8'h80};
@(negedge clk); iv=0;
wait(arvalid); @(negedge clk); arready=1; @(negedge clk); arready=0;
wait(rready); rdata[95:64]=32'h80604020; rlast=1; rvalid=1; @(negedge clk); rvalid=0; rlast=0;
wait(ov); if(oa!==32'h08 || oc!==32'h8024180c) $fatal(1,"masked darken got addr=%h color=%h",oa,oc);
repeat(2) @(posedge clk); if(!idle) $fatal(1,"darken stage did not return idle");
$display("[tb_gs_lpddr_color_blend] PASS color=%08x",oc); $finish;
end
initial #1000 $fatal(1,"timeout");
endmodule
+6
View File
@@ -44,6 +44,9 @@ module tb_gs_lpddr_rd_arb;
.s3_araddr(s3_araddr), .s3_arburst(2'b01), .s3_arid(7'd6), .s3_arlen(8'd0),
.s3_arsize(3'b101), .s3_arvalid(s3_arvalid), .s3_arready(s3_arready),
.s3_rdata(s3_rdata), .s3_rresp(s3_rresp), .s3_rlast(s3_rlast), .s3_rvalid(s3_rvalid), .s3_rready(s3_rready),
.s4_araddr('0), .s4_arburst(2'b01), .s4_arid('0), .s4_arlen('0),
.s4_arsize(3'b101), .s4_arvalid(1'b0), .s4_arready(),
.s4_rdata(), .s4_rresp(), .s4_rlast(), .s4_rvalid(), .s4_rready(1'b0),
.m_araddr(m_araddr), .m_arburst(m_arburst), .m_arid(m_arid), .m_arlen(m_arlen),
.m_arsize(m_arsize), .m_arvalid(m_arvalid), .m_arready(m_arready),
.m_rdata(m_rdata), .m_rresp(m_rresp), .m_rlast(m_rlast), .m_rvalid(m_rvalid), .m_rready(m_rready)
@@ -96,6 +99,9 @@ module tb_gs_lpddr_rd_arb;
int grant_seq [0:3]; int gn;
task automatic priority_test();
begin
// Completion is deliberately registered, so ownership remains
// visible for one cycle after the requester's RLAST handshake.
while (dut.grant!=3'd0) @(posedge clk);
gn = 0;
s0_araddr=30'h10; s1_araddr=30'h11; s2_araddr=30'h12; s3_araddr=30'h13;
s0_arvalid=1; s1_arvalid=1; s2_arvalid=1; s3_arvalid=1;
+192
View File
@@ -0,0 +1,192 @@
// retroDE_ps2 — tb_gs_lpddr_scanout_fb (Ch353 Brick 2 — PSMCT32 LPDDR-FB line-buffer scanout proof)
//
// Closes the loop: the widened writer fills a behavioral LPDDR framebuffer, frame_drained asserts (EMIF domain),
// then gs_lpddr_scanout_lb reads that SAME FB back and serves r/g/b to a REAL video raster (independent video clock,
// with vertical back porch). Asserts every one of the 256x334 pixels — including the BLACK background — matches the
// FB, with underflow=0, rd_errs=0, and exactly 334*32=10688 read beats/frame.
// Codex Brick-2 gates: independent EMIF/video clocks + real cadence; enable gated by frame_drained; FB precleared to
// 0; compare ALL pixels incl. black; account for the 1-video-cycle registered pixel latency; variable AR/R latency.
`timescale 1ns/1ps
module tb_gs_lpddr_scanout_fb;
localparam int W = 256, H = 334; // frame
localparam int STRIDE = W*4; // 1024 B (PSMCT32)
localparam int ROW_BEATS = STRIDE/32; // 32
localparam int FB_BYTES = STRIDE*H; // 342016
localparam int BEATS_PER_FRAME = ROW_BEATS*H; // 10688
// active + blanking (real cadence, incl. horizontal + vertical back/front porch)
localparam int H_ACT=256, V_ACT=334, H_BP=32, H_FP=8, V_BP=16, V_FP=8;
localparam int H_TOT=H_BP+H_ACT+H_FP, V_TOT=V_BP+V_ACT+V_FP;
logic gs_clk=0; always #5 gs_clk=~gs_clk; // writer GS/design clock
logic emif_clk=0; always #2 emif_clk=~emif_clk; // EMIF (fast, independent)
logic video_clk=0; always #7 video_clk=~video_clk;// video (independent of both)
logic rst_n;
logic fb_commit=0; always #13 fb_commit=~fb_commit;
int errors; initial errors=0;
// ============ deterministic frame: left half (x<128) colored, right half BLACK (background) ============
function automatic logic [31:0] exp_word(input int x, input int y);
logic [7:0] r,g,b;
if (x < 128) begin
r = x[7:0]; g = y[7:0]; b = (x*3 + y*7);
if (r==0 && g==0 && b==0) r = 8'h01; // keep the covered region non-black
exp_word = {8'h80, b, g, r};
end else exp_word = 32'd0; // background stays precleared 0
endfunction
// ============ WRITER (PSMCT32) fills the behavioral LPDDR FB ============
logic wr_emit=0, wr_flush=0; logic [31:0] wr_addr=0, wr_data=0;
logic [255:0] fbw_wdata; logic [31:0] fbw_wstrb, fbw_awaddr; logic fbw_awvalid, fbw_wvalid, fbw_bready;
logic fbw_drained, fbw_idle; logic [31:0] fbw_beats, fbw_ovf;
gs_lpddr_axi_master #(.FIFO_DEPTH(64), .PIX_BYTES(4)) u_wr (
.gs_clk(gs_clk), .gs_rst_n(rst_n), .enable(1'b1),
.arm(1'b1), .canary(1'b0), .fb_base(32'h0), .ctrl_commit(fb_commit),
.px_emit(wr_emit), .px_addr(wr_addr), .px_pix32(wr_data), .flush(wr_flush),
.axi_clk(emif_clk), .axi_rst_n(rst_n),
.awaddr(fbw_awaddr), .awlen(), .awsize(), .awburst(), .awid(),
.awvalid(fbw_awvalid), .awready(1'b1),
.wdata(fbw_wdata), .wstrb(fbw_wstrb), .wlast(), .wvalid(fbw_wvalid), .wready(1'b1),
.bvalid(1'b1), .bready(fbw_bready), .bresp(2'b00),
.beats_written(fbw_beats), .bursts_issued(), .bresp_err_count(),
.fifo_overflow_count(fbw_ovf), .idle(fbw_idle), .frame_drained(fbw_drained)
);
// behavioral LPDDR FB — PRECLEARED to 0 (models the HPS preclear), then strobed writes land
logic [7:0] fb [0:FB_BYTES-1];
logic [31:0] fb_awlat;
always_ff @(posedge emif_clk) begin
if (fbw_awvalid) fb_awlat <= fbw_awaddr;
if (fbw_wvalid) for (int i=0;i<32;i++) if (fbw_wstrb[i]) begin
int a; a = fb_awlat + i; if (a>=0 && a<FB_BYTES) fb[a] <= fbw_wdata[i*8 +: 8];
end
end
// ============ SCANOUT reads the SAME FB back ============
logic [11:0] px, py; logic vsync, in_win;
logic [7:0] so_r, so_g, so_b;
logic so_underflow; logic [31:0] so_rd_errs; logic so_line_valid;
logic [29:0] so_araddr; logic [1:0] so_arburst; logic [6:0] so_arid; logic [7:0] so_arlen;
logic [2:0] so_arsize; logic so_arvalid, so_arready;
logic [255:0] so_rdata; logic [1:0] so_rresp; logic so_rlast, so_rvalid, so_rready;
gs_lpddr_scanout_lb #(.FB_BASE(30'd0), .STRIDE_BYTES(STRIDE), .ROW_BEATS(ROW_BEATS),
.N_ROWS(H), .PSMCT32(1'b1)) u_scan (
.axi_clk(emif_clk), .axi_rst_n(rst_n), .enable(fbw_drained), // Codex — gate on frame_drained (EMIF domain)
.video_clk(video_clk), .frame_start(vsync),
.pixel_x(px), .pixel_y(py), .in_window(in_win),
.r(so_r), .g(so_g), .b(so_b),
.line_valid(so_line_valid), .underflow(so_underflow), .rd_errs(so_rd_errs),
.araddr(so_araddr), .arburst(so_arburst), .arid(so_arid), .arlen(so_arlen), .arsize(so_arsize),
.arvalid(so_arvalid), .arready(so_arready), .rdata(so_rdata), .rresp(so_rresp),
.rlast(so_rlast), .rvalid(so_rvalid), .rready(so_rready)
);
// FB read model — single-beat, VARIABLE AR/R latency (Codex), counts read beats/frame
logic [7:0] rlfsr=8'h3C; always_ff @(posedge emif_clk) rlfsr<={rlfsr[6:0], rlfsr[7]^rlfsr[5]^rlfsr[4]^rlfsr[3]};
typedef enum logic [1:0] { R_IDLE, R_WAIT, R_DATA } rst_t; rst_t rst_state;
logic [3:0] rdly; logic [29:0] rd_addr_l; int read_beats;
// count read beats per EMIF-domain frame (between synced vsync edges) — the prefetch's own frame boundary,
// so it's exactly 334 rows regardless of the video/emif CDC phase.
logic [2:0] vs_e; wire vs_edge_e = vs_e[1] && !vs_e[2]; // RISING edge only (one reset per frame)
int fb_reads, fb_reads_last;
always_ff @(posedge emif_clk or negedge rst_n) begin
if (!rst_n) begin rst_state<=R_IDLE; so_arready<=0; so_rvalid<=0; so_rlast<=0; so_rresp<=0; so_rdata<=0; rdly<=0;
read_beats<=0; vs_e<=0; fb_reads<=0; fb_reads_last<=0; end
else begin
so_arready<=0; so_rvalid<=0; so_rlast<=0;
vs_e <= {vs_e[1:0], vsync};
if (vs_edge_e) begin fb_reads_last <= fb_reads; fb_reads <= 0; end
case (rst_state)
R_IDLE: if (so_arvalid) begin so_arready<=1; rd_addr_l<=so_araddr; rdly<=rlfsr[2:0]; rst_state<=R_WAIT; end
R_WAIT: if (rdly==0) rst_state<=R_DATA; else rdly<=rdly-1'b1;
R_DATA: if (so_rready) begin
for (int w=0; w<8; w++) begin
int a; a = rd_addr_l + w*4;
so_rdata[w*32 +: 32] <= (a+3<FB_BYTES) ? {fb[a+3],fb[a+2],fb[a+1],fb[a]} : 32'd0;
end
so_rresp<=2'b00; so_rvalid<=1; so_rlast<=1; read_beats<=read_beats+1;
if (!vs_edge_e) fb_reads<=fb_reads+1;
rst_state<=R_IDLE;
end
endcase
end
end
// ============ REAL video raster (independent video_clk, active + blanking incl. vertical back porch) ============
logic vid_run=0; logic [11:0] rawx, rawy;
always_ff @(posedge video_clk) begin
if (!vid_run) begin rawx<=0; rawy<=0; end
else if (rawx==H_TOT-1) begin rawx<=0; rawy<=(rawy==V_TOT-1)?12'd0:rawy+1'b1; end
else rawx<=rawx+1'b1;
end
// active region is OFFSET by the back porch, giving the row prefetcher lead time before the display needs it.
// The scanout takes ACTIVE-relative pixel_x/pixel_y (0-based) + in_window (as the de25 PCRTC feeds it).
wire active = (rawx>=H_BP)&&(rawx<H_BP+H_ACT)&&(rawy>=V_BP)&&(rawy<V_BP+V_ACT);
assign px = active ? (rawx - H_BP) : 12'd0;
assign py = (rawy>=V_BP && rawy<V_BP+V_ACT) ? (rawy - V_BP) : 12'd0;
assign in_win = active;
assign vsync = vid_run && (rawx==0) && (rawy==0);
// pixel compare — account for the scanout's 1-video-cycle registered latency: r/g/b now correspond to the
// pixel presented LAST cycle (px_q,py_q). Compare against the FB (incl. black background).
logic [11:0] px_q, py_q; logic inwin_q, run_q;
always_ff @(posedge video_clk) begin px_q<=px; py_q<=py; inwin_q<=in_win; run_q<=vid_run; end
int checked; initial checked=0;
logic scoring=0;
always_ff @(posedge video_clk) if (scoring && run_q) begin
logic [7:0] er,eg,eb; logic [31:0] w;
if (inwin_q) begin
int a; a = py_q*STRIDE + px_q*4;
w = {fb[a+3],fb[a+2],fb[a+1],fb[a]};
er = w[7:0]; eg = w[15:8]; eb = w[23:16];
end else begin er=0; eg=0; eb=0; end
checked++;
if (so_r!==er || so_g!==eg || so_b!==eb) begin
if (errors<12) $error("[scan] px(%0d,%0d) in=%b got(%02x,%02x,%02x) exp(%02x,%02x,%02x)",
px_q, py_q, inwin_q, so_r,so_g,so_b, er,eg,eb);
errors++;
end
end
initial begin
rst_n=0;
for (int i=0;i<FB_BYTES;i++) fb[i]=8'h00; // Codex — model the HPS preclear (FB starts black)
repeat(6) @(posedge emif_clk); rst_n=1; repeat(4) @(posedge emif_clk);
// ---- writer fills the FB with the deterministic frame (raster order, left half only) ----
for (int y=0;y<H;y++) for (int x=0;x<128;x++) begin
@(negedge gs_clk); wr_emit<=1'b1; wr_addr<=32'(y*STRIDE + x*4); wr_data<=exp_word(x,y);
end
@(negedge gs_clk); wr_emit<=1'b0;
@(negedge gs_clk); wr_flush<=1'b1; @(negedge gs_clk); wr_flush<=1'b0;
begin int d=0; while(!fbw_drained && d<400000) begin @(posedge emif_clk); d++; end end
if (!fbw_drained) begin $error("[scan] writer frame_drained never asserted"); errors++; end
if (fbw_ovf!==0) begin $error("[scan] writer FIFO overflow=%0d", fbw_ovf); errors++; end
// ---- now enable=frame_drained; run the video raster, let the pipeline prime one frame, then score one full frame ----
vid_run=1;
// prime one full frame (fill line buffers, warm the prefetch), landing on the 2nd frame start
begin int d=0; while(!vsync && d<200000) begin @(posedge video_clk); d++; end end // 1st vsync
@(posedge video_clk); while(!vsync) @(posedge video_clk); // -> 2nd vsync
// score EXACTLY one frame, measured vsync -> next vsync (prefetch reads all 334 rows once)
begin scoring=1;
@(posedge video_clk); while(!vsync) @(posedge video_clk); // score exactly one full frame
scoring=0;
repeat(60) @(posedge emif_clk); // let the emif-domain frame edge latch fb_reads_last
// With rising-edge-only fs_edge_e in the scanout, the prefetcher reads EXACTLY 334 rows/frame.
if (fb_reads_last !== BEATS_PER_FRAME) begin
$error("[scan] read beats/frame=%0d exp %0d (%0dx%0d)", fb_reads_last, BEATS_PER_FRAME, H, ROW_BEATS);
errors++;
end
end
if (so_underflow!==0) begin $error("[scan] underflow asserted (row not ready before its pixel)"); errors++; end
if (so_rd_errs !==0) begin $error("[scan] rd_errs=%0d", so_rd_errs); errors++; end
if (checked < H_ACT*V_ACT) begin $error("[scan] only %0d pixels checked (exp >= %0d) — final rows missed?", checked, H_ACT*V_ACT); errors++; end
$display("[tb_gs_lpddr_scanout_fb] checked=%0d px, read_beats/frame=%0d (exp %0d), underflow=%0b rd_errs=%0d errors=%0d",
checked, fb_reads_last, BEATS_PER_FRAME, so_underflow, so_rd_errs, errors);
if (errors==0) $display("[tb_gs_lpddr_scanout_fb] PASS");
else $display("[tb_gs_lpddr_scanout_fb] FAIL");
$finish;
end
initial begin #60000000; $error("[tb_gs_lpddr_scanout_fb] TIMEOUT"); $finish; end
endmodule : tb_gs_lpddr_scanout_fb
@@ -0,0 +1,156 @@
// ============================================================================
// tb_gs_lpddr_scanout_lb_binomial — Ch438 source-space 3x3 low-pass
//
// Locks the exact separable [1 2 1]/4 kernel, horizontal and vertical edge
// clamps, the 64-source -> 80-output nearest presentation map, and rotation of
// all three physical line buffers. The oracle uses division only in the TB.
// ============================================================================
`timescale 1ns/1ps
module tb_gs_lpddr_scanout_lb_binomial;
localparam int SRC_W=64, OUT_W=80, OUT_H=30;
localparam int STRIDE=320, ROW_BEATS=10, N_ROWS=32, VSTART=2;
logic axi_clk=0, video_clk=0, rst_n=0, enable=0;
always #2 axi_clk=~axi_clk;
always #10 video_clk=~video_clk;
logic frame_start=0, in_window=0;
logic [11:0] pixel_x=0, pixel_y=0;
wire [7:0] r,g,b;
wire line_valid, underflow;
wire [31:0] rd_errs;
wire [29:0] araddr;
wire [1:0] arburst;
wire [6:0] arid;
wire [7:0] arlen;
wire [2:0] arsize;
wire arvalid, rready;
logic arready=0, rvalid=0, rlast=0;
logic [255:0] rdata=0;
logic [1:0] rresp=0;
logic [255:0] mem [0:N_ROWS*ROW_BEATS-1];
initial begin
for (int beat=0; beat<N_ROWS*ROW_BEATS; beat++) mem[beat]='0;
for (int y=0; y<N_ROWS; y++) begin
for (int x=0; x<SRC_W; x++)
mem[y*ROW_BEATS + (x>>3)][(x&7)*32 +: 32] =
{8'hff, 8'(8'h80+x+y), 8'(y), 8'(x)};
end
end
gs_lpddr_scanout_lb #(
.FB_BASE(30'd0), .STRIDE_BYTES(STRIDE), .ROW_BEATS(ROW_BEATS),
.N_ROWS(N_ROWS), .PSMCT32(1'b1), .H_STRETCH_5_TO_4(1'b1),
.V_SOURCE_START(VSTART), .V_STRETCH_15_TO_14(1'b1),
.V_LINEAR_FILTER(1'b0), .H_LINEAR_FILTER(1'b0),
.H_SOURCE_PIXELS(SRC_W), .BINOMIAL_3X3_FILTER(1'b1)
) dut (
.axi_clk(axi_clk), .axi_rst_n(rst_n), .enable(enable),
.video_clk(video_clk), .frame_start(frame_start),
.pixel_x(pixel_x), .pixel_y(pixel_y), .in_window(in_window),
.r(r), .g(g), .b(b), .line_valid(line_valid), .underflow(underflow),
.rd_errs(rd_errs), .araddr(araddr), .arburst(arburst), .arid(arid),
.arlen(arlen), .arsize(arsize), .arvalid(arvalid), .arready(arready),
.rdata(rdata), .rresp(rresp), .rlast(rlast), .rvalid(rvalid), .rready(rready)
);
typedef enum logic [1:0] {S_AR, S_R} state_t;
state_t state=S_AR;
localparam int MEM_BEAT_BITS=$clog2(N_ROWS*ROW_BEATS);
logic [MEM_BEAT_BITS-1:0] beat_q;
always_ff @(posedge axi_clk) begin
arready <= 1'b0;
if (!rst_n) begin
state<=S_AR; rvalid<=1'b0; rlast<=1'b0;
end else case (state)
S_AR: if (arvalid) begin
beat_q<=araddr[MEM_BEAT_BITS+4:5]; arready<=1'b1; state<=S_R;
end
S_R: begin
rdata<=mem[beat_q]; rresp<=2'b00; rlast<=1'b1; rvalid<=1'b1;
if (rready && rvalid) begin rvalid<=1'b0; rlast<=1'b0; state<=S_AR; end
end
endcase
end
function automatic int source_x(input int out_x);
source_x = (out_x * 4) / 5;
endfunction
function automatic int source_y(input int out_y);
source_y = VSTART + (out_y * 14) / 15;
endfunction
function automatic int clamp_x(input int x);
clamp_x = (x < 0) ? 0 : ((x >= SRC_W) ? SRC_W-1 : x);
endfunction
function automatic int clamp_y(input int y);
clamp_y = (y < VSTART) ? VSTART : ((y >= N_ROWS) ? N_ROWS-1 : y);
endfunction
function automatic int sample(input int channel, input int x, input int y);
int xx,yy;
begin
xx=clamp_x(x); yy=clamp_y(y);
case (channel)
0: sample=xx;
1: sample=yy;
default: sample=8'h80+xx+yy;
endcase
end
endfunction
function automatic int hbin(input int channel, input int x, input int y);
hbin=(sample(channel,x-1,y) + 2*sample(channel,x,y) +
sample(channel,x+1,y) + 2) / 4;
endfunction
function automatic int binomial(input int channel, input int x, input int y);
binomial=(hbin(channel,x,y-1) + 2*hbin(channel,x,y) +
hbin(channel,x,y+1) + 2) / 4;
endfunction
int errors=0, checked=0;
logic check_en=0;
logic [11:0] x_d, y_d;
logic in_d;
always_ff @(posedge video_clk) begin
x_d<=pixel_x; y_d<=pixel_y; in_d<=in_window;
if (check_en && in_d) begin
int sx,sy,er,eg,eb;
sx=source_x(x_d); sy=source_y(y_d);
er=binomial(0,sx,sy);
eg=binomial(1,sx,sy);
eb=binomial(2,sx,sy);
checked++;
if (r!==8'(er) || g!==8'(eg) || b!==8'(eb)) begin
if (errors < 32)
$display("[binomial] out=(%0d,%0d) source=(%0d,%0d) got=%02x/%02x/%02x exp=%02x/%02x/%02x",
x_d,y_d,sx,sy,r,g,b,8'(er),8'(eg),8'(eb));
errors++;
end
end
end
initial begin
repeat(8) @(posedge axi_clk); rst_n=1; enable=1;
frame_start=1; repeat(3) @(posedge video_clk); frame_start=0;
repeat(300) @(posedge axi_clk);
repeat(3) @(posedge video_clk);
check_en=1;
for (int y=0; y<OUT_H; y++) begin
for (int x=0; x<OUT_W; x++) begin
@(negedge video_clk);
pixel_x=12'(x); pixel_y=12'(y); in_window=1;
@(posedge video_clk);
end
@(negedge video_clk); in_window=0;
repeat(25) @(posedge video_clk);
end
check_en=0;
$display("[binomial] checked=%0d errors=%0d underflow=%0b rd_errs=%0d", checked,errors,underflow,rd_errs);
if (checked==OUT_W*OUT_H && errors==0 && !underflow && rd_errs==0)
$display("[tb_gs_lpddr_scanout_lb_binomial] PASS");
else
$display("[tb_gs_lpddr_scanout_lb_binomial] FAIL");
$finish;
end
initial begin #2_000_000; $display("[tb_gs_lpddr_scanout_lb_binomial] TIMEOUT"); $finish; end
endmodule
@@ -0,0 +1,161 @@
// ============================================================================
// tb_gs_lpddr_scanout_lb_hstretch — Ch418 captured SH3 display mapping
//
// Proves the divider-free 5-output/4-source presentation map and Ch437 linear
// reconstruction used to reduce
// the dump's 512-source-pixel, MAGH=4 display onto a 640-pixel active line.
// A compact 64-source -> 80-output instance locks the exact x0/xf coordinates
// across all eight beat slots, including the source-right clamp.
// Thirty output lines also lock the captured interlace reduction, source_y =
// 2 + floor(output_y*14/15), including its repeated first line.
// ============================================================================
`timescale 1ns/1ps
module tb_gs_lpddr_scanout_lb_hstretch;
localparam int SRC_W=64, OUT_W=80, OUT_H=30;
// Physical stride is 80 pixels, matching the output raster; the captured
// display source occupies the first 64 pixels of each row.
localparam int STRIDE=320, ROW_BEATS=10, N_ROWS=32, VSTART=2;
logic axi_clk=0, video_clk=0, rst_n=0, enable=0;
always #2 axi_clk=~axi_clk;
always #10 video_clk=~video_clk;
logic frame_start=0, in_window=0;
logic [11:0] pixel_x=0, pixel_y=0;
wire [7:0] r,g,b;
wire line_valid, underflow;
wire [31:0] rd_errs;
wire [29:0] araddr;
wire [1:0] arburst;
wire [6:0] arid;
wire [7:0] arlen;
wire [2:0] arsize;
wire arvalid, rready;
logic arready=0, rvalid=0, rlast=0;
logic [255:0] rdata=0;
logic [1:0] rresp=0;
logic [255:0] mem [0:N_ROWS*ROW_BEATS-1];
initial begin
for (int beat=0; beat<N_ROWS*ROW_BEATS; beat++) mem[beat]='0;
for (int y=0; y<N_ROWS; y++) begin
for (int x=0; x<SRC_W; x++)
mem[y*ROW_BEATS + (x>>3)][(x&7)*32 +: 32] =
{8'hff, 8'(8'h80+x+y), 8'(y), 8'(x)};
end
end
gs_lpddr_scanout_lb #(
.FB_BASE(30'd0), .STRIDE_BYTES(STRIDE), .ROW_BEATS(ROW_BEATS),
.N_ROWS(N_ROWS), .PSMCT32(1'b1), .H_STRETCH_5_TO_4(1'b1),
.V_SOURCE_START(VSTART), .V_STRETCH_15_TO_14(1'b1),
.V_LINEAR_FILTER(1'b1), .H_LINEAR_FILTER(1'b1),
.H_SOURCE_PIXELS(SRC_W)
) dut (
.axi_clk(axi_clk), .axi_rst_n(rst_n), .enable(enable),
.video_clk(video_clk), .frame_start(frame_start),
.pixel_x(pixel_x), .pixel_y(pixel_y), .in_window(in_window),
.r(r), .g(g), .b(b), .line_valid(line_valid), .underflow(underflow),
.rd_errs(rd_errs), .araddr(araddr), .arburst(arburst), .arid(arid),
.arlen(arlen), .arsize(arsize), .arvalid(arvalid), .arready(arready),
.rdata(rdata), .rresp(rresp), .rlast(rlast), .rvalid(rvalid), .rready(rready)
);
typedef enum logic [1:0] {S_AR, S_R} state_t;
state_t state=S_AR;
localparam int MEM_BEAT_BITS=$clog2(N_ROWS*ROW_BEATS);
logic [MEM_BEAT_BITS-1:0] beat_q;
always_ff @(posedge axi_clk) begin
arready <= 1'b0;
if (!rst_n) begin
state<=S_AR; rvalid<=1'b0; rlast<=1'b0;
end else case (state)
S_AR: if (arvalid) begin
beat_q<=araddr[MEM_BEAT_BITS+4:5]; arready<=1'b1; state<=S_R;
end
S_R: begin
rdata<=mem[beat_q]; rresp<=2'b00; rlast<=1'b1; rvalid<=1'b1;
if (rready && rvalid) begin rvalid<=1'b0; rlast<=1'b0; state<=S_AR; end
end
endcase
end
function automatic int source_x(input int out_x);
source_x = (out_x * 4) / 5; // testbench oracle only; not synthesized
endfunction
function automatic int source_x_frac(input int out_x);
source_x_frac = (out_x * 4) % 5;
endfunction
function automatic int source_y(input int out_y);
source_y = VSTART + (out_y * 14) / 15;
endfunction
function automatic int source_y_frac(input int out_y);
source_y_frac = (out_y * 14) % 15;
endfunction
function automatic int blend15(input int cur, input int nxt, input int frac);
blend15 = ((15-frac)*cur + frac*nxt + 7) / 15;
endfunction
function automatic int blend5(input int left, input int right, input int frac);
blend5 = ((5-frac)*left + frac*right + 2) / 5;
endfunction
int errors=0, checked=0;
logic check_en=0;
logic [11:0] x_d, y_d;
logic in_d;
always_ff @(posedge video_clk) begin
x_d<=pixel_x; y_d<=pixel_y; in_d<=in_window;
if (check_en && in_d) begin
int sx,sx1,xf,sy,sy1,yf,er,eg,eb;
sx=source_x(x_d);
sx1=(sx+1<SRC_W) ? sx+1 : sx;
xf=source_x_frac(x_d);
sy=source_y(y_d); yf=source_y_frac(y_d);
sy1=(sy+1<N_ROWS) ? sy+1 : sy;
er=blend5(sx,sx1,xf);
eg=blend15(sy,sy1,yf);
eb=blend5(blend15(8'h80+sx +sy,8'h80+sx +sy1,yf),
blend15(8'h80+sx1+sy,8'h80+sx1+sy1,yf),xf);
checked++;
if (r!==8'(er) || g!==8'(eg) || b!==8'(eb)) begin
$display("[hstretch] out=(%0d,%0d) source=(%0d+%0d/5,%0d+%0d/15) got=%02x/%02x/%02x exp=%02x/%02x/%02x",
x_d,y_d,sx,xf,sy,yf,r,g,b,8'(er),8'(eg),8'(eb));
errors++;
end
end
end
initial begin
repeat(8) @(posedge axi_clk); rst_n=1; enable=1;
frame_start=1; repeat(3) @(posedge video_clk); frame_start=0;
// Load both rows before scanout so this TB isolates coordinate mapping.
repeat(300) @(posedge axi_clk);
repeat(3) @(posedge video_clk);
check_en=1;
for (int y=0; y<OUT_H; y++) begin
for (int x=0; x<OUT_W; x++) begin
// Drive away from the sampling edge. The original Ch418 TB
// raced its next x assignment against the DUT at posedge;
// nearest filtering hid the duplicate x=1 sample, while a
// fractional filter correctly makes x=0 and x=1 differ.
@(negedge video_clk);
pixel_x=12'(x); pixel_y=12'(y); in_window=1;
@(posedge video_clk);
end
// Model a real horizontal blank interval. Linear filtering uses
// it to prefetch the next source row into the retired parity
// buffer before active video resumes.
@(negedge video_clk); in_window=0;
repeat(25) @(posedge video_clk);
end
check_en=0;
$display("[hstretch] checked=%0d errors=%0d underflow=%0b rd_errs=%0d", checked,errors,underflow,rd_errs);
if (checked==OUT_W*OUT_H && errors==0 && !underflow && rd_errs==0)
$display("[tb_gs_lpddr_scanout_lb_hstretch] PASS");
else
$display("[tb_gs_lpddr_scanout_lb_hstretch] FAIL");
$finish;
end
initial begin #2_000_000; $display("[tb_gs_lpddr_scanout_lb_hstretch] TIMEOUT"); $finish; end
endmodule
+213
View File
@@ -0,0 +1,213 @@
// retroDE_ps2 — tb_gs_lpddr_z_rmw (Ch357 gate 1 unit TB — packed PSMZ16S LPDDR Z RMW engine)
//
// Proves the standalone Z RMW engine against a software golden model:
// * clamp16 source (min(z,0xFFFF)) + GEQUAL + ZMSK (test always runs; write suppressed only when zmsk=1).
// * packed 16-bit Z, single write-back cache line: same-beat / same-PIXEL RMW hazards forwarded (no stale read).
// * cross-beat PERSISTENCE: revisit passes read back Z written earlier (evicted to LPDDR then refetched).
// * RANDOM AXI backpressure on AR/R/AW/W/B and random p_ready backpressure -> assert ZERO drops/overflow, 0 BRESP errs.
// * preclear to the GEQUAL clear value.
// Every fragment must produce exactly one result with p_pass == golden; final stored Z (LPDDR + the one dirty line) == golden.
`timescale 1ns/1ps
module tb_gs_lpddr_z_rmw;
localparam int FB_PXW = 64, FB_H = 32;
localparam int NPX = FB_PXW*FB_H; // 2048
localparam int NBEATS = (NPX+15)/16; // 128
localparam int BIW = $clog2(NBEATS); // beat-index width for mem addressing
localparam [31:0] ZBASE = 32'h0030_0000;
localparam [15:0] Z_CLEAR = 16'h0000;
localparam int NFRAG = 6000;
logic clk=0; always #5 clk=~clk;
logic rst_n;
int errors; initial errors=0;
// DUT I/O
logic enable, clear_start, clear_done, scene_flush, z_drained;
logic f_valid, f_ready; logic [11:0] f_x, f_y; logic [31:0] f_z; logic f_zmsk; logic [1:0] f_ztst;
logic p_valid, p_ready, p_pass; logic [11:0] p_x, p_y; logic [15:0] p_zq;
logic [31:0] araddr; logic [7:0] arlen; logic [2:0] arsize; logic [1:0] arburst; logic arvalid, arready;
logic [255:0] rdata; logic [1:0] rresp; logic rlast, rvalid, rready;
logic [31:0] awaddr; logic [7:0] awlen; logic [2:0] awsize; logic [1:0] awburst; logic awvalid, awready;
logic [255:0] wdata; logic [31:0] wstrb; logic wlast, wvalid, wready;
logic bvalid, bready; logic [1:0] bresp;
logic [31:0] beats_read, beats_written, bresp_err; logic idle;
gs_lpddr_z_rmw #(.ZBASE(ZBASE), .FB_PXW(FB_PXW), .FB_H(FB_H), .Z_CLEAR(Z_CLEAR)) dut (
.clk(clk), .rst_n(rst_n), .enable(enable), .clear_start(clear_start), .clear_done(clear_done),
.scene_flush(scene_flush), .z_drained(z_drained),
.f_valid(f_valid), .f_ready(f_ready), .f_x(f_x), .f_y(f_y), .f_z(f_z), .f_zmsk(f_zmsk), .f_ztst(f_ztst),
.p_valid(p_valid), .p_ready(p_ready), .p_pass(p_pass), .p_x(p_x), .p_y(p_y), .p_zq(p_zq),
.araddr(araddr), .arlen(arlen), .arsize(arsize), .arburst(arburst), .arvalid(arvalid), .arready(arready),
.rdata(rdata), .rresp(rresp), .rlast(rlast), .rvalid(rvalid), .rready(rready),
.awaddr(awaddr), .awlen(awlen), .awsize(awsize), .awburst(awburst), .awvalid(awvalid), .awready(awready),
.wdata(wdata), .wstrb(wstrb), .wlast(wlast), .wvalid(wvalid), .wready(wready),
.bvalid(bvalid), .bready(bready), .bresp(bresp),
.beats_read(beats_read), .beats_written(beats_written), .bresp_err(bresp_err), .idle(idle)
);
// ---------------- behavioral LPDDR (256-bit) with RANDOM backpressure ----------------
logic [255:0] mem [0:NBEATS-1];
logic [15:0] lfsr=16'hACE1; always_ff @(posedge clk) lfsr<={lfsr[14:0], lfsr[15]^lfsr[13]^lfsr[12]^lfsr[10]};
// read channel
logic [31:0] rd_addr_l; logic rd_pending; logic [3:0] rd_dly;
// write channel
logic [31:0] wr_addr_l; logic [255:0] wr_data_l; logic aw_seen, w_seen; logic [3:0] b_dly; logic b_pending;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
arready<=0; rvalid<=0; rlast<=0; rresp<=0; rdata<=0; rd_pending<=0; rd_dly<=0;
awready<=0; wready<=0; bvalid<=0; bresp<=0; aw_seen<=0; w_seen<=0; b_pending<=0; b_dly<=0;
end else begin
// AR: random-accept when no read in flight
arready <= (!rd_pending && !rvalid) ? lfsr[0] : 1'b0;
if (arvalid && arready) begin rd_addr_l<=araddr; rd_pending<=1; rd_dly<={1'b0,lfsr[3:1]}; arready<=0; end
// R: after random delay, present data
if (rd_pending && !rvalid) begin
if (rd_dly==0) begin rdata<=mem[rd_addr_l[5 +: BIW]]; rresp<=2'b00; rvalid<=1; rlast<=1; rd_pending<=0; end
else rd_dly<=rd_dly-1'b1;
end
if (rvalid && rready) begin rvalid<=0; rlast<=0; end
// AW / W: random-accept
awready <= (!aw_seen) ? lfsr[4] : 1'b0;
wready <= (!w_seen) ? lfsr[5] : 1'b0;
if (awvalid && awready) begin wr_addr_l<=awaddr; aw_seen<=1; awready<=0; end
if (wvalid && wready ) begin wr_data_l<=wdata; w_seen<=1; wready<=0; end
// commit write when both seen, then random B delay
if (aw_seen && w_seen && !b_pending && !bvalid) begin
mem[wr_addr_l[5 +: BIW]]<=wr_data_l; b_pending<=1; b_dly<={1'b0,lfsr[8:6]};
end
if (b_pending && !bvalid) begin
if (b_dly==0) begin bvalid<=1; bresp<=2'b00; b_pending<=0; aw_seen<=0; w_seen<=0; end
else b_dly<=b_dly-1'b1;
end
if (bvalid && bready) bvalid<=0;
end
end
// ---------------- golden model + fragment vectors ----------------
logic [11:0] vx [0:NFRAG-1]; logic [11:0] vy [0:NFRAG-1]; logic [31:0] vz [0:NFRAG-1]; logic vzmsk [0:NFRAG-1]; logic [1:0] vztst [0:NFRAG-1];
logic exp_pass [0:NFRAG-1];
logic [15:0] gold [0:NPX-1];
function automatic logic [15:0] clamp16(input logic [31:0] z); clamp16 = (|z[31:16]) ? 16'hFFFF : z[15:0]; endfunction
function automatic logic zpass(input logic [1:0] op, input logic [15:0] s, input logic [15:0] d);
case (op) 2'd0:zpass=0; 2'd1:zpass=1; 2'd2:zpass=(s>=d); default:zpass=(s>d); endcase
endfunction
// disjoint-range monitor: every Z AXI address must land inside the Z buffer region [ZBASE, ZBASE+NBEATS*32)
always_ff @(posedge clk) if (rst_n) begin
if (arvalid && (araddr < ZBASE || araddr >= ZBASE + NBEATS*32)) begin $error("[zrmw] AR addr %h out of Z range", araddr); errors++; end
if (awvalid && (awaddr < ZBASE || awaddr >= ZBASE + NBEATS*32)) begin $error("[zrmw] AW addr %h out of Z range", awaddr); errors++; end
end
int fi_drv, fi_chk;
initial begin
errors=0; enable=0; clear_start=0; scene_flush=0; f_valid=0; f_x=0; f_y=0; f_z=0; f_zmsk=0; f_ztst=2'd2; p_ready=0;
for (int i=0;i<NPX;i++) gold[i]=Z_CLEAR;
// generate fragments: 3 "epochs" over overlapping regions to force evict->refill persistence + revisits,
// with same-pixel/same-beat bursts and a fraction of zmsk (test-only) fragments.
begin int f, reg_w, reg_h, ox, oy; f=0;
for (int ep=0; ep<3 && f<NFRAG; ep++) begin
reg_w = 24 + (ep*8); reg_h = 16 + (ep*4); ox = ep*4; oy = ep*2;
for (int n=0; n<NFRAG/3 && f<NFRAG; n++) begin
int lx, ly; logic [31:0] zz; logic zm;
lx = ox + ({$random}%reg_w); ly = oy + ({$random}%reg_h);
if (lx<0) lx=0; if (lx>=FB_PXW) lx=FB_PXW-1;
if (ly<0) ly=0; if (ly>=FB_H) ly=FB_H-1;
// Z: mix full-range (clamps) and sub-0xFFFF (discriminating) + occasional same value
case ({$random}%4)
0: zz = {$random} & 32'h00FF_FFFF; // 24-bit (often clamps)
1: zz = {$random} & 32'h0000_FFFF; // <=0xFFFF (discriminating)
2: zz = {$random} & 32'h0000_3FFF; // small
default: zz = 32'h0001_0000 + ({$random}&32'hFFFF); // just over 0xFFFF -> clamps to 0xFFFF
endcase
zm = (({$random}%8)==0); // ~12% test-only (zmsk=1)
vx[f]=lx[11:0]; vy[f]=ly[11:0]; vz[f]=zz; vzmsk[f]=zm;
// Preserve the old GEQUAL-heavy stress while explicitly
// covering NEVER, ALWAYS and strict GREATER.
vztst[f]=(n[3:0]==4'd0)?2'd1:(n[3:0]==4'd1)?2'd0:(n[3:0]==4'd2)?2'd3:2'd2;
begin int idx; logic [15:0] sz, dz; logic pass;
idx=ly*FB_PXW+lx; sz=clamp16(zz); dz=gold[idx]; pass=zpass(vztst[f],sz,dz);
exp_pass[f]=pass; if (pass && !zm) gold[idx]=sz;
end
f=f+1;
end
end
// pad remainder (if any) with same-pixel bursts to stress forwarding
while (f<NFRAG) begin
int idx; logic [15:0] sz, dz; logic pass; logic [31:0] zz;
vx[f]=12'd10; vy[f]=12'd10; zz={$random}&32'h0000_FFFF; vz[f]=zz; vzmsk[f]=1'b0; vztst[f]=2'd2;
idx=10*FB_PXW+10; sz=clamp16(zz); dz=gold[idx]; pass=(sz>=dz); exp_pass[f]=pass; if(pass) gold[idx]=sz;
f=f+1;
end
end
rst_n=0; repeat(6) @(posedge clk); rst_n=1; repeat(4) @(posedge clk);
enable=1;
// ---- preclear ----
@(negedge clk) clear_start=1; @(negedge clk) clear_start=0;
begin int g=0; while(!clear_done && g<200000) begin @(posedge clk); g++; end end
if (!clear_done) begin $error("[zrmw] preclear never completed"); errors++; end
for (int b=0;b<NBEATS;b++) if (mem[b]!=={16{Z_CLEAR}}) begin
if (errors<8) $error("[zrmw] preclear beat %0d = %h", b, mem[b]); errors++; end
$display("[zrmw] preclear done: %0d beats = 0x%04x, bresp_err=%0d", NBEATS, Z_CLEAR, bresp_err);
fork
begin : DRIVER
fi_drv=0;
while (fi_drv<NFRAG) begin
// random bubbles on the producer side
if (lfsr[9]) begin f_valid<=0; @(posedge clk); end
else begin
f_valid<=1; f_x<=vx[fi_drv]; f_y<=vy[fi_drv]; f_z<=vz[fi_drv]; f_zmsk<=vzmsk[fi_drv]; f_ztst<=vztst[fi_drv];
@(posedge clk);
if (f_valid && f_ready) fi_drv=fi_drv+1;
end
end
f_valid<=0;
end
begin : CHECKER
fi_chk=0;
while (fi_chk<NFRAG) begin
p_ready <= lfsr[11]; // random consumer backpressure
@(posedge clk);
if (p_valid && p_ready) begin
if (p_pass !== exp_pass[fi_chk]) begin
if (errors<16) $error("[zrmw] frag %0d (%0d,%0d z=%h zmsk=%b ztst=%0d): pass=%b exp=%b zq=%h",
fi_chk, p_x, p_y, vz[fi_chk], vzmsk[fi_chk], vztst[fi_chk], p_pass, exp_pass[fi_chk], p_zq); errors++; end
if (p_x!==vx[fi_chk] || p_y!==vy[fi_chk]) begin
if (errors<16) $error("[zrmw] frag %0d result xy (%0d,%0d) exp (%0d,%0d)", fi_chk,p_x,p_y,vx[fi_chk],vy[fi_chk]); errors++; end
fi_chk=fi_chk+1;
end
end
p_ready<=1;
end
join
// ---- scene-end flush: assert scene_flush, wait z_drained; then ALL stored Z (in LPDDR) must equal golden ----
// (this also proves the cache PERSISTS: after the flush it stays valid+clean, and mem is fully durable.)
repeat(20) @(posedge clk);
scene_flush<=1'b1;
begin int g=0; while(!z_drained && g<200000) begin @(posedge clk); g++; end end
if (!z_drained) begin $error("[zrmw] scene_flush: z_drained never asserted"); errors++; end
if (dut.cache_dirty) begin $error("[zrmw] scene_flush: cache still dirty after drain"); errors++; end
if (!dut.cache_valid) begin $error("[zrmw] scene_flush: cache invalidated (Z must persist across epochs)"); errors++; end
repeat(4) @(posedge clk); scene_flush<=1'b0;
for (int idx=0; idx<NPX; idx++) begin
int b, ln; logic [15:0] stored;
b=idx>>4; ln=idx[3:0]; stored=mem[b][ln*16 +: 16]; // no cache peek: after scene flush, LPDDR is authoritative
if (stored!==gold[idx]) begin
if (errors<16) $error("[zrmw] durable Z px %0d (beat %0d lane %0d) = %04x exp %04x", idx,b,ln,stored,gold[idx]); errors++; end
end
$display("[zrmw] scene_flush drained: z_drained=%0b cache_valid=%0b(persist) cache_dirty=%0b; LPDDR==golden",
z_drained, dut.cache_valid, dut.cache_dirty);
if (bresp_err!==0) begin $error("[zrmw] bresp_err=%0d", bresp_err); errors++; end
if (fi_drv!==NFRAG || fi_chk!==NFRAG) begin $error("[zrmw] DROP: driven=%0d checked=%0d exp %0d", fi_drv, fi_chk, NFRAG); errors++; end
$display("[zrmw] frags driven=%0d checked=%0d (zero drops) beats_read=%0d beats_written=%0d bresp_err=%0d errors=%0d",
fi_drv, fi_chk, beats_read, beats_written, bresp_err, errors);
if (errors==0) $display("[tb_gs_lpddr_z_rmw] PASS"); else $display("[tb_gs_lpddr_z_rmw] FAIL");
$finish;
end
initial begin #20000000; $error("[tb_gs_lpddr_z_rmw] TIMEOUT"); $finish; end
endmodule : tb_gs_lpddr_z_rmw
+217
View File
@@ -0,0 +1,217 @@
// retroDE_ps2 — tb_gs_lpddr_zc_emit (Ch357 — Z-then-color integration unit TB)
//
// Two-clock (gs_clk != axi_clk) test of the integration wrapper against a software golden model. Covers Codex gate-7:
// * request-FIFO backpressure (g_ready) across the CDC.
// * REJECTED-COLOR SUPPRESSION: a fragment failing Z writes NO color.
// * ordered SCENE-MARKER drain: after each marker, combined frame_drained rises (color + Z BRESPs complete).
// * Z cache PERSISTENCE across epochs: Z precleared ONCE at frame start; 3 epochs accumulate.
// * DISJOINT ranges: Z AXI in [ZBASE..), color AXI in [COLBASE..); the two never cross.
// * col_ovf==0, bresp_err==0; final color FB == golden (Z-winning color per pixel); final Z == golden.
`timescale 1ns/1ps
module tb_gs_lpddr_zc_emit;
localparam int FB_PXW=32, FB_H=16;
localparam int NPX=FB_PXW*FB_H; // 512
localparam int ZBEATS=(NPX+15)/16; // 32
localparam int CBEATS=(NPX+7)/8; // 64 (8 PSMCT32 / beat)
localparam [31:0] COLBASE=32'h0000_0000, ZBASE=32'h0014_0000;
localparam int NFRAG=1500, NEP=3;
localparam int ZBW=$clog2(ZBEATS), CBW=$clog2(CBEATS);
logic gs_clk=0; always #5 gs_clk=~gs_clk;
logic axi_clk=0; always #7 axi_clk=~axi_clk;
logic gs_rst_n, axi_rst_n; int errors; initial errors=0;
logic enable, clear_start, clear_done, frame_drained;
logic g_valid, g_ready; logic [11:0] g_x, g_y; logic [15:0] g_zq; logic g_zmsk, g_ztest, g_scene; logic [1:0] g_ztst; logic [31:0] g_color;
// Z AXI
logic [31:0] z_araddr; logic [7:0] z_arlen; logic [2:0] z_arsize; logic [1:0] z_arburst; logic z_arvalid, z_arready;
logic [255:0] z_rdata; logic [1:0] z_rresp; logic z_rlast, z_rvalid, z_rready;
logic [31:0] z_awaddr; logic [7:0] z_awlen; logic [2:0] z_awsize; logic [1:0] z_awburst; logic z_awvalid, z_awready;
logic [255:0] z_wdata; logic [31:0] z_wstrb; logic z_wlast, z_wvalid, z_wready; logic z_bvalid, z_bready; logic [1:0] z_bresp;
// Color AXI
logic [31:0] c_awaddr; logic [7:0] c_awlen; logic [2:0] c_awsize; logic [1:0] c_awburst; logic c_awvalid, c_awready;
logic [255:0] c_wdata; logic [31:0] c_wstrb; logic c_wlast, c_wvalid, c_wready; logic c_bvalid, c_bready; logic [1:0] c_bresp;
logic [31:0] z_beats_read, z_beats_written, c_beats_written, col_ovf, bresp_err; logic idle;
gs_lpddr_zc_emit #(.COLBASE(COLBASE), .ZBASE(ZBASE), .FB_PXW(FB_PXW), .FB_H(FB_H), .REQ_DEPTH(16), .COL_DEPTH(64)) dut (
.gs_clk(gs_clk), .gs_rst_n(gs_rst_n), .enable(enable),
.g_valid(g_valid), .g_ready(g_ready), .g_x(g_x), .g_y(g_y), .g_zq(g_zq), .g_zmsk(g_zmsk),
.g_ztest(g_ztest), .g_ztst(g_ztst), .g_color(g_color), .g_be(4'hF), .g_scene(g_scene),
.axi_clk(axi_clk), .axi_rst_n(axi_rst_n), .clear_start(clear_start), .clear_done(clear_done), .frame_drained(frame_drained),
.z_araddr(z_araddr), .z_arlen(z_arlen), .z_arsize(z_arsize), .z_arburst(z_arburst), .z_arvalid(z_arvalid), .z_arready(z_arready),
.z_rdata(z_rdata), .z_rresp(z_rresp), .z_rlast(z_rlast), .z_rvalid(z_rvalid), .z_rready(z_rready),
.z_awaddr(z_awaddr), .z_awlen(z_awlen), .z_awsize(z_awsize), .z_awburst(z_awburst), .z_awvalid(z_awvalid), .z_awready(z_awready),
.z_wdata(z_wdata), .z_wstrb(z_wstrb), .z_wlast(z_wlast), .z_wvalid(z_wvalid), .z_wready(z_wready),
.z_bvalid(z_bvalid), .z_bready(z_bready), .z_bresp(z_bresp),
.c_awaddr(c_awaddr), .c_awlen(c_awlen), .c_awsize(c_awsize), .c_awburst(c_awburst), .c_awvalid(c_awvalid), .c_awready(c_awready),
.c_wdata(c_wdata), .c_wstrb(c_wstrb), .c_wlast(c_wlast), .c_wvalid(c_wvalid), .c_wready(c_wready),
.c_bvalid(c_bvalid), .c_bready(c_bready), .c_bresp(c_bresp),
.z_beats_read(z_beats_read), .z_beats_written(z_beats_written), .c_beats_written(c_beats_written),
.col_ovf(col_ovf), .bresp_err(bresp_err), .idle(idle)
);
logic [15:0] lf=16'hBEEF; always_ff @(posedge axi_clk) lf<={lf[14:0], lf[15]^lf[13]^lf[12]^lf[10]};
// ---- Z LPDDR slave (256b, random backpressure) ----
logic [255:0] zmem [0:ZBEATS-1];
logic [31:0] zra; logic zrp; logic [3:0] zrd; logic [31:0] zwa; logic [255:0] zwd; logic zaw,zw; logic [3:0] zbd; logic zbp;
always_ff @(posedge axi_clk or negedge axi_rst_n) begin
if(!axi_rst_n) begin z_arready<=0;z_rvalid<=0;z_rlast<=0;z_rresp<=0;z_rdata<=0;zrp<=0;zrd<=0;
z_awready<=0;z_wready<=0;z_bvalid<=0;z_bresp<=0;zaw<=0;zw<=0;zbp<=0;zbd<=0; end
else begin
z_arready<=(!zrp&&!z_rvalid)?lf[0]:1'b0;
if(z_arvalid&&z_arready) begin zra<=z_araddr;zrp<=1;zrd<={1'b0,lf[3:1]};z_arready<=0; end
if(zrp&&!z_rvalid) begin if(zrd==0) begin z_rdata<=zmem[zra[5+:ZBW]];z_rresp<=0;z_rvalid<=1;z_rlast<=1;zrp<=0; end else zrd<=zrd-1; end
if(z_rvalid&&z_rready) begin z_rvalid<=0;z_rlast<=0; end
z_awready<=(!zaw)?lf[4]:1'b0; z_wready<=(!zw)?lf[5]:1'b0;
if(z_awvalid&&z_awready) begin zwa<=z_awaddr;zaw<=1;z_awready<=0; end
if(z_wvalid&&z_wready) begin zwd<=z_wdata;zw<=1;z_wready<=0; end
if(zaw&&zw&&!zbp&&!z_bvalid) begin zmem[zwa[5+:ZBW]]<=zwd;zbp<=1;zbd<={1'b0,lf[8:6]}; end
if(zbp&&!z_bvalid) begin if(zbd==0) begin z_bvalid<=1;z_bresp<=0;zbp<=0;zaw<=0;zw<=0; end else zbd<=zbd-1; end
if(z_bvalid&&z_bready) z_bvalid<=0;
end
end
// ---- Color LPDDR slave (256b, per-byte wstrb, random backpressure) ----
logic [255:0] cmem [0:CBEATS-1];
logic [31:0] cwa; logic [255:0] cwd; logic [31:0] cws; logic caw,cw; logic [3:0] cbd; logic cbp;
logic col_force_stall = 1'b0; // Ch357 (Codex) — PROLONGED px_ready=0: hard-stall the color AXI to fully back up
// u_c's FIFO -> col_px_ready drops -> the new zc_emit output register must HOLD.
always_ff @(posedge axi_clk or negedge axi_rst_n) begin
if(!axi_rst_n) begin c_awready<=0;c_wready<=0;c_bvalid<=0;c_bresp<=0;caw<=0;cw<=0;cbp<=0;cbd<=0; end
else begin
c_awready<=(!caw && !col_force_stall)?lf[9]:1'b0; c_wready<=(!cw && !col_force_stall)?lf[11]:1'b0;
if(c_awvalid&&c_awready) begin cwa<=c_awaddr;caw<=1;c_awready<=0; end
if(c_wvalid&&c_wready) begin cwd<=c_wdata;cws<=c_wstrb;cw<=1;c_wready<=0; end
if(caw&&cw&&!cbp&&!c_bvalid) begin
for(int b=0;b<32;b++) if(cws[b]) cmem[cwa[5+:CBW]][b*8+:8]<=cwd[b*8+:8];
cbp<=1;cbd<={1'b0,lf[14:12]};
end
if(cbp&&!c_bvalid) begin if(cbd==0) begin c_bvalid<=1;c_bresp<=0;cbp<=0;caw<=0;cw<=0; end else cbd<=cbd-1; end
if(c_bvalid&&c_bready) c_bvalid<=0;
end
end
// ---- disjoint-range monitors ----
always_ff @(posedge axi_clk) if(axi_rst_n) begin
if(z_arvalid && (z_araddr<ZBASE || z_araddr>=ZBASE+ZBEATS*32)) begin $error("Z AR %h out of range",z_araddr);errors++; end
if(z_awvalid && (z_awaddr<ZBASE || z_awaddr>=ZBASE+ZBEATS*32)) begin $error("Z AW %h out of range",z_awaddr);errors++; end
if(c_awvalid && (c_awaddr<COLBASE || c_awaddr>=COLBASE+CBEATS*32)) begin $error("C AW %h out of range",c_awaddr);errors++; end
end
// ---- golden ----
logic [15:0] zg [0:NPX-1]; logic [31:0] cg [0:NPX-1]; logic cw_seen [0:NPX-1];
logic [11:0] vx[0:NFRAG-1],vy[0:NFRAG-1]; logic [15:0] vz[0:NFRAG-1]; logic vzm[0:NFRAG-1]; logic [31:0] vc[0:NFRAG-1];
int fdrv;
function automatic logic [15:0] cl16(input logic [31:0] z); cl16=(|z[31:16])?16'hFFFF:z[15:0]; endfunction
// simple producer: drive one item, wait accept
task automatic push(input logic sc, input logic [11:0] x, input logic [11:0] y, input logic [15:0] zq,
input logic zm, input logic [1:0] zt, input logic [31:0] col);
@(negedge gs_clk);
g_valid<=1; g_scene<=sc; g_x<=x; g_y<=y; g_zq<=zq; g_zmsk<=zm; g_ztest<=1'b1; g_ztst<=zt; g_color<=col;
@(posedge gs_clk);
while(!(g_valid && g_ready)) @(posedge gs_clk);
@(negedge gs_clk); g_valid<=0;
// random gap
if(lf[7]) begin repeat(1+(lf[2:0])) @(posedge gs_clk); end
endtask
initial begin
errors=0; enable=0; clear_start=0; g_valid=0; g_scene=0; g_x=0; g_y=0; g_zq=0; g_zmsk=0; g_ztest=1; g_ztst=2'd2; g_color=0;
for(int i=0;i<NPX;i++) begin zg[i]=16'h0000; cg[i]=32'd0; cw_seen[i]=1'b0; end
for(int b=0;b<CBEATS;b++) cmem[b]=256'd0; // host preclears the color FB to 0 (so never-won pixels stay 0)
// precompute fragment vectors + golden (3 epochs over overlapping regions; Z spread; ~10% zmsk)
begin int f; f=0;
for(int ep=0;ep<NEP && f<NFRAG;ep++) begin
int rw,rh,ox,oy; rw=16+ep*4; rh=10+ep*2; ox=ep*3; oy=ep*1;
for(int n=0;n<NFRAG/NEP && f<NFRAG;n++) begin
int lx,ly; logic [31:0] zz; logic zm; logic [31:0] col;
lx=ox+({$random}%rw); ly=oy+({$random}%rh);
if(lx<0)lx=0; if(lx>=FB_PXW)lx=FB_PXW-1; if(ly<0)ly=0; if(ly>=FB_H)ly=FB_H-1;
case({$random}%3) 0: zz={$random}&32'h0000_FFFF; 1: zz={$random}&32'h0000_3FFF; default: zz={$random}&32'h00FF_FFFF; endcase
zm=(({$random}%10)==0); col={$random}|32'h0000_0001; // nonzero color to distinguish "written"
vx[f]=lx[11:0];vy[f]=ly[11:0];vz[f]=cl16(zz); vzm[f]=zm; vc[f]=col;
begin int idx; logic [15:0] sz; logic pass; idx=ly*FB_PXW+lx; sz=cl16(zz); pass=(sz>=zg[idx]);
if(pass) begin cg[idx]=col; cw_seen[idx]=1'b1; if(!zm) zg[idx]=sz; end
end
f=f+1;
end
end
end
gs_rst_n=0; axi_rst_n=0; repeat(6) @(posedge axi_clk); gs_rst_n=1; axi_rst_n=1; repeat(4) @(posedge axi_clk);
enable=1;
// preclear Z once at frame start
@(negedge axi_clk) clear_start=1; @(negedge axi_clk) clear_start=0;
begin int g=0; while(!clear_done && g<200000) begin @(posedge axi_clk); g++; end end
if(!clear_done) begin $error("[zc] preclear timeout");errors++; end
// drive 3 epochs, marker + wait frame_drained after each
fdrv=0;
// Ch357 (Codex) — PROLONGED px_ready=0 coverage: once a handful of fragments are in flight, hard-stall the color
// AXI for a long window so u_c's FIFO fully backs up and col_px_ready stays 0; the zc_emit output register must
// HOLD its pixel (no drop). Release and let it drain. The exact-framebuffer + col_ovf==0 checks below prove it.
fork
begin
while (fdrv < 12) @(posedge axi_clk);
col_force_stall = 1'b1; repeat(500) @(posedge axi_clk); col_force_stall = 1'b0;
end
join_none
for(int ep=0;ep<NEP;ep++) begin
int lo,hi; lo=ep*(NFRAG/NEP); hi=(ep+1)*(NFRAG/NEP);
for(int f=lo;f<hi;f++) begin push(1'b0, vx[f],vy[f],vz[f],vzm[f],2'd2,vc[f]); fdrv++; end
// end-of-scene marker
push(1'b1, 12'd0,12'd0,16'd0,1'b0,2'd2,32'd0);
begin int g=0; while(!frame_drained && g<400000) begin @(posedge axi_clk); g++; end end
if(!frame_drained) begin $error("[zc] epoch %0d: frame_drained never rose",ep);errors++; end
$display("[zc] epoch %0d drained (frame_drained=1) fdrv=%0d z_wr=%0d c_wr=%0d col_ovf=%0d",ep,fdrv,z_beats_written,c_beats_written,col_ovf);
repeat(30) @(posedge axi_clk);
end
// Fidelity gate: the SH3 darken sprites use TEST.ZTST=ALWAYS with a
// low incoming Z. Prove that mode survives the gs->axi FIFO instead
// of being collapsed to GEQUAL. ZMSK preserves the existing depth;
// the unmistakable color must nevertheless commit.
begin
int idx, g;
logic [15:0] z_before;
idx = 7*FB_PXW + 11;
z_before = zg[idx];
push(1'b0, 12'd11, 12'd7, 16'h0000, 1'b1, 2'd1, 32'h50A5_3CC3);
cg[idx] = 32'h50A5_3CC3; cw_seen[idx] = 1'b1;
g=0; while(frame_drained && g<2000) begin @(posedge axi_clk); g++; end
push(1'b1, 12'd0,12'd0,16'd0,1'b0,2'd2,32'd0);
g=0; while(!frame_drained && g<400000) begin @(posedge axi_clk); g++; end
if(!frame_drained) begin $error("[zc] ZTST=ALWAYS directed epoch never drained"); errors++; end
if(zg[idx] !== z_before) begin $error("[zc] directed ZMSK golden changed unexpectedly"); errors++; end
$display("[zc] directed ZTST=ALWAYS accepted low-Z color at (%0d,%0d), preserved Z=%04x",11,7,z_before);
end
// ---- final checks: Z LPDDR == golden ; color LPDDR == golden (written pixels) ----
repeat(40) @(posedge axi_clk);
for(int idx=0;idx<NPX;idx++) begin
logic [15:0] zs; zs=zmem[idx>>4][(idx[3:0])*16+:16];
if(zs!==zg[idx]) begin if(errors<12)$error("[zc] Z px%0d=%04x exp %04x",idx,zs,zg[idx]);errors++; end
end
for(int idx=0;idx<NPX;idx++) if(cw_seen[idx]) begin
logic [31:0] cs; cs=cmem[idx>>3][(idx[2:0])*32+:32];
if(cs!==cg[idx]) begin if(errors<12)$error("[zc] COLOR px%0d=%08x exp %08x",idx,cs,cg[idx]);errors++; end
end
// pixels never won by any fragment must have NO color written (still 0) — rejected-color suppression sanity
for(int idx=0;idx<NPX;idx++) if(!cw_seen[idx]) begin
logic [31:0] cs; cs=cmem[idx>>3][(idx[2:0])*32+:32];
if(cs!==32'd0) begin if(errors<12)$error("[zc] px%0d never-won but color=%08x (suppression fail)",idx,cs);errors++; end
end
if(col_ovf!==0) begin $error("[zc] col_ovf=%0d",col_ovf);errors++; end
if(bresp_err!==0) begin $error("[zc] bresp_err=%0d",bresp_err);errors++; end
if(fdrv!==NFRAG) begin $error("[zc] fdrv=%0d exp %0d",fdrv,NFRAG);errors++; end
$display("[zc] frags=%0d z_read=%0d z_wr=%0d c_wr=%0d col_ovf=%0d bresp_err=%0d errors=%0d",
fdrv,z_beats_read,z_beats_written,c_beats_written,col_ovf,bresp_err,errors);
if(errors==0) $display("[tb_gs_lpddr_zc_emit] PASS"); else $display("[tb_gs_lpddr_zc_emit] FAIL");
$finish;
end
initial begin #60000000; $error("[tb_gs_lpddr_zc_emit] TIMEOUT"); $finish; end
endmodule : tb_gs_lpddr_zc_emit
+71 -2
View File
@@ -20,14 +20,16 @@ module tb_gs_prim_list_feeder;
logic [63:0] gif_reg_data;
localparam logic [7:0] REG_PRIM=8'h00, REG_RGBAQ=8'h01, REG_UV=8'h03, REG_XYZ2=8'h05,
REG_TEX0=8'h06, REG_ALPHA=8'h42, REG_TEST=8'h47, REG_FRAME=8'h4C, REG_ZBUF=8'h4E;
REG_TEX0=8'h06, REG_ALPHA=8'h42, REG_TEST=8'h47, REG_CLAMP=8'h48,
REG_FRAME=8'h4C, REG_ZBUF=8'h4E;
// ---- staging RAM (synchronous read) ----
logic [63:0] stg [0:127];
always_ff @(posedge clk) stg_rd_data <= stg[stg_rd_addr[6:0]];
localparam logic [63:0] D_FRAME=64'h00F0, D_ALPHA=64'h00A0, D_TEST=64'h0070,
D_ZBUF=64'h00B0, D_TEX0=64'h0060, D_PRIM=64'h0003;
D_ZBUF=64'h00B0, D_TEX0=64'h0060, D_CLAMP=64'h0000_07fc_007f_c00a,
D_PRIM=64'h0003;
function automatic logic [63:0] vtx_sentinel(input int ti, input int v, input int w);
vtx_sentinel = 64'h1000_0000 * (ti+1) + 64'h100 * v + w;
endfunction
@@ -115,6 +117,73 @@ module tb_gs_prim_list_feeder;
wait (done==1'b1); repeat(2) @(posedge clk);
if (cap_n != EMITS_TOTAL) begin $error("[feeder] after release emitted %0d (expected %0d)", cap_n, EMITS_TOTAL); errors++; end
// ===== PROOF 3 — Ch402 optional CLAMP header and word-8 vertex base =====
cap_n=0; exp_n=0;
stg[0]=NTRI|(64'h1<<34); stg[6]=D_CLAMP; stg[7]=D_PRIM;
for (int t=0;t<NTRI;t++)
for (int v=0;v<3;v++) begin
stg[8 + 9*t + 3*v + 0] = vtx_sentinel(t,v,0);
stg[8 + 9*t + 3*v + 1] = vtx_sentinel(t,v,1);
stg[8 + 9*t + 3*v + 2] = vtx_sentinel(t,v,2);
end
push_exp(REG_FRAME,D_FRAME); push_exp(REG_ALPHA,D_ALPHA); push_exp(REG_TEST,D_TEST);
push_exp(REG_ZBUF,D_ZBUF); push_exp(REG_TEX0,D_TEX0); push_exp(REG_CLAMP,D_CLAMP);
for (int t=0;t<NTRI;t++) begin
push_exp(REG_PRIM,D_PRIM);
for (int v=0;v<3;v++) begin
push_exp(REG_RGBAQ, vtx_sentinel(t,v,0));
push_exp(REG_UV, vtx_sentinel(t,v,1));
push_exp(REG_XYZ2, vtx_sentinel(t,v,2));
end
end
@(negedge clk); start=1; @(negedge clk); start=0;
wait (done==1'b1); repeat(2) @(posedge clk);
if (cap_n != exp_n) begin $error("[feeder-clamp] emit count %0d, expected %0d", cap_n, exp_n); errors++; end
for (int i=0;i<exp_n && i<cap_n;i++)
if (cap_num[i]!==exp_num[i] || cap_data[i]!==exp_data[i]) begin
$error("[feeder-clamp] emit %0d: got (num=%02x data=%016x) exp (num=%02x data=%016x)",
i, cap_num[i], cap_data[i], exp_num[i], exp_data[i]); errors++;
end
// ===== PROOF 4 — GS per-vertex FOG: PRIM.FGE=1 tags the completing
// vertex commit as XYZF2 (0x04) instead of XYZ2 (0x05); the RGBAQ/UV
// words and the commit DATA (fog byte included) are byte-unchanged. =====
cap_n=0; exp_n=0;
// Restore the legacy (non-clamp) header layout and set PRIM.FGE (bit 5).
for (int i=0;i<128;i++) stg[i] = 64'd0;
stg[0]=NTRI;
stg[1]=D_FRAME; stg[2]=D_ALPHA; stg[3]=D_TEST; stg[4]=D_ZBUF; stg[5]=D_TEX0;
stg[6]=D_PRIM | (64'h1<<5); // PRIM + FGE
for (int t=0;t<NTRI;t++)
for (int v=0;v<3;v++) begin
stg[7 + 9*t + 3*v + 0] = vtx_sentinel(t,v,0);
stg[7 + 9*t + 3*v + 1] = vtx_sentinel(t,v,1);
// commit word carries a distinct fog byte in [63:56].
stg[7 + 9*t + 3*v + 2] = vtx_sentinel(t,v,2) | (64'h9A<<56);
end
push_exp(REG_FRAME,D_FRAME); push_exp(REG_ALPHA,D_ALPHA); push_exp(REG_TEST,D_TEST);
push_exp(REG_ZBUF,D_ZBUF); push_exp(REG_TEX0,D_TEX0);
for (int t=0;t<NTRI;t++) begin
push_exp(REG_PRIM, D_PRIM | (64'h1<<5));
for (int v=0;v<3;v++) begin
push_exp(REG_RGBAQ, vtx_sentinel(t,v,0));
push_exp(REG_UV, vtx_sentinel(t,v,1));
push_exp(8'h04 /*XYZF2*/, vtx_sentinel(t,v,2) | (64'h9A<<56));
end
end
@(negedge clk); start=1; @(negedge clk); start=0;
wait (done==1'b1); repeat(2) @(posedge clk);
if (cap_n != exp_n) begin $error("[feeder-fge] emit count %0d, expected %0d", cap_n, exp_n); errors++; end
for (int i=0;i<exp_n && i<cap_n;i++)
if (cap_num[i]!==exp_num[i] || cap_data[i]!==exp_data[i]) begin
$error("[feeder-fge] emit %0d: got (num=%02x data=%016x) exp (num=%02x data=%016x)",
i, cap_num[i], cap_data[i], exp_num[i], exp_data[i]); errors++;
end
// Explicitly confirm the PRIM word kept its FGE bit and the commit reg is 0x04.
if ((cap_data[5] & (64'h1<<5)) == 0) begin $error("[feeder-fge] PRIM lost FGE bit"); errors++; end
if (cap_num[8] !== 8'h04) begin $error("[feeder-fge] completing vtx reg=%02x, expected 04 (XYZF2)", cap_num[8]); errors++; end
if (cap_data[8][63:56] !== 8'h9A) begin $error("[feeder-fge] fog byte lost: got %02x", cap_data[8][63:56]); errors++; end
$display("[tb_gs_prim_list_feeder] emits=%0d held@%0d errors=%0d", EMITS_TOTAL, EMITS_BEFORE_FIRST_COMPLETING, errors);
if (errors==0) $display("[tb_gs_prim_list_feeder] PASS");
else $display("[tb_gs_prim_list_feeder] FAIL");
+71 -13
View File
@@ -25,6 +25,7 @@ module tb_gs_psmt8_alpha_sprite;
localparam logic [5:0] PSMCT32 = 6'h00;
localparam logic [5:0] PSMT8 = 6'h13;
localparam logic [5:0] PSMT4 = 6'h14;
logic clk;
logic rst_n;
@@ -69,6 +70,8 @@ module tb_gs_psmt8_alpha_sprite;
logic [3:0] raster_pixel_be_q;
logic [31:0] raster_pixel_mask_q;
logic [5:0] raster_pixel_psm_q;
logic raster_pixel_abe_q;
logic [16:0] raster_pixel_alpha_q;
logic raster_active;
logic raster_overflow;
logic raster_fifo_full;
@@ -139,6 +142,8 @@ module tb_gs_psmt8_alpha_sprite;
.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_active(raster_active),
.raster_overflow(raster_overflow),
.raster_fifo_full(raster_fifo_full),
@@ -201,20 +206,24 @@ module tb_gs_psmt8_alpha_sprite;
R_TEX0_1=8'h06, R_ALPHA_1=8'h42, R_FRAME_1=8'h4C;
localparam logic [63:0] PRIM_SPR_TEX_ABE = 64'd6 | (64'd1<<4) | (64'd1<<6);
localparam logic [63:0] ALPHA_SRCOVER = 64'h0000_0000_0000_0044;
localparam logic [63:0] ALPHA_DARKEN = 64'h0000_0000_0000_0046;
localparam logic [63:0] FRAME_1_FB0 = 64'h0000_0000_0001_0000; // FBP=0 FBW=1 PSMCT32
localparam int SPRITE_W = 4, SPRITE_H = 4;
localparam int FB0_STRIDE = 64*4;
localparam logic [63:0] TINT_RGBAQ = 64'h0000_0000_10FF_8040; // A ignored; R*0x40 G*0x80 B*0xFF
localparam logic [63:0] TINT_RGBAQ = 64'h0000_0000_10FF_8040; // MODULATE: A*0x10 R*0x40 G*0x80 B*0xFF
localparam logic [31:0] BG_PIXEL = 32'hFF20_2020;
// ---- PSMT8 texture (4x4 indices) at TBP0=8, TBW=1; PSMCT32 control texture at TBP0=16 ----
// ---- Indexed textures plus PSMCT32 control ----
localparam logic [63:0] TEX0_T8 = 64'd8 | (64'd1<<14) | (64'(PSMT8)<<20)
| (64'd2<<26) | (64'd2<<30) | (64'd1<<34); // TCC=1 -> texel(CLUT) alpha
localparam logic [63:0] TEX0_T4 = 64'd24 | (64'd1<<14) | (64'(PSMT4)<<20)
| (64'd2<<26) | (64'd2<<30) | (64'd1<<34);
localparam logic [63:0] TEX0_C32 = 64'd16 | (64'd1<<14) | (64'd0<<20)
| (64'd2<<26) | (64'd2<<30) | (64'd1<<34);
localparam logic [31:0] T8_BASE = 32'd8 * 32'd256; // 2048
localparam logic [31:0] C32_BASE = 32'd16 * 32'd256; // 4096
localparam logic [31:0] T4_BASE = 32'd24 * 32'd256; // 6144
localparam int ROW_TEXELS = 64; // TBW*64
// distinct index per pixel so a byte-lane error is caught; cycles all 4 palette entries incl. idx0.
@@ -250,15 +259,16 @@ module tb_gs_psmt8_alpha_sprite;
endfunction
// expected sprite pixel from an arbitrary ABGR texel (CLUT entry or PSMCT32 word).
function automatic logic [31:0] expected_from_texel(input logic [31:0] t);
logic [7:0] cs_r, cs_g, cs_b;
logic [7:0] cs_r, cs_g, cs_b, cs_a;
begin
cs_r = sw_mod8(t[7:0], TINT_RGBAQ[7:0]);
cs_g = sw_mod8(t[15:8], TINT_RGBAQ[15:8]);
cs_b = sw_mod8(t[23:16], TINT_RGBAQ[23:16]);
expected_from_texel = {t[31:24],
sw_blend(cs_b, BG_PIXEL[23:16], t[31:24]),
sw_blend(cs_g, BG_PIXEL[15:8], t[31:24]),
sw_blend(cs_r, BG_PIXEL[7:0], t[31:24])};
cs_a = sw_mod8(t[31:24], TINT_RGBAQ[31:24]);
expected_from_texel = {cs_a,
sw_blend(cs_b, BG_PIXEL[23:16], cs_a),
sw_blend(cs_g, BG_PIXEL[15:8], cs_a),
sw_blend(cs_r, BG_PIXEL[7:0], cs_a)};
end
endfunction
@@ -283,11 +293,12 @@ module tb_gs_psmt8_alpha_sprite;
task automatic force_byte(input logic [31:0] a, input logic [7:0] d); u_vram.mem[a] = d; endtask
// ---- a textured-alpha SPRITE at FB (ox,oy), bound to tex0 ----
task automatic draw_sprite(input logic [63:0] tex0, input int ox, input int oy);
task automatic draw_sprite(input logic [63:0] tex0, input logic [63:0] alpha,
input int ox, input int oy);
drive_reg(R_PRIM, PRIM_SPR_TEX_ABE);
drive_reg(R_FRAME_1, FRAME_1_FB0);
drive_reg(R_TEX0_1, tex0);
drive_reg(R_ALPHA_1, ALPHA_SRCOVER);
drive_reg(R_ALPHA_1, alpha);
drive_reg(R_RGBAQ, TINT_RGBAQ);
drive_reg(R_UV, uv_data(0, 0));
drive_reg(R_XYZ2, xyz2_data(12'(ox), 12'(oy)));
@@ -308,9 +319,30 @@ module tb_gs_psmt8_alpha_sprite;
overlap_count <= overlap_count + 1;
end
// The external LPDDR ROP must receive architectural ABE/ALPHA metadata even
// when the legacy internal-BRAM sprite blender does not implement that mode.
// x=24 is the authentic SH3 destination-darken control; the other sprites
// retain the established source-over metadata.
int lpddr_meta_count; initial lpddr_meta_count = 0;
always_ff @(posedge clk)
if (rst_n && raster_pixel_emit) begin
logic [16:0] exp_alpha;
exp_alpha = (raster_pixel_x_q >= 12'd24)
? {1'b1, 2'd2, 2'd1, 2'd0, 2'd1, 8'd0} // ALPHA=0x46
: {1'b1, 2'd0, 2'd1, 2'd0, 2'd1, 8'd0}; // ALPHA=0x44
if (!raster_pixel_abe_q || raster_pixel_alpha_q !== exp_alpha) begin
$error("LPDDR alpha metadata x=%0d got abe=%0b alpha=%05x exp=%05x",
raster_pixel_x_q, raster_pixel_abe_q, raster_pixel_alpha_q, exp_alpha);
errors++;
end
lpddr_meta_count++;
end
int errors; initial errors = 0;
integer x, y; logic [31:0] got, exp;
localparam int C32_OX = 8; // PSMCT32 control sprite at FB x=8..11
localparam int T4_OX = 16; // PSMT4 alpha sprite at FB x=16..19
localparam int DARK_OX = 24; // PSMCT32 ALPHA=0x46 LPDDR-sideband control
initial begin
rst_n=1'b0; gif_reg_wr_en=1'b0; gif_reg_num=0; gif_reg_data=0; vram_read_addr=0;
@@ -318,25 +350,39 @@ module tb_gs_psmt8_alpha_sprite;
repeat (4) @(posedge clk); rst_n=1'b1; repeat (2) @(posedge clk);
// PSMT8 index texture (1 B/texel), PSMCT32 control texture, and the dest BG under BOTH sprites.
// PSMT8 (byte indices), PSMT4 (packed nibble indices), PSMCT32
// control texture, and destination BG under all three sprites.
for (y=0; y<SPRITE_H; y++) for (x=0; x<SPRITE_W; x++) begin
force_byte(T8_BASE + (y*ROW_TEXELS) + x, idx_pattern(x,y));
force_word(C32_BASE + ((y*ROW_TEXELS)+x)*4, c32_texel(x,y));
force_word((y*FB0_STRIDE) + (x*4), BG_PIXEL); // FB region for PSMT8 sprite
force_word((y*FB0_STRIDE) + ((C32_OX+x)*4), BG_PIXEL); // FB region for PSMCT32 sprite
force_word((y*FB0_STRIDE) + ((T4_OX+x)*4), BG_PIXEL); // FB region for PSMT4 sprite
force_word((y*FB0_STRIDE) + ((DARK_OX+x)*4), BG_PIXEL); // FB region for darken control
end
for (y=0; y<SPRITE_H; y++) for (x=0; x<SPRITE_W; x+=2)
force_byte(T4_BASE + ((y*ROW_TEXELS+x)>>1),
(idx_pattern(x+1,y)<<4) | idx_pattern(x,y));
// Program the CLUT (board: clut_loader_stub on a CLD!=0 TEX0; here TB-direct, same write port).
for (int i=0; i<4; i++) clut_write(8'(i), palette(8'(i)));
// ===== PSMT8 CLUT alpha sprite =====
draw_sprite(TEX0_T8, 0, 0);
draw_sprite(TEX0_T8, ALPHA_SRCOVER, 0, 0);
// ===== PSMCT32 control alpha sprite (param ON -> must stay green) =====
draw_sprite(TEX0_C32, C32_OX, 0);
draw_sprite(TEX0_C32, ALPHA_SRCOVER, C32_OX, 0);
// ===== PSMT4 CLUT alpha sprite (Ch408) =====
draw_sprite(TEX0_T4, ALPHA_SRCOVER, T4_OX, 0);
// The BRAM path deliberately does not blend this generic mode; this
// control exists to prove the external ROP sideband is nevertheless live.
draw_sprite(TEX0_C32, ALPHA_DARKEN, DARK_OX, 0);
// PROOF 4 — read2 never overlapped.
if (overlap_count != 0) begin
$error("read2 OVERLAP cycles=%0d — invariant VIOLATED", overlap_count); errors++;
end
if (lpddr_meta_count != 4*SPRITE_W*SPRITE_H) begin
$error("LPDDR metadata count got=%0d exp=%0d", lpddr_meta_count, 4*SPRITE_W*SPRITE_H); errors++;
end
// PROOF 1/2/3 — PSMT8 pixels == source-over(CLUT[idx]*tint, BG), As from the CLUT entry.
for (y=0; y<SPRITE_H; y++) for (x=0; x<SPRITE_W; x++) begin
@@ -357,7 +403,19 @@ module tb_gs_psmt8_alpha_sprite;
end
end
$display("[tb_gs_psmt8_alpha_sprite] PSMT8+PSMCT32 sprites drawn, read2_overlap=%0d errors=%0d", overlap_count, errors);
// Ch408 — packed-nibble select, CLUT lookup, texel-alpha blend, and
// registered read2 alignment must match the same independent oracle.
for (y=0; y<SPRITE_H; y++) for (x=0; x<SPRITE_W; x++) begin
logic [7:0] idx; logic [31:0] t4;
idx = idx_pattern(x,y) & 8'h0F; t4 = palette(idx);
vram_word((y*FB0_STRIDE)+((T4_OX+x)*4), got);
exp = expected_from_texel(t4);
if (got !== exp) begin
$error("[PSMT4] FB(%0d,%0d) got=%08x exp=%08x idx=0x%01x clut=%08x", x,y,got,exp,idx[3:0],t4); errors++;
end
end
$display("[tb_gs_psmt8_alpha_sprite] PSMT8+PSMT4+PSMCT32 sprites drawn, read2_overlap=%0d errors=%0d", overlap_count, errors);
if (errors==0) begin $display("[tb_gs_psmt8_alpha_sprite] PASS"); $finish; end
else $fatal(1, "[tb_gs_psmt8_alpha_sprite] FAIL (%0d errors)", errors);
end
+92 -18
View File
@@ -7,8 +7,10 @@
// Two triangles through gs_stub (PERSPECTIVE_CORRECT=1, registered read2 = board model):
// 1. AFFINE : PRIM=TRI|TME, FST=1 per-vertex UV (UV==screen XY -> affine u=x,v=y). Uses the affine DDA.
// 2. PERSPECTIVE: PRIM=TRI|TME, FST=0 per-vertex ST + RGBAQ.Q (constant Q=1 -> ST==XY). Uses gs_persp_uv.
// Both sample a PSMT8 index texture through clut_stub. PASS: every covered interior pixel's emitted color
// == CLUT[index(x,y)] (DECAL), for BOTH triangles; PSMCT32 control triangle stays correct.
// Both sample a PSMT8 index texture through clut_stub. The perspective case
// enables the serialized palette-bilinear path and independently checks that
// every covered sample is emitted exactly once; the affine case retains the
// exact CLUT[index(x,y)] color check.
`timescale 1ns/1ps
@@ -31,7 +33,7 @@ module tb_gs_psmt8_clut_triangle;
logic [63:0] pixel_color_q; logic [8:0] pixel_fbp_q; logic [5:0] pixel_fbw_q,pixel_psm_q; logic [31:0] pixel_fb_addr_q;
logic raster_pixel_emit; logic [31:0] raster_pixel_emit_count; logic [11:0] raster_pixel_x_q,raster_pixel_y_q;
logic [63:0] raster_pixel_color_q; logic [31:0] raster_pixel_fb_addr_q; logic [3:0] raster_pixel_be_q;
logic [31:0] raster_pixel_mask_q; logic [5:0] raster_pixel_psm_q;
logic [31:0] raster_pixel_mask_q; logic [5:0] raster_pixel_psm_q; logic [16:0] raster_pixel_alpha_q;
logic raster_active,raster_overflow,raster_fifo_full,raster_degenerate;
logic tex_rd_en; logic [31:0] tex_rd_addr,tex_rd_data;
logic fb_rd_en; logic [31:0] fb_rd_addr,fb_rd_data;
@@ -40,6 +42,7 @@ module tb_gs_psmt8_clut_triangle;
logic [63:0] gs_ev_arg0,gs_ev_arg1,gs_ev_arg2,gs_ev_arg3; logic [31:0] gs_ev_flags;
gs_stub #(.PERSPECTIVE_CORRECT(1'b1),
.BILINEAR_ENABLE(1'b1), .PALETTE_BILINEAR(1'b1),
.TEX_RD_REGISTERED(1'b1), .FB_RD_REGISTERED(1'b1), .Z_RD_REGISTERED(1'b1)) u_gs (
.clk(clk), .rst_n(rst_n),
.reg_wr_en(1'b0), .reg_wr_addr(16'd0), .reg_wr_data(64'd0),
@@ -62,6 +65,7 @@ module tb_gs_psmt8_clut_triangle;
.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_be_q(raster_pixel_be_q),
.raster_pixel_mask_q(raster_pixel_mask_q), .raster_pixel_psm_q(raster_pixel_psm_q),
.raster_pixel_alpha_q(raster_pixel_alpha_q),
.raster_active(raster_active), .raster_overflow(raster_overflow), .raster_fifo_full(raster_fifo_full), .raster_degenerate(raster_degenerate),
.tex_rd_en(tex_rd_en), .tex_rd_addr(tex_rd_addr), .tex_rd_data(tex_rd_data),
.fb_rd_en(fb_rd_en), .fb_rd_addr(fb_rd_addr), .fb_rd_data(fb_rd_data),
@@ -87,7 +91,7 @@ module tb_gs_psmt8_clut_triangle;
always_ff @(posedge clk) rd2_data_reg <= rd2_data;
assign tex_rd_data = rd2_data_reg; assign fb_rd_data = rd2_data_reg;
localparam logic [7:0] R_PRIM=8'h00,R_RGBAQ=8'h01,R_ST=8'h02,R_UV=8'h03,R_XYZ2=8'h05,R_TEX0_1=8'h06,R_FRAME_1=8'h4C;
localparam logic [7:0] R_PRIM=8'h00,R_RGBAQ=8'h01,R_ST=8'h02,R_UV=8'h03,R_XYZ2=8'h05,R_TEX0_1=8'h06,R_ALPHA_1=8'h42,R_FRAME_1=8'h4C;
localparam logic [63:0] PRIM_TRI_TEX = 64'd3 | (64'd1<<4); // TRIANGLE + TME, FST=0 (ST)
localparam logic [63:0] PRIM_TRI_TEX_FST = PRIM_TRI_TEX | (64'd1<<8); // + FST=1 (UV affine)
localparam logic [63:0] FRAME_1_FB0 = 64'h0000_0000_0001_0000; // FBP=0 FBW=1 PSMCT32
@@ -96,6 +100,7 @@ module tb_gs_psmt8_clut_triangle;
// TEX0: PSMT8, TBP0=8, TBW=1, TW=4 TH=4 (16x16), TFX=DECAL(1), TCC=1
localparam logic [63:0] TEX0_T8 = {14'd0/*pad*/, TBP0} | (64'd1<<14) | (64'(PSMT8)<<20)
| (64'd4<<26) | (64'd4<<30) | (64'd1<<34) | (64'd1<<35);
localparam logic [63:0] TEX0_T8_MOD = TEX0_T8 & ~(64'd3<<35);
function automatic logic [7:0] idx_pat(input int x, input int y); return 8'(((x*3) ^ (y*5) + x + y) & 8'hFF); endfunction
function automatic logic [31:0] pal(input logic [7:0] i); // distinct ABGR per index
@@ -146,6 +151,26 @@ module tb_gs_psmt8_clut_triangle;
cap_px [raster_pixel_y_q][raster_pixel_x_q] <= raster_pixel_color_q[31:0];
end
// LPDDR ROP metadata is separate from the legacy internal-BRAM alpha path.
// Capture a direct-color-style ABE triangle control so the production
// perspective emit cannot silently degrade to an opaque copy.
bit alpha_cap_armed; int alpha_emit_count, alpha_meta_bad;
always_ff @(posedge clk)
if (rst_n && alpha_cap_armed && raster_pixel_emit) begin
alpha_emit_count <= alpha_emit_count + 1;
if (raster_pixel_alpha_q !== {1'b1,2'd2,2'd1,2'd0,2'd1,8'h80})
alpha_meta_bad <= alpha_meta_bad + 1;
end
bit mod_cap_armed; int mod_emit_count, mod_rgb_bad, mod_alpha_bad;
always_ff @(posedge clk)
if (rst_n && mod_cap_armed && raster_pixel_emit) begin
mod_emit_count <= mod_emit_count + 1;
if (raster_pixel_color_q[23:0] !== 24'd0)
mod_rgb_bad <= mod_rgb_bad + 1;
if (raster_pixel_color_q[31:24] !== 8'h50)
mod_alpha_bad <= mod_alpha_bad + 1;
end
int errors, inside_ok; integer x,y;
// draw one TME triangle (vertices in vx*/vy*); fst=1 -> UV, fst=0 -> ST. check covered px == CLUT[idx(x,y)].
@@ -184,7 +209,8 @@ module tb_gs_psmt8_clut_triangle;
endtask
initial begin
errors=0; inside_ok=0; cap_armed=1'b0;
errors=0; inside_ok=0; cap_armed=1'b0; alpha_cap_armed=1'b0; alpha_emit_count=0; alpha_meta_bad=0;
mod_cap_armed=1'b0; mod_emit_count=0; mod_rgb_bad=0; mod_alpha_bad=0;
rst_n=1'b0; gif_reg_wr_en=1'b0; gif_reg_num=0; gif_reg_data=0; vram_read_addr=0; clut_we=1'b0; clut_widx=0; clut_wdata=0;
repeat (4) @(posedge clk); rst_n=1'b1; repeat (2) @(posedge clk);
@@ -200,30 +226,78 @@ module tb_gs_psmt8_clut_triangle;
// path = covered pixels show MANY distinct CLUT colors (varied indices) + every color is a real CLUT
// entry. v0:(2,1)u0v0 Q1 v1:(13,2)u15v0 Q2 v2:(5,7)u0v15 Q1 -> u=S/Q sweeps 0..15, v 0..15.
begin
logic [31:0] seen[0:255]; int nseen; bit all_clut;
logic [31:0] seen[0:255]; int nseen; bit all_known;
for (y=0;y<16;y++) for (x=0;x<16;x++) begin covered[y][x]=1'b0; cap_px[y][x]=0; end
cap_armed=1'b1;
drive_reg(R_PRIM, PRIM_TRI_TEX); drive_reg(R_FRAME_1, FRAME_1_FB0); drive_reg(R_TEX0_1, TEX0_T8);
drive_reg(R_RGBAQ, rgbaqQ(0,0,0,1)); drive_reg(R_ST, st_data(0, 0)); drive_reg(R_XYZ2, xyz2_data(2,1));
drive_reg(R_RGBAQ, rgbaqQ(0,0,0,2)); drive_reg(R_ST, st_data(30,0)); drive_reg(R_XYZ2, xyz2_data(13,2));
drive_reg(R_RGBAQ, rgbaqQ(0,0,0,1)); drive_reg(R_ST, st_data(0, 15)); drive_reg(R_XYZ2, xyz2_data(5,7));
drive_idle(); repeat (400) @(posedge clk); cap_armed=1'b0; @(posedge clk);
nseen=0; all_clut=1'b1;
drive_idle(); repeat (2000) @(posedge clk); cap_armed=1'b0; @(posedge clk);
nseen=0; all_known=1'b1;
begin
int expected_covered, actual_covered, missing_covered;
expected_covered=0; actual_covered=0; missing_covered=0;
for (y=0;y<16;y++) for (x=0;x<16;x++) begin
if (ref_inside(x,y)) begin
expected_covered++;
if (!covered[y][x]) missing_covered++;
end
if (covered[y][x]) actual_covered++;
end
$display("[tb_gs_psmt8_clut_triangle] PERSP coverage: actual=%0d expected=%0d missing=%0d",
actual_covered, expected_covered, missing_covered);
if (missing_covered != 0) begin
$error("PERSP: %0d/%0d covered samples missing (serialized sampler skipped pixels)",
missing_covered, expected_covered);
errors++;
end
end
for (y=0;y<16;y++) for (x=0;x<16;x++) if (covered[y][x]) begin
bit f; bit found_idx; logic [31:0] pc;
bit f;
f=1'b0; for (int k=0;k<nseen;k++) if (seen[k]===cap_px[y][x]) f=1'b1;
if (!f && nseen<256) begin seen[nseen]=cap_px[y][x]; nseen++; end
found_idx=1'b0;
for (int i=0;i<256;i++) begin pc=pal(8'(i)); if (pc[23:0]===cap_px[y][x][23:0]) found_idx=1'b1; end
if (!found_idx) all_clut=1'b0;
if (^cap_px[y][x] === 1'bx) all_known=1'b0;
end
// Standalone direct-drive perspective tris under-interpolate the persp UV (affects PSMCT32 too —
// confirmed by isolation: the texel-stage UV barely varies without the feeder S1-path setup the
// Ch342 silicon demo uses). So here we prove the PSMT8->CLUT CHAIN on the persp path (every covered
// pixel is a real CLUT entry); RICH perspective interpolation is proven by the feeder integration.
$display("[tb_gs_psmt8_clut_triangle] PERSP(chain): distinct CLUT colors=%0d all_from_CLUT=%b (rich persp interp -> feeder integration TB)", nseen, all_clut);
// The enabled palette-bilinear sampler legitimately blends CLUT
// colors, so blended outputs need not equal a single palette entry.
$display("[tb_gs_psmt8_clut_triangle] PERSP(chain): distinct colors=%0d all_known=%b",
nseen, all_known);
if (nseen < 2) begin $error("PERSP: %0d distinct colors — persp path produced no CLUT variety", nseen); errors++; end
if (!all_clut) begin $error("PERSP: a covered pixel is NOT a CLUT entry — PSMT8->CLUT broken on persp path"); errors++; end
if (!all_known) begin $error("PERSP: covered pixel contains unknown data"); errors++; end
end
// Authentic SH3 darken selectors: A=ZERO, B=Cd, C=As, D=Cd,
// FIX=0x80. The triangle is perspective because the board fixture
// lowers each source sprite to ST/Q triangles.
alpha_emit_count=0; alpha_meta_bad=0; alpha_cap_armed=1'b1;
drive_reg(R_ALPHA_1, 64'h0000_0080_0000_0046);
drive_reg(R_PRIM, PRIM_TRI_TEX | (64'd1<<6));
drive_reg(R_FRAME_1, FRAME_1_FB0); drive_reg(R_TEX0_1, TEX0_T8);
drive_reg(R_RGBAQ, rgbaqQ(0,0,0,1)); drive_reg(R_ST, st_data(0,0)); drive_reg(R_XYZ2, xyz2_data(2,1));
drive_reg(R_RGBAQ, rgbaqQ(0,0,0,1)); drive_reg(R_ST, st_data(15,0)); drive_reg(R_XYZ2, xyz2_data(13,2));
drive_reg(R_RGBAQ, rgbaqQ(0,0,0,1)); drive_reg(R_ST, st_data(0,15)); drive_reg(R_XYZ2, xyz2_data(5,7));
drive_idle(); repeat (500) @(posedge clk); alpha_cap_armed=1'b0; @(posedge clk);
$display("[tb_gs_psmt8_clut_triangle] TRI ABE metadata: emits=%0d bad=%0d", alpha_emit_count, alpha_meta_bad);
if (alpha_emit_count==0 || alpha_meta_bad!=0) begin
$error("TRI ABE metadata missing/corrupt: emits=%0d bad=%0d", alpha_emit_count, alpha_meta_bad); errors++;
end
// TFX=MODULATE with a black vertex tint must emit black RGB, not the
// DECAL texel. This is the other half of the SH3 darken source packet.
// Give every sampled texel A=0x80 and use vertex A=0x50. GS
// MODULATE+TCC must therefore emit As=0x50.
for (int i=0;i<256;i++) clut_write(8'(i), (pal(8'(i)) & 32'h00ff_ffff) | 32'h8000_0000);
mod_emit_count=0; mod_rgb_bad=0; mod_alpha_bad=0; mod_cap_armed=1'b1;
drive_reg(R_PRIM, PRIM_TRI_TEX); drive_reg(R_FRAME_1, FRAME_1_FB0); drive_reg(R_TEX0_1, TEX0_T8_MOD);
drive_reg(R_RGBAQ, {f32(1),8'h50,24'd0}); drive_reg(R_ST, st_data(0,0)); drive_reg(R_XYZ2, xyz2_data(2,1));
drive_reg(R_RGBAQ, {f32(1),8'h50,24'd0}); drive_reg(R_ST, st_data(15,0)); drive_reg(R_XYZ2, xyz2_data(13,2));
drive_reg(R_RGBAQ, {f32(1),8'h50,24'd0}); drive_reg(R_ST, st_data(0,15)); drive_reg(R_XYZ2, xyz2_data(5,7));
drive_idle(); repeat (500) @(posedge clk); mod_cap_armed=1'b0; @(posedge clk);
$display("[tb_gs_psmt8_clut_triangle] TRI MODULATE black/A: emits=%0d bad_rgb=%0d bad_alpha=%0d", mod_emit_count, mod_rgb_bad, mod_alpha_bad);
if (mod_emit_count==0 || mod_rgb_bad!=0 || mod_alpha_bad!=0) begin
$error("TRI MODULATE ignored/misaligned: emits=%0d bad_rgb=%0d bad_alpha=%0d", mod_emit_count, mod_rgb_bad, mod_alpha_bad); errors++;
end
if (raster_overflow) begin $error("raster_overflow"); errors++; end
+98 -17
View File
@@ -30,7 +30,10 @@
`timescale 1ns/1ps
module tb_gs_raster_pipeline;
module tb_gs_raster_pipeline #(
parameter bit USE_SUBPIXEL = 1'b0,
parameter int GRAD_CYCLES = 1
);
logic clk;
logic rst_n;
@@ -64,6 +67,7 @@ module tb_gs_raster_pipeline;
logic [11:0] raster_pixel_x_q, raster_pixel_y_q;
logic [63:0] raster_pixel_color_q;
logic [31:0] raster_pixel_fb_addr_q;
logic [31:0] raster_pixel_z_q;
logic raster_active;
logic raster_overflow;
logic raster_degenerate;
@@ -73,7 +77,11 @@ module tb_gs_raster_pipeline;
logic [63:0] ev_arg0, ev_arg1, ev_arg2, ev_arg3;
logic [31:0] ev_flags;
gs_stub u_gs (
gs_stub #(
.SUBPIXEL_XY(USE_SUBPIXEL),
.GRAD_SEQ_DIVIDER(1'b0),
.GRAD_DIV_CYCLES(GRAD_CYCLES)
) u_gs (
.clk(clk), .rst_n(rst_n),
.reg_wr_en(1'b0), .reg_wr_addr(16'd0), .reg_wr_data(64'd0),
.gif_reg_wr_en(gif_reg_wr_en),
@@ -110,6 +118,7 @@ module tb_gs_raster_pipeline;
.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_active(raster_active),
.raster_overflow(raster_overflow),
.raster_degenerate(raster_degenerate),
@@ -130,17 +139,32 @@ module tb_gs_raster_pipeline;
int emit_cycles [0:31];
int emit_count;
int errors;
logic [7:0] sub_r [0:3][0:3];
logic [31:0] sub_z [0:3][0:3];
logic sub_seen [0:3][0:3];
initial begin
v_close_cycle = -1;
emit_count = 0;
errors = 0;
for (int y = 0; y < 4; y++) begin
for (int x = 0; x < 4; x++) begin
sub_r[y][x] = 8'd0;
sub_z[y][x] = 32'd0;
sub_seen[y][x] = 1'b0;
end
end
end
always_ff @(posedge clk) begin
if (rst_n && raster_pixel_emit && emit_count < 32) begin
emit_cycles[emit_count] <= cycle_idx;
emit_count <= emit_count + 1;
if (USE_SUBPIXEL && raster_pixel_x_q < 4 && raster_pixel_y_q < 4) begin
sub_r[raster_pixel_y_q][raster_pixel_x_q] <= raster_pixel_color_q[7:0];
sub_z[raster_pixel_y_q][raster_pixel_x_q] <= raster_pixel_z_q;
sub_seen[raster_pixel_y_q][raster_pixel_x_q] <= 1'b1;
end
end
end
@@ -167,6 +191,21 @@ module tb_gs_raster_pipeline;
return {32'd0, y_int, 4'd0, x_int, 4'd0};
endfunction
function automatic logic [63:0] xyz2_sub(input logic [15:0] x_12_4,
input logic [15:0] y_12_4);
return {32'd0, y_12_4, x_12_4};
endfunction
function automatic logic [63:0] xyz2_sub_z(input logic [15:0] x_12_4,
input logic [15:0] y_12_4,
input logic [31:0] z);
return {z, y_12_4, x_12_4};
endfunction
function automatic logic [63:0] rgbaq_r(input logic [7:0] r);
return {32'd0, 8'hff, 8'd0, 8'd0, r};
endfunction
localparam logic [7:0] R_PRIM = 8'h00;
localparam logic [7:0] R_RGBAQ = 8'h01;
localparam logic [7:0] R_XYZ2 = 8'h05;
@@ -186,13 +225,27 @@ module tb_gs_raster_pipeline;
rst_n = 1'b1;
repeat (2) @(posedge clk);
drive_reg(R_PRIM, PRIM_SPRITE);
drive_reg(R_PRIM, USE_SUBPIXEL ? 64'd3 : PRIM_SPRITE);
drive_reg(R_FRAME_1, FRAME_1_VAL);
drive_reg(R_RGBAQ, RGBAQ_VAL);
// 4×4 sprite — bbox=[0..3]×[0..3] = 16 pixels.
drive_reg(R_XYZ2, xyz2_data(12'd0, 12'd0)); // v1
drive_reg(R_XYZ2, xyz2_data(12'd3, 12'd3)); // v2 — close S1
if (USE_SUBPIXEL) begin
// Right triangle at (0.75,0.75),(3.75,0.75),(0.75,3.75).
// Pixel-center 12.4 coverage is exactly (1,1),(2,1),(1,2).
// R and Z form exact planes: dR/dx=32, dR/dy=64,
// dZ/dx=100, dZ/dy=200. This checks fractional gradient
// setup and +0.5 pixel-center evaluation as well as coverage.
drive_reg(R_RGBAQ, rgbaq_r(8'd0));
drive_reg(R_XYZ2, xyz2_sub_z(16'h000c,16'h000c,32'd1000));
drive_reg(R_RGBAQ, rgbaq_r(8'd96));
drive_reg(R_XYZ2, xyz2_sub_z(16'h003c,16'h000c,32'd1300));
drive_reg(R_RGBAQ, rgbaq_r(8'd192));
drive_reg(R_XYZ2, xyz2_sub_z(16'h000c,16'h003c,32'd1600));
end else begin
// 4×4 sprite — bbox=[0..3]×[0..3] = 16 pixels.
drive_reg(R_XYZ2, xyz2_data(12'd0, 12'd0));
drive_reg(R_XYZ2, xyz2_data(12'd3, 12'd3));
end
v_close_cycle = cycle_idx; // capture posedge index of v2 close
// Stop driving (deassert gif_reg_wr_en) and let the
@@ -200,7 +253,12 @@ module tb_gs_raster_pipeline;
// high and re-commits the v2 vertex every cycle, kicking
// off extra sprites and overflowing the FIFO.
drive_idle();
repeat (40) @(posedge clk);
// A TRI cannot pop until all 14 affine gradients are ready. The
// production combinational-divider FSM spends GRAD_CYCLES settle
// cycles plus select/commit overhead per step, so scale this gate
// with the configured latency instead of falsely timing out at the
// legacy fixed 40 cycles.
repeat (USE_SUBPIXEL ? (14 * (GRAD_CYCLES + 2) + 20) : 40) @(posedge clk);
// ---- Assertions ----
$display("[tb_gs_raster_pipeline] v_close_cycle=%0d emit_count=%0d raster_pixel_emit_count=%0d raster_overflow=%b",
@@ -209,12 +267,13 @@ module tb_gs_raster_pipeline;
$display("[tb_gs_raster_pipeline] emit[%0d] @ cyc=%0d", i, emit_cycles[i]);
end
if (emit_count != 16) begin
$error("emit_count=%0d (expected 16 for 4×4 sprite)", emit_count);
if (emit_count != (USE_SUBPIXEL ? 3 : 16)) begin
$error("emit_count=%0d (expected %0d)", emit_count, USE_SUBPIXEL ? 3 : 16);
errors = errors + 1;
end
if (raster_pixel_emit_count != 32'd16) begin
$error("raster_pixel_emit_count=%0d (expected 16)", raster_pixel_emit_count);
if (raster_pixel_emit_count != (USE_SUBPIXEL ? 32'd3 : 32'd16)) begin
$error("raster_pixel_emit_count=%0d (expected %0d)", raster_pixel_emit_count,
USE_SUBPIXEL ? 3 : 16);
errors = errors + 1;
end
if (raster_overflow !== 1'b0) begin
@@ -222,9 +281,27 @@ module tb_gs_raster_pipeline;
errors = errors + 1;
end
if (USE_SUBPIXEL) begin
if (!sub_seen[1][1] || sub_r[1][1] != 8'd72 || sub_z[1][1] != 32'd1225) begin
$error("subpixel attr (1,1): seen=%b R=%0d Z=%0d expected R=72 Z=1225",
sub_seen[1][1], sub_r[1][1], sub_z[1][1]);
errors = errors + 1;
end
if (!sub_seen[1][2] || sub_r[1][2] != 8'd104 || sub_z[1][2] != 32'd1325) begin
$error("subpixel attr (2,1): seen=%b R=%0d Z=%0d expected R=104 Z=1325",
sub_seen[1][2], sub_r[1][2], sub_z[1][2]);
errors = errors + 1;
end
if (!sub_seen[2][1] || sub_r[2][1] != 8'd136 || sub_z[2][1] != 32'd1425) begin
$error("subpixel attr (1,2): seen=%b R=%0d Z=%0d expected R=136 Z=1425",
sub_seen[2][1], sub_r[2][1], sub_z[2][1]);
errors = errors + 1;
end
end
// Throughput: every consecutive pair of emits must be on
// adjacent cycles (delta == 1). 1 pixel/cycle.
for (int i = 1; i < emit_count; i++) begin
for (int i = 1; i < emit_count && !USE_SUBPIXEL; i++) begin
int d;
d = emit_cycles[i] - emit_cycles[i-1];
if (d != 1) begin
@@ -234,15 +311,19 @@ module tb_gs_raster_pipeline;
end
end
// Latency: 5 posedges from v_close to first observed
// raster_pixel_emit (see header for breakdown).
if (emit_count > 0) begin
// Latency: 6 posedges from v_close to first observed raster_pixel_emit. Ch357 (Codex) pipelined the attr_ram
// WRITE (register the assembled word on push, commit to M20K the next cycle + attr_pending holds the slot until
// the write lands), so the assembled prim is poppable at push+2 instead of push+1 — +1 cycle vs the old
// v_close+5. Correctness is unchanged (emit_count/overflow identical); this pins the new pipeline depth.
// The TRI subpixel case waits for the shared gradient engine before
// pop; this latency assertion is specifically the legacy SPRITE pipe.
if (emit_count > 0 && !USE_SUBPIXEL) begin
int expected_first;
int actual_first;
expected_first = v_close_cycle + 5;
expected_first = v_close_cycle + 6;
actual_first = emit_cycles[0];
if (actual_first != expected_first) begin
$error("first-emit latency: emit[0]@cyc=%0d (expected %0d = v_close+5)",
$error("first-emit latency: emit[0]@cyc=%0d (expected %0d = v_close+6, Ch357 attr-write pipeline)",
actual_first, expected_first);
errors = errors + 1;
end
+8 -7
View File
@@ -230,8 +230,8 @@ module tb_gs_textured_alpha_sprite;
localparam logic [31:0] TEX_BASE_BYTES = 32'd8 * 32'd256; // 2048
localparam int TEX_ROW_TEXELS = 64; // TBW*64
// Per-vertex RGBAQ TINT: {A=0x10, B=0xFF, G=0x80, R=0x40}. The vertex A (0x10) MUST be ignored
// (source alpha comes from the TEXEL); R/G/B modulate the texel (R halves, G identity, B doubles).
// Per-vertex RGBAQ TINT: {A=0x10, B=0xFF, G=0x80, R=0x40}.
// TFX=MODULATE,TCC=1 modulates all four channels, including texture alpha.
localparam logic [63:0] TINT_RGBAQ = 64'h0000_0000_10FF_8040;
// Background (dest) PSMCT32 pixel pre-filled under the sprite: {A=FF,B=20,G=20,R=20}.
localparam logic [31:0] BG_PIXEL = 32'hFF20_2020;
@@ -262,16 +262,17 @@ module tb_gs_textured_alpha_sprite;
end
endfunction
function automatic logic [31:0] expected(input int x, input int y);
logic [31:0] t; logic [7:0] cs_r, cs_g, cs_b;
logic [31:0] t; logic [7:0] cs_r, cs_g, cs_b, cs_a;
begin
t = texel(x, y);
cs_r = sw_mod8(t[7:0], TINT_RGBAQ[7:0]); // *0x40 -> half
cs_g = sw_mod8(t[15:8], TINT_RGBAQ[15:8]); // *0x80 -> identity
cs_b = sw_mod8(t[23:16], TINT_RGBAQ[23:16]); // *0xFF -> ~double (clamps)
expected = {t[31:24], // stored A = source(texel) alpha
sw_blend(cs_b, BG_PIXEL[23:16], t[31:24]),
sw_blend(cs_g, BG_PIXEL[15:8], t[31:24]),
sw_blend(cs_r, BG_PIXEL[7:0], t[31:24])};
cs_a = sw_mod8(t[31:24], TINT_RGBAQ[31:24]);
expected = {cs_a,
sw_blend(cs_b, BG_PIXEL[23:16], cs_a),
sw_blend(cs_g, BG_PIXEL[15:8], cs_a),
sw_blend(cs_r, BG_PIXEL[7:0], cs_a)};
end
endfunction