// 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