ec82764bef
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>
258 lines
10 KiB
Systemverilog
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
|