Files
retroDE_ps2/sim/tb/gif_gs/tb_gs_raster_psmt8.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

359 lines
14 KiB
Systemverilog
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// retroDE_ps2 — tb_gs_raster_psmt8 (Ch105)
//
// Locks the contract for gs_stub's WRITE-side PSMT8 emit. The
// raster pipeline (Ch88: S0/S1/S2) now writes 1 byte per pixel
// when FRAME_1.PSM = PSMT8 (= 0x13): the natural ABGR's R channel
// (low 8 bits) becomes the PSMT8 index, gets placed in the LOW
// byte of the emit lane, and `raster_pixel_be_q = 4'b0001` so
// vram_stub commits exactly the 1 byte at fb_addr without
// stomping the neighbouring 3 pixels in the same 32-bit word.
//
// Setup:
// PRIM = SPRITE
// FRAME_1: FBP=0, FBW=1, PSM=PSMT8 (0x13)
// FBW at bit 16 → 0x0001_0000
// PSM at bit 24 → 0x1300_0000
// RGBAQ: R=0x55 chosen as the canonical "index" byte. The other
// channels (G/B/A) are non-zero (0xAA / 0xBB / 0xCC) on purpose:
// ONLY R should land in VRAM at the PSMT8 fb. If a buggy emit
// path leaked G/B/A into adjacent bytes, the post-row zero
// checks would catch it.
// v1 = (1, 0); v2 = (5, 2) → 5×3 sprite. Width = 5 forces an
// ODD number of pixels per row, so the per-byte-lane handling
// gets exercised at byte alignments 1, 2, 3, and 0 within a
// single row (lanes 1,2,3,0,1 → spans two 32-bit words and the
// first 32-bit word's first pixel must be left UNTOUCHED at
// byte 0).
//
// Expected per-pixel:
// - Each sprite pixel writes mem[fb_addr] = 0x55.
// - fb_addr = FBP*2048 + FBW*64*y + x (FBW*64 pixels/row, 1 B/pix)
//
// PASS criteria:
// - 15 raster_pixel_emit pulses (5×3)
// - raster_pixel_be_q == 4'b0001 on every emit
// - raster_pixel_psm_q == 6'h13 on every emit
// - Every expected sprite byte reads back as 0x55.
// - Byte 0 (left-of-sprite, same row) reads back as 0x00 — a
// 32-bit-aligned overwrite would have stomped it with a
// replicated index. With per-byte be active it stays 0.
// - Byte right-of-sprite on each row reads back as 0x00 — same
// reasoning.
// - Byte 0 of any row OUTSIDE the sprite stays 0x00.
`timescale 1ns/1ps
module tb_gs_raster_psmt8;
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] pmode_q, dispfb1_q, display1_q;
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 [5:0] raster_pixel_psm_q;
logic raster_active;
logic raster_overflow;
logic raster_degenerate;
logic gs_ev_valid;
trace_pkg::subsys_e gs_ev_subsys;
trace_pkg::event_e gs_ev_event;
logic [63:0] gs_ev_arg0, gs_ev_arg1, gs_ev_arg2, gs_ev_arg3;
logic [31:0] gs_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),
.pmode_q(pmode_q), .dispfb1_q(dispfb1_q), .display1_q(display1_q),
.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_psm_q(raster_pixel_psm_q),
.raster_active(raster_active),
.raster_overflow(raster_overflow),
.raster_degenerate(raster_degenerate),
.ev_valid(gs_ev_valid),
.ev_subsys(gs_ev_subsys),
.ev_event(gs_ev_event),
.ev_arg0(gs_ev_arg0), .ev_arg1(gs_ev_arg1),
.ev_arg2(gs_ev_arg2), .ev_arg3(gs_ev_arg3),
.ev_flags(gs_ev_flags)
);
logic [31:0] vram_read_addr;
logic [31:0] vram_read_data;
vram_stub #(.BYTES(4096)) u_vram (
.clk(clk), .rst_n(rst_n),
.write_en (raster_pixel_emit),
.write_addr(raster_pixel_fb_addr_q),
.write_data(raster_pixel_color_q[31:0]),
.write_be (raster_pixel_be_q),
.write_mask(32'hFFFF_FFFF),
.read_addr (vram_read_addr),
.read_data (vram_read_data),
.read2_addr(32'd0),
.read2_data()
);
int errors;
int psmt8_emit_seen;
initial begin
errors = 0;
psmt8_emit_seen = 0;
end
// Sanity observer: while emitting PSMT8, raster_pixel_be_q must
// be 4'b0001 and raster_pixel_psm_q must be 6'h13.
always_ff @(posedge clk) begin
if (rst_n && raster_pixel_emit) begin
psmt8_emit_seen <= psmt8_emit_seen + 1;
if (raster_pixel_psm_q !== 6'h13) begin
$error("emit %0d: raster_pixel_psm_q=0x%02x (expected 0x13 PSMT8)",
psmt8_emit_seen, raster_pixel_psm_q);
errors <= errors + 1;
end
if (raster_pixel_be_q !== 4'b0001) begin
$error("emit %0d: raster_pixel_be_q=%b (expected 4'b0001 for PSMT8)",
psmt8_emit_seen, raster_pixel_be_q);
errors <= errors + 1;
end
end
end
task automatic step_drive(input logic wr_en,
input logic [7:0] num,
input logic [63:0] data);
@(negedge clk);
gif_reg_wr_en = wr_en;
gif_reg_num = num;
gif_reg_data = data;
@(posedge clk);
endtask
task automatic drive_reg(input logic [7:0] num, input logic [63:0] data);
step_drive(1'b1, num, data);
endtask
task automatic drive_idle();
step_drive(1'b0, 8'd0, 64'd0);
endtask
function automatic logic [63:0] xyz2_data(input logic [11:0] x_int,
input logic [11:0] y_int);
return {32'd0, y_int, 4'd0, x_int, 4'd0};
endfunction
task automatic check_byte(input logic [31:0] byte_addr,
input logic [7:0] expected,
input string tag);
logic [7:0] got;
// vram_stub returns 4 bytes starting at read_addr — read at
// a 4-byte-aligned address that contains the target byte,
// then mux based on byte_addr[1:0].
vram_read_addr = byte_addr & ~32'd3;
#1;
unique case (byte_addr[1:0])
2'd0: got = vram_read_data[7:0];
2'd1: got = vram_read_data[15:8];
2'd2: got = vram_read_data[23:16];
2'd3: got = vram_read_data[31:24];
endcase
if (got !== expected) begin
$error("[%s] @byte 0x%08x got 0x%02x expected 0x%02x",
tag, byte_addr, got, expected);
errors = errors + 1;
end
endtask
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_FRAME_1 = 8'h4C;
localparam logic [63:0] PRIM_SPRITE = 64'd6;
// FRAME_1: FBP[8:0]=0, FBW[21:16]=1, PSM[29:24]=PSMT8 (0x13).
// FBW=1 at bit 16 → 0x0001_0000
// PSM=0x13 at bit 24 → 0x1300_0000
localparam logic [63:0] FRAME_1_VAL = 64'h0000_0000_1301_0000;
// RGBAQ: A=0xCC B=0xBB G=0xAA R=0x55. The PSMT8 emit must take
// ONLY R (0x55) and write nothing for the other channels.
// Lower 32 bits {A, B, G, R} = 0xCCBB_AA55.
localparam logic [63:0] RGBAQ_VAL = 64'h0000_0000_CCBB_AA55;
localparam logic [7:0] EXP_INDEX = 8'h55;
// 5×3 sprite at (1,0)..(5,2). FBW=1 → 64 pixels/row, 1 byte/pixel,
// so row stride is 64 bytes. Sprite pixels span byte_addr:
// row 0: 1, 2, 3, 4, 5
// row 1: 65, 66, 67, 68, 69
// row 2: 129, 130, 131, 132, 133
localparam int SPRITE_X0 = 1;
localparam int SPRITE_Y0 = 0;
localparam int SPRITE_X1 = 5;
localparam int SPRITE_Y1 = 2;
localparam int SPRITE_W = SPRITE_X1 - SPRITE_X0 + 1; // 5
localparam int SPRITE_H = SPRITE_Y1 - SPRITE_Y0 + 1; // 3
localparam int ROW_STRIDE_BYTES = 64;
initial begin
rst_n = 1'b0;
gif_reg_wr_en = 1'b0;
gif_reg_num = 8'd0;
gif_reg_data = 64'd0;
vram_read_addr = 32'd0;
repeat (4) @(posedge clk);
rst_n = 1'b1;
repeat (2) @(posedge clk);
drive_reg(R_PRIM, PRIM_SPRITE);
drive_reg(R_FRAME_1, FRAME_1_VAL);
drive_reg(R_RGBAQ, RGBAQ_VAL);
drive_reg(R_XYZ2, xyz2_data(12'(SPRITE_X0), 12'(SPRITE_Y0)));
drive_reg(R_XYZ2, xyz2_data(12'(SPRITE_X1), 12'(SPRITE_Y1)));
drive_idle();
wait (raster_active == 1'b1);
wait (raster_active == 1'b0);
repeat (10) @(posedge clk);
if (raster_pixel_emit_count != 32'(SPRITE_W * SPRITE_H)) begin
$error("expected %0d emits, got %0d",
SPRITE_W * SPRITE_H, raster_pixel_emit_count);
errors = errors + 1;
end
if (psmt8_emit_seen != SPRITE_W * SPRITE_H) begin
$error("PSMT8 emit observer count=%0d (expected %0d)",
psmt8_emit_seen, SPRITE_W * SPRITE_H);
errors = errors + 1;
end
// Per-sprite-pixel byte must equal R = 0x55.
for (int y = SPRITE_Y0; y <= SPRITE_Y1; y++) begin
for (int x = SPRITE_X0; x <= SPRITE_X1; x++) begin
logic [31:0] byte_addr;
byte_addr = 32'(y * ROW_STRIDE_BYTES + x);
check_byte(byte_addr, EXP_INDEX, "sprite-pixel");
end
end
// Load-bearing per-byte be: byte 0 of row 0 (left of the
// sprite) must stay 0. Without write_be byte gating, a
// 32-bit aligned write at byte 0 of row 0 would stomp it
// with a replicated index when emitting pixel (1,0).
check_byte(32'(SPRITE_Y0 * ROW_STRIDE_BYTES + 0),
8'h00, "left-of-sprite-row0-must-be-zero");
check_byte(32'(SPRITE_Y0 * ROW_STRIDE_BYTES + (SPRITE_X1 + 1)),
8'h00, "right-of-sprite-row0-must-be-zero");
check_byte(32'(SPRITE_Y1 * ROW_STRIDE_BYTES + 0),
8'h00, "left-of-sprite-row2-must-be-zero");
// Bytes between rows must stay 0. Spot-check one mid-row
// byte (row 0, byte 32 — well past the sprite's right edge).
check_byte(32'd32, 8'h00, "mid-row0-must-be-zero");
// Bytes in a row OUTSIDE the sprite's vertical range must
// stay 0. Row 3 is past the sprite.
check_byte(32'(3 * ROW_STRIDE_BYTES + 1), 8'h00, "row3-must-be-zero");
// Channel-isolation check: G/B/A bytes (0xAA/0xBB/0xCC)
// MUST NOT appear anywhere in VRAM. Walk the full 4 KiB
// and assert nothing reads back as one of those values.
// (PSMT8 emit takes ONLY R — the other channels should
// never reach vram_stub.)
begin
int leak;
leak = 0;
for (int addr = 0; addr < 4096; addr += 4) begin
vram_read_addr = addr[31:0];
#1;
for (int b = 0; b < 4; b++) begin
logic [7:0] v;
v = vram_read_data[b*8 +: 8];
if (v == 8'hAA || v == 8'hBB || v == 8'hCC) begin
$error("leak: byte 0x%08x = 0x%02x (G/B/A leaked into VRAM)",
addr + b, v);
leak = leak + 1;
end
end
end
if (leak != 0) errors = errors + leak;
end
$display("[tb_gs_raster_psmt8] sprite=%0dx%0d index=0x%02x emits=%0d",
SPRITE_W, SPRITE_H, EXP_INDEX, raster_pixel_emit_count);
if (errors == 0) $display("[tb_gs_raster_psmt8] PASS");
else $display("[tb_gs_raster_psmt8] FAIL");
$finish;
end
initial begin
#5000000;
$error("[tb_gs_raster_psmt8] timeout");
$finish;
end
endmodule : tb_gs_raster_psmt8