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

330 lines
13 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_psmct16 (Ch95)
//
// Locks the contract for gs_stub's WRITE-side PSMCT16 emit. The
// raster pipeline (Ch88: S0/S1/S2) now packs ABGR → RGB5A1 when
// FRAME_1.PSM = PSMCT16 (= 0x02), and emits a 4-bit `write_be`
// alongside the data so vram_stub commits exactly 2 bytes per
// pixel without stomping the neighboring halfword.
//
// Setup:
// PRIM = SPRITE
// FRAME_1: FBP=0, FBW=1, PSM=PSMCT16
// The PSM bits are at FRAME_1[29:24]. PSMCT16 = 0x02.
// RGBAQ: pick channels with distinct high-5-bit values so the
// packed RGB5A1 is non-degenerate. Use R=0x84, G=0xC6, B=0x42,
// A=0x80 (matches the Ch94 read-side test colors after 5→8
// expansion, except A=0x80 instead of 0; the alpha bit is
// informational).
// v1 = (0, 0); v2 = (3, 3) → 4×4 sprite.
//
// Expected per-pixel pack:
// r5 = 0x84[7:3] = 0x10
// g5 = 0xC6[7:3] = 0x18
// b5 = 0x42[7:3] = 0x08
// a1 = 0x80[7] = 1
// pix16 = {a1, b5, g5, r5} = (1 << 15) | (0x08 << 10) | (0x18 << 5) | 0x10
// = 0x8000 | 0x2000 | 0x0300 | 0x10 = 0xA310
//
// Expected VRAM after raster (FBW=1 → 64 pixels/row × 2 bytes
// = 128 bytes/row stride; sprite 4 wide × 4 tall):
// Row 0 at byte 0: [pix0=0xA310 | pix1=0xA310 | pix2=0xA310 | pix3=0xA310]
// = mem[0..7] = {0x10,0xA3, 0x10,0xA3, 0x10,0xA3, 0x10,0xA3}
// Row 1 at byte 128: same pattern at bytes 128..135
// Row 2 at byte 256: bytes 256..263
// Row 3 at byte 384: bytes 384..391
// Bytes 8..127 of row 0 (and similarly for other rows) MUST
// stay zero (no halfword leak from the next row's writes or
// from the sprite's own boundary).
//
// PASS criteria:
// - 16 raster_pixel_emit pulses
// - Every expected sprite halfword reads back as 0xA310
// - Specifically, the halfword right after the sprite on each
// row (byte 8..9 / 136..137 / etc.) reads as 0 — proves the
// write_be byte gating works (without it, a 32-bit write at
// byte 6 would stomp bytes 8..9 with the duplicate halfword).
`timescale 1ns/1ps
module tb_gs_raster_psmct16;
logic clk;
logic rst_n;
initial clk = 1'b0;
always #5 clk = ~clk;
// gs_stub IO
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 psmct16_emit_seen;
initial begin
errors = 0;
psmct16_emit_seen = 0;
end
// Sanity: while emitting PSMCT16, raster_pixel_be_q should be
// 4'b0011 (gs_stub uses byte-addressed write_addr so the same
// be works for both even and odd pixels), and raster_pixel_psm_q
// should be 6'h02.
always_ff @(posedge clk) begin
if (rst_n && raster_pixel_emit) begin
psmct16_emit_seen <= psmct16_emit_seen + 1;
if (raster_pixel_psm_q !== 6'h02) begin
$error("emit %0d: raster_pixel_psm_q=0x%02x (expected 0x02 PSMCT16)",
psmct16_emit_seen, raster_pixel_psm_q);
errors <= errors + 1;
end
if (raster_pixel_be_q !== 4'b0011) begin
$error("emit %0d: raster_pixel_be_q=%b (expected 4'b0011 for PSMCT16)",
psmct16_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_halfword(input logic [31:0] byte_addr,
input logic [15:0] expected,
input string tag);
logic [15:0] got;
// vram_stub returns 4 bytes starting at read_addr; read at
// a 4-byte-aligned address that contains the target
// halfword, then mux based on byte_addr[1].
vram_read_addr = byte_addr & ~32'd3;
#1;
got = byte_addr[1] ? vram_read_data[31:16] : vram_read_data[15:0];
if (got !== expected) begin
$error("[%s] @byte 0x%08x got 0x%04x expected 0x%04x",
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]=PSMCT16(0x02).
// FBW=1 at bit 16 → 0x0001_0000
// PSM=2 at bit 24 → 0x0200_0000
localparam logic [63:0] FRAME_1_VAL = 64'h0000_0000_0201_0000;
// RGBAQ: A=0x80 B=0x42 G=0xC6 R=0x84.
// Lower 32 bits {A, B, G, R} = 0x8042_C684.
localparam logic [63:0] RGBAQ_VAL = 64'h0000_0000_8042_C684;
// Expected packed PSMCT16 pixel.
localparam logic [4:0] EXP_R5 = 5'h10;
localparam logic [4:0] EXP_G5 = 5'h18;
localparam logic [4:0] EXP_B5 = 5'h08;
localparam logic EXP_A1 = 1'b1;
localparam logic [15:0] EXP_PIX = {EXP_A1, EXP_B5, EXP_G5, EXP_R5};
localparam int SPRITE_W = 4;
localparam int SPRITE_H = 4;
// FBW=1 → 64 pixels/row × 2 bytes = 128 bytes/row stride.
localparam int ROW_STRIDE_BYTES = 128;
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'd0, 12'd0));
drive_reg(R_XYZ2, xyz2_data(12'(SPRITE_W - 1),
12'(SPRITE_H - 1)));
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 (psmct16_emit_seen != SPRITE_W * SPRITE_H) begin
$error("PSMCT16 emit observer count=%0d (expected %0d)",
psmct16_emit_seen, SPRITE_W * SPRITE_H);
errors = errors + 1;
end
// Verify each sprite pixel landed as the right RGB5A1 halfword.
for (int y = 0; y < SPRITE_H; y++) begin
for (int x = 0; x < SPRITE_W; x++) begin
logic [31:0] byte_addr;
byte_addr = (y * ROW_STRIDE_BYTES) + (x * 2);
check_halfword(byte_addr, EXP_PIX, "sprite-pixel");
end
end
// Load-bearing: the halfword RIGHT AFTER the sprite on
// each row must be 0. Without write_be byte gating, a
// 32-bit write at the rightmost sprite pixel (e.g. byte 6
// for row 0) would stomp bytes 8..9 with the duplicate
// halfword. With write_be active, those bytes stay 0.
for (int y = 0; y < SPRITE_H; y++) begin
logic [31:0] right_of_sprite;
right_of_sprite = (y * ROW_STRIDE_BYTES) + (SPRITE_W * 2);
check_halfword(right_of_sprite, 16'h0000, "right-of-sprite-must-be-zero");
end
// Also: bytes between rows (e.g. bytes 8..127 on row 0)
// should mostly be zero. Spot-check one.
check_halfword(32'd64, 16'h0000, "mid-row-must-be-zero");
$display("[tb_gs_raster_psmct16] sprite=%0dx%0d pix16=0x%04x emits=%0d",
SPRITE_W, SPRITE_H, EXP_PIX, raster_pixel_emit_count);
if (errors == 0) $display("[tb_gs_raster_psmct16] PASS");
else $display("[tb_gs_raster_psmct16] FAIL");
$finish;
end
initial begin
#5000000;
$error("[tb_gs_raster_psmct16] timeout");
$finish;
end
endmodule : tb_gs_raster_psmct16