Files
retroDE_ps2/sim/tb/top/tb_top_psmct32_triangle_demo.sv
thejayman77 ec82764bef Initial commit: retroDE_ps2 — first-of-its-kind PS2 GS FPGA core (DE25-Nano / Agilex 5)
RTL (GS rasterizer, EE core stub, platform bridge, LPDDR4B path), sim regression
(272 TBs), docs, and tooling. Copyrighted PS2 content (BIOS, game code, GS dumps,
and all dump-derived textures/traces) is excluded via .gitignore and stays local.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 20:10:50 -04:00

258 lines
10 KiB
Systemverilog

// retroDE_ps2 — tb_top_psmct32_triangle_demo (Brick 3)
//
// TOP-LEVEL non-axis-aligned GOURAUD TRIANGLE demo TB for the BRAM BOARD
// VARIANT. Proves the first real triangle renders end-to-end through
// `top_psmct32_raster_demo_bram` exactly as a board load would SHOW on
// HDMI — with interpolated COLOR (Gouraud) and interpolated DEPTH (Z).
//
// EE bootlet (bios_triangle.mem) + GIF payload (payload_triangle.mem):
// U1 PACKED -> TRI A: Gouraud, v0=(1,1) RED, v1=(14,1) GREEN,
// v2=(7,7) BLUE, FLAT-NEAR Z=0x300, GEQUAL Z-test,
// ZBUF ZBP=2 PSMZ32 (cleared -> passes, stamps Z=0x300).
// U2 PACKED -> TRI B: grey (0x80) at FLAT-FAR Z=0x100, v0=(2,5)
// v1=(13,5) v2=(7,2). Drawn second; GEQUAL.
//
// Expected on screen:
// * TRI A is a real non-axis-aligned (downward) Gouraud triangle: its
// interior shows a visible R/G/B gradient (corners tend toward the
// vertex colors).
// * In the band where TRI B overlaps TRI A, TRI A WINS (its Z=0x300 >
// TRI B's interpolated Z=0x100 under GEQUAL), so those pixels are
// TRI A's Gouraud color, NOT grey — proving the per-pixel
// interpolated depth gated the second triangle's write.
// * TRI B only paints grey where TRI A did NOT cover (stored Z=0 there).
//
// Checks:
// (1) coverage: a representative set of TRI A interior pixels are
// drawn (non-zero color, DE asserted), and a clearly-outside
// pixel is background (black).
// (2) gradient: the color VARIES across TRI A (not flat) and the
// near-vertex regions lean toward the corresponding vertex color.
// (3) depth gating: a pixel inside the TRI A/TRI B overlap is NOT grey
// (TRI A won) — proving interpolated-Z depth test gated TRI B.
`timescale 1ns/1ps
module tb_top_psmct32_triangle_demo;
localparam int H_ACTIVE = 16;
localparam int V_ACTIVE = 8;
logic clk;
logic rst_n;
initial clk = 1'b0;
always #5 clk = ~clk;
logic core_go;
logic [7:0] r, g, b;
logic hsync, vsync, de;
logic core_halt;
logic dma_done_seen;
logic frame_seen;
logic raster_overflow;
logic frame_toggle;
logic dma_done_toggle;
top_psmct32_raster_demo_bram #(
.H_ACTIVE (H_ACTIVE),
.V_ACTIVE (V_ACTIVE),
.PSMCT32_SWIZZLE(1'b0)
) dut (
.clk(clk), .rst_n(rst_n),
.core_go(core_go),
.r(r), .g(g), .b(b),
.hsync(hsync), .vsync(vsync), .de(de),
.core_halt(core_halt),
.dma_done_seen(dma_done_seen),
.frame_seen(frame_seen),
.raster_overflow(raster_overflow),
.frame_toggle(frame_toggle),
.dma_done_toggle(dma_done_toggle),
.joy_a_pressed_i(1'b0),
.joy_b_pressed_i(1'b0)
);
// ----- Frame capture (delayed counters: BRAM read is registered) -----
logic [7:0] cap_r [0:V_ACTIVE-1][0:H_ACTIVE-1];
logic [7:0] cap_g [0:V_ACTIVE-1][0:H_ACTIVE-1];
logic [7:0] cap_b [0:V_ACTIVE-1][0:H_ACTIVE-1];
logic cap_de[0:V_ACTIVE-1][0:H_ACTIVE-1];
bit capture_armed;
initial begin
for (int y = 0; y < V_ACTIVE; y++)
for (int x = 0; x < H_ACTIVE; x++) begin
cap_r[y][x] = 8'd0; cap_g[y][x] = 8'd0;
cap_b[y][x] = 8'd0; cap_de[y][x] = 1'b0;
end
capture_armed = 1'b0;
end
logic [31:0] hcnt_d, vcnt_d;
always_ff @(posedge clk) begin
if (!rst_n) begin
hcnt_d <= 32'd0; vcnt_d <= 32'd0;
end else begin
hcnt_d <= 32'(dut.u_pcrtc.hcnt);
vcnt_d <= 32'(dut.u_pcrtc.vcnt);
end
end
always_ff @(posedge clk) begin
if (rst_n && capture_armed && de
&& (vcnt_d < V_ACTIVE) && (hcnt_d < H_ACTIVE)) begin
cap_r [vcnt_d][hcnt_d] <= r;
cap_g [vcnt_d][hcnt_d] <= g;
cap_b [vcnt_d][hcnt_d] <= b;
cap_de[vcnt_d][hcnt_d] <= 1'b1;
end
end
int errors;
initial errors = 0;
function automatic bit is_grey(input int x, input int y);
return (cap_r[y][x] == 8'h80) && (cap_g[y][x] == 8'h80) && (cap_b[y][x] == 8'h80);
endfunction
function automatic bit nonzero(input int x, input int y);
return (cap_r[y][x] != 0) || (cap_g[y][x] != 0) || (cap_b[y][x] != 0);
endfunction
initial begin
rst_n = 1'b0;
core_go = 1'b0;
repeat (4) @(posedge clk);
rst_n = 1'b1;
repeat (8) @(posedge clk);
@(negedge clk); core_go = 1'b1;
@(negedge clk); core_go = 1'b0;
wait (core_halt == 1'b1);
repeat (4) @(posedge clk);
wait (dma_done_seen == 1'b1);
repeat (10) @(posedge clk);
if (dut.xfer_busy == 1'b1) wait (dut.xfer_busy == 1'b0);
if (dut.u_gs.raster_active == 1'b1) wait (dut.u_gs.raster_active == 1'b0);
repeat (10) @(posedge clk);
@(posedge dut.u_pcrtc.end_of_frame);
@(posedge clk);
capture_armed = 1'b1;
@(posedge dut.u_pcrtc.end_of_frame);
@(posedge clk);
capture_armed = 1'b0;
// ---- (1) Coverage: representative TRI A interior pixels drawn ----
// TRI A spans v0=(1,1) v1=(14,1) v2=(7,7); centre column near the
// top is solidly interior. Check a few deep-interior pixels.
begin
int interior_drawn;
interior_drawn = 0;
for (int yy = 2; yy <= 5; yy++)
for (int xx = 5; xx <= 9; xx++)
if (cap_de[yy][xx] && nonzero(xx, yy))
interior_drawn = interior_drawn + 1;
if (interior_drawn < 12) begin
$error("TRI A interior coverage too sparse: %0d/20 deep-interior pixels drawn",
interior_drawn);
errors = errors + 1;
end
$display("[tri_demo] TRI A deep-interior drawn = %0d/20", interior_drawn);
end
// Clearly-OUTSIDE pixel (top corners outside the downward tri).
if (nonzero(0, 7)) begin
// (0,7): far bottom-left, outside TRI A and outside TRI B.
$error("outside pixel (0,7) is colored (%02x,%02x,%02x), expected background",
cap_r[7][0], cap_g[7][0], cap_b[7][0]);
errors = errors + 1;
end
// ---- (2) Gradient present: color VARIES across TRI A ----
// Top-left interior (near RED v0) should have more R than B;
// top-right interior (near GREEN v1) more G than R; lower-centre
// (near BLUE v2) more B than R. We sample interior pixels.
begin
// near v0 (1,1) RED: pixel (3,2)
if (cap_de[2][3] && !(cap_r[2][3] > cap_b[2][3])) begin
$error("gradient: near-RED (3,2) R=%0d not > B=%0d", cap_r[2][3], cap_b[2][3]);
errors = errors + 1;
end
// near v1 (14,1) GREEN: pixel (12,2)
if (cap_de[2][12] && !(cap_g[2][12] > cap_r[2][12])) begin
$error("gradient: near-GREEN (12,2) G=%0d not > R=%0d", cap_g[2][12], cap_r[2][12]);
errors = errors + 1;
end
// near v2 (7,7) BLUE: pixel (7,6)
if (cap_de[6][7] && !(cap_b[6][7] > cap_r[6][7])) begin
$error("gradient: near-BLUE (7,6) B=%0d not > R=%0d", cap_b[6][7], cap_r[6][7]);
errors = errors + 1;
end
$display("[tri_demo] gradient samples: nearR(3,2)=(%0d,%0d,%0d) nearG(12,2)=(%0d,%0d,%0d) nearB(7,6)=(%0d,%0d,%0d)",
cap_r[2][3],cap_g[2][3],cap_b[2][3],
cap_r[2][12],cap_g[2][12],cap_b[2][12],
cap_r[6][7],cap_g[6][7],cap_b[6][7]);
end
// ---- (3) Depth gating: TRI A/TRI B overlap shows TRI A (not grey) ----
// The TRI B band is around y=2..5 mid-x; pixel (7,4) is inside both
// TRI A and TRI B. TRI A (Z=0x300) was stamped first; TRI B
// (Z=0x100) must FAIL GEQUAL there -> pixel is TRI A's Gouraud
// color, NOT grey 0x80.
begin
bit any_overlap_checked;
any_overlap_checked = 1'b0;
for (int yy = 3; yy <= 4; yy++) begin
for (int xx = 6; xx <= 8; xx++) begin
if (cap_de[yy][xx] && nonzero(xx, yy)) begin
any_overlap_checked = 1'b1;
if (is_grey(xx, yy)) begin
$error("depth gating FAIL: overlap (%0d,%0d) is GREY (TRI B won) — interp-Z not gated!",
xx, yy);
errors = errors + 1;
end
end
end
end
if (!any_overlap_checked) begin
$error("depth gating: no overlap pixel observed to test");
errors = errors + 1;
end
// And TRI B SHOULD have painted grey somewhere it was unoccluded
// (e.g. its lower band where TRI A didn't reach). Confirm grey
// appears at least once so we know TRI B actually drew.
begin
int grey_count;
grey_count = 0;
for (int yy = 0; yy < V_ACTIVE; yy++)
for (int xx = 0; xx < H_ACTIVE; xx++)
if (cap_de[yy][xx] && is_grey(xx, yy)) grey_count = grey_count + 1;
if (grey_count == 0) begin
$error("TRI B never painted grey anywhere — second triangle did not render");
errors = errors + 1;
end
$display("[tri_demo] grey (TRI B unoccluded) pixel count = %0d", grey_count);
end
end
if (!core_halt) begin $error("core_halt low at end"); errors = errors + 1; end
if (!dma_done_seen) begin $error("dma_done_seen never latched"); errors = errors + 1; end
if (!frame_seen) begin $error("frame_seen never latched"); errors = errors + 1; end
if (raster_overflow) begin $error("raster_overflow set"); errors = errors + 1; end
$display("[tb_top_psmct32_triangle_demo] raster_emits=%0d errors=%0d",
dut.u_gs.raster_pixel_emit_count, errors);
if (errors == 0) $display("[tb_top_psmct32_triangle_demo] PASS");
else $display("[tb_top_psmct32_triangle_demo] FAIL");
$finish;
end
initial begin
#20000000;
$error("[tb_top_psmct32_triangle_demo] TIMEOUT");
$finish;
end
endmodule : tb_top_psmct32_triangle_demo