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

509 lines
21 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_psmt4 (Ch106)
//
// Locks the contract for gs_stub's WRITE-side PSMT4 emit. PSMT4
// packs 2 pixels per byte (low nibble = even pixel, high nibble =
// odd pixel within a row), so each emit is a sub-byte write — the
// nibble NOT being written must be preserved.
//
// The Ch106 mechanism: gs_stub places the 4-bit index in the
// targeted nibble position of write_data[7:0], drives
// `raster_pixel_be_q = 4'b0001` (1-byte commit) and
// `raster_pixel_mask_q = 32'h0000_000F` (low nibble) or
// `32'h0000_00F0` (high nibble). vram_stub commits each byte as
// mem[addr] <= (mem[addr] & ~mask[7:0]) | (data[7:0] & mask[7:0])
// which is the read-modify-write the indexed format requires.
// Back-to-back same-byte emits chain through NBA semantics — the
// second NBA samples mem[addr] AFTER the first commit applies.
//
// This TB is INTENTIONALLY adversarial about preservation:
// - VRAM is preloaded with a non-zero pattern (0xA5 everywhere
// in the sprite's row range) BEFORE raster runs, so a buggy
// full-byte write would visibly clobber the un-touched
// nibble.
// - The sprite spans both an even-x start (low-nibble first
// within each pair-of-pixels) AND an odd-x start (high-
// nibble first), exercising both nibble positions as the
// leading pixel of a pair.
// - A single-pixel SPRITE drawn on top of the preloaded byte
// proves that ONE nibble update preserves the OTHER nibble
// even with NO companion pixel in the same emit run.
//
// Setup (Phase A — even-aligned, full-byte coverage):
// PRIM = SPRITE
// FRAME_1: FBP=0, FBW=1, PSM=PSMT4 (0x14)
// FBW=1 at bit 16 → 0x0001_0000
// PSM=0x14 at bit 24 → 0x1400_0000
// RGBAQ A=0xCC B=0xBB G=0xAA R=0x05 → PSMT4 index = R[3:0] = 5.
// v1 = (0,0); v2 = (3,1) → 4×2 sprite, all pairs (x even / x+1
// odd) land in the same byte → emits both nibbles of each
// byte and the full byte ends up = 0x55. fb_addr formula:
// pixel_index = y*64 + x
// byte_addr = pixel_index >> 1
// So row 0 sprite spans bytes 0..1, row 1 spans bytes 32..33.
//
// Setup (Phase B — odd-aligned start, isolated high-nibble write):
// New SPRITE with v1=(5,2) → v2=(5,2), single pixel at x=5
// (odd → high nibble). Index = R[3:0] = 7. Targets byte 2 of
// row 2: byte_addr = (2*64 + 5) >> 1 = 66; nibble = high.
// Pre-load mem[66] = 0xA5. Expected after: mem[66] = 0x75 (low
// nibble 5 preserved; high nibble updated to 7).
//
// Setup (Phase C — odd-aligned start, isolated low-nibble write):
// SPRITE v1=(4,3)..v2=(4,3), single pixel at x=4 (even → low
// nibble). Index = 0x9. Pre-load mem[(3*64+4)>>1=98] = 0xA5.
// Expected after: mem[98] = 0xA9 (high nibble A preserved; low
// nibble updated to 9).
//
// Setup (Phase D — distinct adjacent nibbles in the SAME byte,
// composed across two emits with DIFFERENT index values):
// SPRITE is flat-color, so we use TWO single-pixel SPRITEs at
// adjacent x coords landing in the same byte, each with a
// different R. SPRITE 1: (36, 1) with R=0x3 → low nibble.
// SPRITE 2: (37, 1) with R=0xC → high nibble. Both target
// byte_addr = (1*64 + 36) >> 1 = 50. Pre-load mem[50] = 0xA5.
// Expected after the two emits: mem[50] = 0xC3 — a composed
// byte with NON-IDENTICAL nibbles, proving that the second
// emit (high) does not stomp the first emit's freshly-written
// low nibble. Phase A's all-5s pattern proved accumulation but
// not distinct composition; Phase D ties that race down by
// forcing the result to encode the order-independent merge.
//
// PASS criteria:
// - 8 + 1 + 1 + 2 = 12 raster_pixel_emit pulses total across phases
// - raster_pixel_psm_q == 6'h14 on every emit
// - raster_pixel_be_q == 4'b0001 on every emit
// - raster_pixel_mask_q ∈ {0x0F, 0xF0} on every emit
// - Phase A: each sprite byte ends at 0x55, neighbouring bytes
// stay at 0xA5 (preload preserved).
// - Phase B: mem[66] = 0x75 (high nibble updated, low nibble
// preserved at 5).
// - Phase C: mem[98] = 0xA9 (low nibble updated, high nibble
// preserved at A).
// - No 0xAA / 0xBB / 0xCC byte appears anywhere — channel
// isolation: only R[3:0] reaches VRAM.
`timescale 1ns/1ps
module tb_gs_raster_psmt4;
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 [31:0] raster_pixel_mask_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_mask_q(raster_pixel_mask_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)
);
// VRAM with TB-side preload mux: we drive synthetic preload
// writes into vram_stub's write port BEFORE raster runs by
// muxing the TB's preload signals onto write_*. During raster,
// gs_stub drives write_*. A single-bit `tb_preload_active` flag
// selects.
logic tb_preload_active;
logic tb_preload_en;
logic [31:0] tb_preload_addr;
logic [31:0] tb_preload_data;
logic [3:0] tb_preload_be;
logic [31:0] tb_preload_mask;
logic vram_write_en;
logic [31:0] vram_write_addr;
logic [31:0] vram_write_data;
logic [3:0] vram_write_be;
logic [31:0] vram_write_mask;
assign vram_write_en = tb_preload_active ? tb_preload_en : raster_pixel_emit;
assign vram_write_addr = tb_preload_active ? tb_preload_addr : raster_pixel_fb_addr_q;
assign vram_write_data = tb_preload_active ? tb_preload_data : raster_pixel_color_q[31:0];
assign vram_write_be = tb_preload_active ? tb_preload_be : raster_pixel_be_q;
assign vram_write_mask = tb_preload_active ? tb_preload_mask : raster_pixel_mask_q;
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 (vram_write_en),
.write_addr(vram_write_addr),
.write_data(vram_write_data),
.write_be (vram_write_be),
.write_mask(vram_write_mask),
.read_addr (vram_read_addr),
.read_data (vram_read_data),
.read2_addr(32'd0),
.read2_data()
);
int errors;
int psmt4_emit_seen;
initial begin
errors = 0;
psmt4_emit_seen = 0;
end
// Continuous observer for PSMT4 emits.
always_ff @(posedge clk) begin
if (rst_n && !tb_preload_active && raster_pixel_emit) begin
psmt4_emit_seen <= psmt4_emit_seen + 1;
if (raster_pixel_psm_q !== 6'h14) begin
$error("emit %0d: raster_pixel_psm_q=0x%02x (expected 0x14 PSMT4)",
psmt4_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)",
psmt4_emit_seen, raster_pixel_be_q);
errors <= errors + 1;
end
if (raster_pixel_mask_q !== 32'h0000_000F &&
raster_pixel_mask_q !== 32'h0000_00F0) begin
$error("emit %0d: raster_pixel_mask_q=0x%08x (expected 0x0F or 0xF0)",
psmt4_emit_seen, raster_pixel_mask_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
task automatic preload_byte(input logic [31:0] byte_addr,
input logic [7:0] val);
// Write a single byte using vram_stub's per-byte be.
@(negedge clk);
tb_preload_active = 1'b1;
tb_preload_en = 1'b1;
tb_preload_addr = byte_addr;
tb_preload_data = {24'd0, val};
tb_preload_be = 4'b0001;
tb_preload_mask = 32'hFFFF_FFFF;
@(posedge clk);
@(negedge clk);
tb_preload_en = 1'b0;
tb_preload_active = 1'b0;
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_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=0 / FBW=1 / PSM=0x14 (PSMT4).
localparam logic [63:0] FRAME_1_VAL = 64'h0000_0000_1401_0000;
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;
tb_preload_active = 1'b0;
tb_preload_en = 1'b0;
tb_preload_addr = 32'd0;
tb_preload_data = 32'd0;
tb_preload_be = 4'd0;
tb_preload_mask = 32'hFFFF_FFFF;
repeat (4) @(posedge clk);
rst_n = 1'b1;
repeat (2) @(posedge clk);
// ----------------------------------------------------------------
// Pre-load adversarial pattern at every byte we'll touch (and
// a couple bytes adjacent for neighbour-preservation).
// ----------------------------------------------------------------
for (int a = 0; a < 4; a++) preload_byte(32'(a), 8'hA5); // Phase A row 0 + neighbours
for (int a = 32; a < 36; a++) preload_byte(32'(a), 8'hA5); // Phase A row 1 + neighbours
preload_byte(32'd66, 8'hA5); // Phase B target byte
preload_byte(32'd98, 8'hA5); // Phase C target byte
preload_byte(32'd50, 8'hA5); // Phase D target byte
// ----------------------------------------------------------------
// Phase A: 4×2 sprite at (0,0)..(3,1). Index = R[3:0] = 5.
// 8 emits across bytes 0,0,1,1,32,32,33,33. Each pair
// (low+high) of the same byte should accumulate to 0x55.
// ----------------------------------------------------------------
drive_reg(R_PRIM, PRIM_SPRITE);
drive_reg(R_FRAME_1, FRAME_1_VAL);
drive_reg(R_RGBAQ, 64'h0000_0000_CCBB_AA05);
drive_reg(R_XYZ2, xyz2_data(12'd0, 12'd0));
drive_reg(R_XYZ2, xyz2_data(12'd3, 12'd1));
drive_idle();
wait (raster_active == 1'b1);
wait (raster_active == 1'b0);
repeat (5) @(posedge clk);
// Phase A asserts.
check_byte(32'd0, 8'h55, "phaseA-byte0");
check_byte(32'd1, 8'h55, "phaseA-byte1");
check_byte(32'd32, 8'h55, "phaseA-byte32");
check_byte(32'd33, 8'h55, "phaseA-byte33");
// Neighbour bytes should still be the preload value.
check_byte(32'd2, 8'hA5, "phaseA-byte2-untouched");
check_byte(32'd3, 8'hA5, "phaseA-byte3-untouched");
check_byte(32'd34, 8'hA5, "phaseA-byte34-untouched");
check_byte(32'd35, 8'hA5, "phaseA-byte35-untouched");
// ----------------------------------------------------------------
// Phase B: single pixel at (5, 2). x=5 odd → high nibble.
// pixel_index = 2*64 + 5 = 133; byte_addr = 66; nibble=high.
// Index = R[3:0] = 7. Expected mem[66] = 0x75 (low nibble
// 5 from preload preserved; high nibble updated from A→7).
// ----------------------------------------------------------------
drive_reg(R_PRIM, PRIM_SPRITE);
drive_reg(R_FRAME_1, FRAME_1_VAL);
drive_reg(R_RGBAQ, 64'h0000_0000_CCBB_AA07);
drive_reg(R_XYZ2, xyz2_data(12'd5, 12'd2));
drive_reg(R_XYZ2, xyz2_data(12'd5, 12'd2));
drive_idle();
wait (raster_active == 1'b1);
wait (raster_active == 1'b0);
repeat (5) @(posedge clk);
// Wait — the preload was 0xA5 (high=A, low=5). After
// overwriting high nibble with 7, expect 0x75.
check_byte(32'd66, 8'h75, "phaseB-high-nibble-rmw");
// ----------------------------------------------------------------
// Phase C: single pixel at (4, 3). x=4 even → low nibble.
// pixel_index = 3*64 + 4 = 196; byte_addr = 98; nibble=low.
// Index = R[3:0] = 9. Expected mem[98] = 0xA9 (high nibble
// A from preload preserved; low nibble updated from 5→9).
// ----------------------------------------------------------------
drive_reg(R_PRIM, PRIM_SPRITE);
drive_reg(R_FRAME_1, FRAME_1_VAL);
drive_reg(R_RGBAQ, 64'h0000_0000_CCBB_AA09);
drive_reg(R_XYZ2, xyz2_data(12'd4, 12'd3));
drive_reg(R_XYZ2, xyz2_data(12'd4, 12'd3));
drive_idle();
wait (raster_active == 1'b1);
wait (raster_active == 1'b0);
repeat (5) @(posedge clk);
check_byte(32'd98, 8'hA9, "phaseC-low-nibble-rmw");
// ----------------------------------------------------------------
// Phase D: distinct adjacent nibbles in the same byte, written
// across TWO emits with DIFFERENT R values. SPRITE is flat-
// color so we issue two single-pixel SPRITEs back-to-back.
// - sprite 1 at (36, 1) with R=0x3 → byte 50 low nibble = 3
// - sprite 2 at (37, 1) with R=0xC → byte 50 high nibble = C
// Preload mem[50] = 0xA5. Expected after both emits: 0xC3 (a
// distinct-nibble composition that NO single uniform R could
// produce, locking the per-emit nibble-merge claim tighter
// than Phase A's all-5s accumulation).
// ----------------------------------------------------------------
drive_reg(R_PRIM, PRIM_SPRITE);
drive_reg(R_FRAME_1, FRAME_1_VAL);
drive_reg(R_RGBAQ, 64'h0000_0000_CCBB_AA03);
drive_reg(R_XYZ2, xyz2_data(12'd36, 12'd1));
drive_reg(R_XYZ2, xyz2_data(12'd36, 12'd1));
drive_idle();
wait (raster_active == 1'b1);
wait (raster_active == 1'b0);
repeat (5) @(posedge clk);
// Spot-check after sprite 1: low nibble updated 5→3, high
// nibble still A.
check_byte(32'd50, 8'hA3, "phaseD-after-sprite1-low-write");
drive_reg(R_PRIM, PRIM_SPRITE);
drive_reg(R_FRAME_1, FRAME_1_VAL);
drive_reg(R_RGBAQ, 64'h0000_0000_CCBB_AA0C);
drive_reg(R_XYZ2, xyz2_data(12'd37, 12'd1));
drive_reg(R_XYZ2, xyz2_data(12'd37, 12'd1));
drive_idle();
wait (raster_active == 1'b1);
wait (raster_active == 1'b0);
repeat (5) @(posedge clk);
// Final composed byte: high nibble updated A→C, low nibble
// preserved from the sprite-1 write at 3.
check_byte(32'd50, 8'hC3, "phaseD-distinct-nibble-composition");
// ----------------------------------------------------------------
// Aggregate counts.
// ----------------------------------------------------------------
if (raster_pixel_emit_count != 32'd12) begin
$error("expected 12 emits total, got %0d", raster_pixel_emit_count);
errors = errors + 1;
end
if (psmt4_emit_seen != 12) begin
$error("PSMT4 emit observer count=%0d (expected 12)", psmt4_emit_seen);
errors = errors + 1;
end
// ----------------------------------------------------------------
// Channel-isolation sweep: the only non-preload bytes ever
// committed should be the sprite pixels. Walk all 4 KiB and
// assert NO byte equals 0xAA / 0xBB / 0xCC (G/B/A leaks).
// ----------------------------------------------------------------
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_psmt4] phaseA=4x2 phaseB=high-rmw phaseC=low-rmw phaseD=distinct-compose emits=%0d",
raster_pixel_emit_count);
if (errors == 0) $display("[tb_gs_raster_psmt4] PASS");
else $display("[tb_gs_raster_psmt4] FAIL");
$finish;
end
initial begin
#5000000;
$error("[tb_gs_raster_psmt4] timeout");
$finish;
end
endmodule : tb_gs_raster_psmt4