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>
390 lines
16 KiB
Systemverilog
390 lines
16 KiB
Systemverilog
// retroDE_ps2 — tb_gs_zbuffer (Brick 2b)
|
|
//
|
|
// White-box UNIT TB for the gs_stub Z-buffer (depth test) datapath.
|
|
// Wires gs_stub + vram_stub (byte-addressable, combinational read2)
|
|
// directly via gif_reg_* (no DMA). vram_stub.read2 is the stored-Z
|
|
// read port (Z_RD_REGISTERED=0); the raster_pixel_emit channel is the
|
|
// single VRAM write port (fb color + Z value serialized 2-beat).
|
|
//
|
|
// Covers, for a FLAT PSMCT32 SPRITE with TEST_1.ZTE=1, ZBUF PSMZ32:
|
|
// - GEQUAL: a fragment with Z >= stored Z passes (writes color + Z);
|
|
// a fragment with Z < stored Z fails (no fb write, no Z write).
|
|
// - GREATER: Z > stored passes; Z == stored fails.
|
|
// - ALWAYS / NEVER.
|
|
// - ZMSK=1 gates the Z update (color still writes on pass; Z unchanged).
|
|
// - Draw-order independence: NEAR (large Z) then FAR (small Z) overlap;
|
|
// GEQUAL → near wins in the overlap.
|
|
//
|
|
// VRAM layout (PSMCT32 fb at FBP=0, FBW=1 → 64 px/row; PSMZ32 Z buffer
|
|
// at ZBP=1 → byte base 1*2048=0x800). Sprites are small (a few px) so
|
|
// addresses are easy to predict. The Z buffer is pre-seeded by direct
|
|
// writes through the same vram_stub write port before each scenario.
|
|
|
|
`timescale 1ns/1ps
|
|
|
|
module tb_gs_zbuffer;
|
|
|
|
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 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 gs_tex_rd_en;
|
|
logic [31:0] gs_tex_rd_addr;
|
|
logic gs_fb_rd_en;
|
|
logic [31:0] gs_fb_rd_addr;
|
|
logic gs_z_rd_en;
|
|
logic [31:0] gs_z_rd_addr;
|
|
logic [31:0] read2_data;
|
|
|
|
// gs_stub with combinational read2 (Z_RD_REGISTERED=0, matches vram_stub).
|
|
gs_stub #(
|
|
.FB_RD_REGISTERED(1'b0),
|
|
.Z_RD_REGISTERED (1'b0)
|
|
) 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_g(), .bg_b(),
|
|
.pmode_q(), .dispfb1_q(), .display1_q(),
|
|
.prim_q(), .rgbaq_q(),
|
|
.xyz2_q(), .xyzf2_q(),
|
|
.frame_1_q(), .zbuf_1_q(),
|
|
.tex0_1_q(), .tex0_1_cbp_q(), .tex0_1_cpsm_q(),
|
|
.tex0_1_csm_q(), .tex0_1_csa_q(), .tex0_1_cld_q(), .tex0_1_wr_q(),
|
|
.bitbltbuf_q(), .trxpos_q(), .trxreg_q(), .trxdir_q(), .trxdir_wr_q(),
|
|
.prim_complete(), .prim_complete_count(),
|
|
.prim_v0_q(), .prim_v1_q(), .prim_v2_q(),
|
|
.prim_color_q(),
|
|
.prim_color_v0_q(), .prim_color_v1_q(), .prim_color_v2_q(),
|
|
.prim_v0_decoded_q(), .prim_v1_decoded_q(), .prim_v2_decoded_q(),
|
|
.prim_v0_color_decoded_q(), .prim_v1_color_decoded_q(), .prim_v2_color_decoded_q(),
|
|
.pixel_emit(), .pixel_emit_count(),
|
|
.pixel_x_q(), .pixel_y_q(),
|
|
.pixel_color_q(),
|
|
.pixel_fbp_q(), .pixel_fbw_q(), .pixel_psm_q(), .pixel_fb_addr_q(),
|
|
.raster_pixel_emit(raster_pixel_emit),
|
|
.raster_pixel_emit_count(raster_pixel_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_degenerate(),
|
|
.tex_rd_en(gs_tex_rd_en), .tex_rd_addr(gs_tex_rd_addr), .tex_rd_data(read2_data),
|
|
.fb_rd_en(gs_fb_rd_en), .fb_rd_addr(gs_fb_rd_addr), .fb_rd_data(read2_data),
|
|
.z_rd_en(gs_z_rd_en), .z_rd_addr(gs_z_rd_addr), .z_rd_data(read2_data),
|
|
.ev_valid(), .ev_subsys(), .ev_event(),
|
|
.ev_arg0(), .ev_arg1(), .ev_arg2(), .ev_arg3(), .ev_flags()
|
|
);
|
|
|
|
// ----- VRAM (byte-addressable, combinational read2) -----
|
|
// TB-direct write override so the harness can pre-seed fb + Z.
|
|
logic tb_we;
|
|
logic [31:0] tb_waddr;
|
|
logic [31:0] tb_wdata;
|
|
|
|
logic vram_we;
|
|
logic [31:0] vram_waddr;
|
|
logic [31:0] vram_wdata;
|
|
logic [3:0] vram_wbe;
|
|
|
|
assign vram_we = tb_we ? 1'b1 : raster_pixel_emit;
|
|
assign vram_waddr = tb_we ? tb_waddr : raster_pixel_fb_addr_q;
|
|
assign vram_wdata = tb_we ? tb_wdata : raster_pixel_color_q[31:0];
|
|
assign vram_wbe = tb_we ? 4'b1111 : raster_pixel_be_q;
|
|
|
|
// read2 address mux (matches the wrapper arbitration; only one of the
|
|
// three consumers is ever active for a Z-tested flat sprite).
|
|
logic [31:0] read2_addr;
|
|
assign read2_addr = gs_tex_rd_en ? gs_tex_rd_addr
|
|
: gs_fb_rd_en ? gs_fb_rd_addr
|
|
: gs_z_rd_en ? gs_z_rd_addr
|
|
: 32'd0;
|
|
|
|
logic [31:0] rd_addr;
|
|
logic [31:0] rd_data;
|
|
|
|
vram_stub u_vram (
|
|
.clk(clk), .rst_n(rst_n),
|
|
.write_en (vram_we),
|
|
.write_addr(vram_waddr),
|
|
.write_data(vram_wdata),
|
|
.write_be (vram_wbe),
|
|
.write_mask(32'hFFFF_FFFF),
|
|
.read_addr (rd_addr),
|
|
.read_data (rd_data),
|
|
.read2_addr(read2_addr),
|
|
.read2_data(read2_data)
|
|
);
|
|
|
|
// ---- encoders ----
|
|
localparam logic [7:0] R_PRIM = 8'h00;
|
|
localparam logic [7:0] R_RGBAQ = 8'h01;
|
|
localparam logic [7:0] R_XYZ2 = 8'h05;
|
|
localparam logic [7:0] R_TEST_1 = 8'h47;
|
|
localparam logic [7:0] R_FRAME_1 = 8'h4C;
|
|
localparam logic [7:0] R_ZBUF_1 = 8'h4E;
|
|
|
|
localparam logic [63:0] PRIM_SPRITE = 64'd6;
|
|
// FRAME_1: FBP=0, FBW=1 (64 px/row), PSMCT32.
|
|
localparam logic [63:0] FRAME_1_VAL = 64'h0000_0000_0001_0000;
|
|
// ZBUF_1: ZBP=1 (byte base 0x800), PSM=PSMZ32(0), ZMSK depends.
|
|
|
|
function automatic logic [63:0] xyz2_dataz(input logic [11:0] x_int,
|
|
input logic [11:0] y_int,
|
|
input logic [31:0] z);
|
|
// X=[15:4], Y=[31:20], Z=[63:32].
|
|
return {z, y_int, 4'd0, x_int, 4'd0};
|
|
endfunction
|
|
|
|
function automatic logic [63:0] test1_z(input logic ztst_en, input logic [1:0] ztst);
|
|
// ZTE=bit16, ZTST=bits[18:17].
|
|
logic [63:0] v;
|
|
v = 64'd0;
|
|
v[16] = ztst_en;
|
|
v[18:17] = ztst;
|
|
return v;
|
|
endfunction
|
|
|
|
function automatic logic [63:0] zbuf1(input logic [8:0] zbp, input logic zmsk);
|
|
// ZBP=[8:0], PSM=[27:24]=0 (PSMZ32), ZMSK=bit0. The task spec
|
|
// places ZMSK at bit 0 which overlaps ZBP[0]; we keep ZBP EVEN
|
|
// (bit0=0) in every test so the two never alias.
|
|
logic [63:0] v;
|
|
v = 64'd0;
|
|
v[8:0] = zbp;
|
|
v[27:24] = 4'd0; // PSMZ32
|
|
v[0] = zmsk;
|
|
return v;
|
|
endfunction
|
|
|
|
// fb byte address for (x,y): FBP*2048 + (y*FBW*64 + x)*4. FBP=0, FBW=1.
|
|
function automatic logic [31:0] fb_addr_of(input logic [11:0] x, input logic [11:0] y);
|
|
return ((y * 64) + x) * 4;
|
|
endfunction
|
|
// Z byte address: ZBP*2048 + (y*FBW*64 + x)*4. ZBP=2 -> 0x1000.
|
|
function automatic logic [31:0] z_addr_of(input logic [11:0] x, input logic [11:0] y);
|
|
return 32'h1000 + (((y * 64) + x) * 4);
|
|
endfunction
|
|
|
|
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);
|
|
@(negedge clk);
|
|
gif_reg_wr_en = 1'b0;
|
|
@(posedge clk);
|
|
endtask
|
|
|
|
task automatic tb_write(input logic [31:0] addr, input logic [31:0] data);
|
|
@(negedge clk);
|
|
tb_we = 1'b1;
|
|
tb_waddr = addr;
|
|
tb_wdata = data;
|
|
@(posedge clk);
|
|
@(negedge clk);
|
|
tb_we = 1'b0;
|
|
@(posedge clk);
|
|
endtask
|
|
|
|
// Combinational read helper (task; iverilog-12 forbids delays in
|
|
// functions). Drives rd_addr, settles, returns rd_data via output.
|
|
task automatic peek(input logic [31:0] addr, output logic [31:0] val);
|
|
rd_addr = addr;
|
|
#1;
|
|
val = rd_data;
|
|
endtask
|
|
|
|
int errors;
|
|
|
|
task automatic expect_fb(input logic [11:0] x, input logic [11:0] y,
|
|
input logic [31:0] exp, input string label);
|
|
logic [31:0] got;
|
|
peek(fb_addr_of(x, y), got);
|
|
if (got !== exp) begin
|
|
$error("[%s] fb(%0d,%0d) got 0x%08x exp 0x%08x", label, x, y, got, exp);
|
|
errors = errors + 1;
|
|
end
|
|
endtask
|
|
|
|
task automatic expect_z(input logic [11:0] x, input logic [11:0] y,
|
|
input logic [31:0] exp, input string label);
|
|
logic [31:0] got;
|
|
peek(z_addr_of(x, y), got);
|
|
if (got !== exp) begin
|
|
$error("[%s] Z(%0d,%0d) got 0x%08x exp 0x%08x", label, x, y, got, exp);
|
|
errors = errors + 1;
|
|
end
|
|
endtask
|
|
|
|
// Draw a 1x1 Z-tested flat sprite at (x,y) with color/Z; ZTST + ZMSK
|
|
// configured per call. Returns after raster drains.
|
|
task automatic draw_ztest_pixel(input logic [11:0] x,
|
|
input logic [11:0] y,
|
|
input logic [31:0] color,
|
|
input logic [31:0] z,
|
|
input logic [1:0] ztst,
|
|
input logic zmsk);
|
|
drive_reg(R_PRIM, PRIM_SPRITE);
|
|
drive_reg(R_FRAME_1, FRAME_1_VAL);
|
|
drive_reg(R_TEST_1, test1_z(1'b1, ztst));
|
|
drive_reg(R_ZBUF_1, zbuf1(9'd2, zmsk));
|
|
drive_reg(R_RGBAQ, {32'd0, color});
|
|
drive_reg(R_XYZ2, xyz2_dataz(x, y, z)); // v1
|
|
drive_reg(R_XYZ2, xyz2_dataz(x, y, z)); // v2 close (1x1)
|
|
repeat (30) @(posedge clk);
|
|
endtask
|
|
|
|
initial begin
|
|
rst_n = 1'b0;
|
|
gif_reg_wr_en = 1'b0;
|
|
gif_reg_num = 8'd0;
|
|
gif_reg_data = 64'd0;
|
|
tb_we = 1'b0;
|
|
tb_waddr = 32'd0;
|
|
tb_wdata = 32'd0;
|
|
rd_addr = 32'd0;
|
|
errors = 0;
|
|
|
|
repeat (4) @(posedge clk);
|
|
rst_n = 1'b1;
|
|
repeat (4) @(posedge clk);
|
|
|
|
// ============================================================
|
|
// Scenario 1 — GEQUAL pass: stored Z=0x100, frag Z=0x200 (>=).
|
|
// Pixel passes → color written, Z updated to 0x200.
|
|
// ============================================================
|
|
tb_write(z_addr_of(1, 1), 32'h0000_0100); // seed stored Z
|
|
tb_write(fb_addr_of(1, 1), 32'hDEAD_BEEF); // seed fb (sentinel)
|
|
draw_ztest_pixel(12'd1, 12'd1, 32'h0000_00AA, 32'h0000_0200, 2'd2, 1'b0);
|
|
expect_fb(1, 1, 32'h0000_00AA, "geq_pass_color");
|
|
expect_z (1, 1, 32'h0000_0200, "geq_pass_z");
|
|
|
|
// ============================================================
|
|
// Scenario 2 — GEQUAL fail: stored Z=0x200, frag Z=0x100 (<).
|
|
// Pixel fails → fb stays sentinel, Z stays 0x200.
|
|
// ============================================================
|
|
tb_write(z_addr_of(2, 1), 32'h0000_0200);
|
|
tb_write(fb_addr_of(2, 1), 32'hDEAD_BEEF);
|
|
draw_ztest_pixel(12'd2, 12'd1, 32'h0000_00BB, 32'h0000_0100, 2'd2, 1'b0);
|
|
expect_fb(2, 1, 32'hDEAD_BEEF, "geq_fail_color"); // discarded
|
|
expect_z (2, 1, 32'h0000_0200, "geq_fail_z"); // unchanged
|
|
|
|
// ============================================================
|
|
// Scenario 3 — GEQUAL equal-Z passes (>=): stored=frag=0x55.
|
|
// ============================================================
|
|
tb_write(z_addr_of(3, 1), 32'h0000_0055);
|
|
tb_write(fb_addr_of(3, 1), 32'hDEAD_BEEF);
|
|
draw_ztest_pixel(12'd3, 12'd1, 32'h0000_00CC, 32'h0000_0055, 2'd2, 1'b0);
|
|
expect_fb(3, 1, 32'h0000_00CC, "geq_equal_color");
|
|
expect_z (3, 1, 32'h0000_0055, "geq_equal_z");
|
|
|
|
// ============================================================
|
|
// Scenario 4 — GREATER equal-Z FAILS: stored=frag=0x55.
|
|
// ============================================================
|
|
tb_write(z_addr_of(4, 1), 32'h0000_0055);
|
|
tb_write(fb_addr_of(4, 1), 32'hDEAD_BEEF);
|
|
draw_ztest_pixel(12'd4, 12'd1, 32'h0000_00DD, 32'h0000_0055, 2'd3, 1'b0);
|
|
expect_fb(4, 1, 32'hDEAD_BEEF, "gtr_equal_fail_color");
|
|
expect_z (4, 1, 32'h0000_0055, "gtr_equal_fail_z");
|
|
|
|
// GREATER strictly-greater PASSES.
|
|
tb_write(z_addr_of(5, 1), 32'h0000_0055);
|
|
tb_write(fb_addr_of(5, 1), 32'hDEAD_BEEF);
|
|
draw_ztest_pixel(12'd5, 12'd1, 32'h0000_00EE, 32'h0000_0056, 2'd3, 1'b0);
|
|
expect_fb(5, 1, 32'h0000_00EE, "gtr_pass_color");
|
|
expect_z (5, 1, 32'h0000_0056, "gtr_pass_z");
|
|
|
|
// ============================================================
|
|
// Scenario 5 — ALWAYS passes regardless of stored Z.
|
|
// ============================================================
|
|
tb_write(z_addr_of(6, 1), 32'hFFFF_FFFF); // huge stored Z
|
|
tb_write(fb_addr_of(6, 1), 32'hDEAD_BEEF);
|
|
draw_ztest_pixel(12'd6, 12'd1, 32'h0000_0011, 32'h0000_0001, 2'd1, 1'b0);
|
|
expect_fb(6, 1, 32'h0000_0011, "always_color");
|
|
expect_z (6, 1, 32'h0000_0001, "always_z");
|
|
|
|
// ============================================================
|
|
// Scenario 6 — NEVER: nothing written.
|
|
// ============================================================
|
|
tb_write(z_addr_of(7, 1), 32'h0000_0000);
|
|
tb_write(fb_addr_of(7, 1), 32'hDEAD_BEEF);
|
|
draw_ztest_pixel(12'd7, 12'd1, 32'h0000_0022, 32'h0000_9999, 2'd0, 1'b0);
|
|
expect_fb(7, 1, 32'hDEAD_BEEF, "never_color");
|
|
expect_z (7, 1, 32'h0000_0000, "never_z");
|
|
|
|
// ============================================================
|
|
// Scenario 7 — ZMSK=1: pass writes color but NOT Z.
|
|
// stored=0x100 frag=0x200 GEQUAL passes; Z must remain 0x100.
|
|
// ============================================================
|
|
tb_write(z_addr_of(8, 1), 32'h0000_0100);
|
|
tb_write(fb_addr_of(8, 1), 32'hDEAD_BEEF);
|
|
draw_ztest_pixel(12'd8, 12'd1, 32'h0000_0033, 32'h0000_0200, 2'd2, 1'b1);
|
|
expect_fb(8, 1, 32'h0000_0033, "zmsk_color");
|
|
expect_z (8, 1, 32'h0000_0100, "zmsk_z_unchanged");
|
|
|
|
// ============================================================
|
|
// Scenario 8 — DRAW-ORDER INDEPENDENCE.
|
|
// Clear Z at (10,1). Draw NEAR (large Z=0x300, color NEAR) then
|
|
// FAR (small Z=0x100, color FAR) at the SAME pixel with GEQUAL.
|
|
// Near wins: fb == NEAR, Z == 0x300. Proves depth gated, NOT
|
|
// last-write-wins.
|
|
// ============================================================
|
|
tb_write(z_addr_of(10, 1), 32'h0000_0000); // clear Z
|
|
tb_write(fb_addr_of(10, 1), 32'h0000_0000);
|
|
draw_ztest_pixel(12'd10, 12'd1, 32'h0000_00AA, 32'h0000_0300, 2'd2, 1'b0); // NEAR
|
|
draw_ztest_pixel(12'd10, 12'd1, 32'h0000_00BB, 32'h0000_0100, 2'd2, 1'b0); // FAR (behind)
|
|
expect_fb(10, 1, 32'h0000_00AA, "order_near_wins_color");
|
|
expect_z (10, 1, 32'h0000_0300, "order_near_wins_z");
|
|
begin
|
|
logic [31:0] ov;
|
|
peek(fb_addr_of(10, 1), ov);
|
|
if (ov === 32'h0000_00BB) begin
|
|
$error("draw-order: overlap is FAR (last-drawn) — depth NOT gated!");
|
|
errors = errors + 1;
|
|
end
|
|
end
|
|
|
|
$display("[tb_gs_zbuffer] raster_emits=%0d overflow=%b errors=%0d",
|
|
raster_pixel_emit_count, raster_overflow, errors);
|
|
if (raster_overflow) begin
|
|
$error("raster_overflow set"); errors = errors + 1;
|
|
end
|
|
if (errors == 0) $display("[tb_gs_zbuffer] PASS");
|
|
else $display("[tb_gs_zbuffer] FAIL");
|
|
$finish;
|
|
end
|
|
|
|
initial begin
|
|
#5000000;
|
|
$error("[tb_gs_zbuffer] TIMEOUT");
|
|
$finish;
|
|
end
|
|
|
|
endmodule : tb_gs_zbuffer
|