Files
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

497 lines
18 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_clut_load (Ch99)
//
// Locks the contract for TEX0.CLD-driven VRAM→CLUT load. Pre-Ch99
// the CLUT was always TB-direct-programmed; now a TEX0_1 write
// with CLD != 0 (and CPSM=PSMCT32) triggers `clut_loader_stub`
// to copy 256 PSMCT32 entries from VRAM[CBP*256] into clut_stub.
//
// Setup (no gs_stub raster — VRAM written directly):
// - VRAM[0..255] holds a PSMT8 sprite of indices 0x10..0x1F
// (4×4 pixels at 1 byte/pixel).
// - VRAM[1024..] holds 256 PSMCT32 CLUT entries; the entries
// the sprite uses (0x10..0x1F) are programmed
// with the same `clut_entry(i)` formula as
// Ch97 (R=i, G=i+0x40, B=i+0x80, A=0xFF).
// - TEX0_1: CBP=4 (=1024 bytes), CPSM=PSMCT32 (0), CSM=1
// (CSM2 linear), CSA=0, CLD=1 ("always load").
// - Drive TEX0_1 via gs_stub.gif_reg_*; loader's load_busy
// pulses high for 256 cycles; TB waits for the falling edge.
// - Configure DISPFB1.PSM=PSMT8, DISPLAY1 (full active),
// PMODE.EN1=1, capture frame, verify each in-sprite pixel
// reads the correct CLUT-decoded RGB.
//
// Out of scope (deferred):
// - CLD modes 2..7 (load conditional on CBP/CPSM/CSA changes).
// - CPSM != PSMCT32 (PSMCT16 CLUT entries with 5→8 expansion).
// - CSA partial-window loads (CLD=4).
// - Real TEX0_1 in-flight collision / re-trigger semantics.
`timescale 1ns/1ps
module tb_gs_clut_load;
localparam int PCRTC_H_ACTIVE = 16;
localparam int PCRTC_V_ACTIVE = 8;
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 priv_reg_wr_en;
logic [15:0] priv_reg_wr_addr;
logic [63:0] priv_reg_wr_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 [63:0] tex0_1_q;
logic [13:0] tex0_1_cbp_q;
logic [3:0] tex0_1_cpsm_q;
logic tex0_1_csm_q;
logic [4:0] tex0_1_csa_q;
logic [2:0] tex0_1_cld_q;
logic tex0_1_wr_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 (priv_reg_wr_en),
.reg_wr_addr(priv_reg_wr_addr),
.reg_wr_data(priv_reg_wr_data),
.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),
.tex0_1_q(tex0_1_q),
.tex0_1_cbp_q(tex0_1_cbp_q),
.tex0_1_cpsm_q(tex0_1_cpsm_q),
.tex0_1_csm_q(tex0_1_csm_q),
.tex0_1_csa_q(tex0_1_csa_q),
.tex0_1_cld_q(tex0_1_cld_q),
.tex0_1_wr_q(tex0_1_wr_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)
);
// VRAM (TB-direct write port for sprite + CLUT-source bytes;
// pcrtc reads via port 0; loader reads via port 1).
logic vram_we;
logic [31:0] vram_waddr;
logic [31:0] vram_wdata;
logic [3:0] vram_wbe;
logic [31:0] vram_raddr;
logic [31:0] vram_rdata;
logic [31:0] vram_raddr2;
logic [31:0] vram_rdata2;
vram_stub #(.BYTES(8192)) 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 (vram_raddr),
.read_data (vram_rdata),
.read2_addr(vram_raddr2),
.read2_data(vram_rdata2)
);
// CLUT — write port driven by clut_loader_stub; read port
// consumed by gs_pcrtc_stub.
logic clut_we;
logic [7:0] clut_widx;
logic [31:0] clut_wdata;
logic [7:0] clut_ridx;
logic [31:0] clut_rdata;
clut_stub u_clut (
.clk(clk), .rst_n(rst_n),
.write_en (clut_we),
.write_idx (clut_widx),
.write_data(clut_wdata),
.read_idx (clut_ridx),
.read_data (clut_rdata)
);
// The loader: TEX0_1 fields → VRAM read port 1 → clut_stub.
logic loader_busy;
clut_loader_stub u_loader (
.clk(clk), .rst_n(rst_n),
.tex0_wr_pulse (tex0_1_wr_q),
.tex0_cbp (tex0_1_cbp_q),
.tex0_cpsm (tex0_1_cpsm_q),
.tex0_csm (tex0_1_csm_q),
.tex0_csa (tex0_1_csa_q),
.tex0_cld (tex0_1_cld_q),
.vram_read_addr (vram_raddr2),
.vram_read_data (vram_rdata2),
.clut_write_en (clut_we),
.clut_write_idx (clut_widx),
.clut_write_data(clut_wdata),
.load_busy (loader_busy)
);
logic hsync_o, vsync_o, de_o;
logic [7:0] r_o, g_o, b_o;
logic pcrtc_ev_valid;
trace_pkg::subsys_e pcrtc_ev_subsys;
trace_pkg::event_e pcrtc_ev_event;
logic [63:0] pcrtc_ev_arg0, pcrtc_ev_arg1;
logic [63:0] pcrtc_ev_arg2, pcrtc_ev_arg3;
logic [31:0] pcrtc_ev_flags;
gs_pcrtc_stub #(
.H_ACTIVE(PCRTC_H_ACTIVE), .H_FRONT(1), .H_SYNC(1), .H_BACK(1),
.V_ACTIVE(PCRTC_V_ACTIVE), .V_FRONT(1), .V_SYNC(1), .V_BACK(1)
) u_pcrtc (
.clk(clk), .rst_n(rst_n),
.pmode_q (pmode_q),
.dispfb1_q (dispfb1_q),
.display1_q (display1_q),
.vram_read_addr(vram_raddr),
.vram_read_data(vram_rdata),
.clut_enable (1'b1),
.clut_csa (tex0_1_csa_q),
.clut_read_idx (clut_ridx),
.clut_read_data(clut_rdata),
.hsync(hsync_o), .vsync(vsync_o), .de(de_o),
.r(r_o), .g(g_o), .b(b_o),
.ev_valid(pcrtc_ev_valid),
.ev_subsys(pcrtc_ev_subsys),
.ev_event(pcrtc_ev_event),
.ev_arg0(pcrtc_ev_arg0), .ev_arg1(pcrtc_ev_arg1),
.ev_arg2(pcrtc_ev_arg2), .ev_arg3(pcrtc_ev_arg3),
.ev_flags(pcrtc_ev_flags)
);
logic [7:0] cap_r [0:PCRTC_V_ACTIVE-1][0:PCRTC_H_ACTIVE-1];
logic [7:0] cap_g [0:PCRTC_V_ACTIVE-1][0:PCRTC_H_ACTIVE-1];
logic [7:0] cap_b [0:PCRTC_V_ACTIVE-1][0:PCRTC_H_ACTIVE-1];
logic cap_de[0:PCRTC_V_ACTIVE-1][0:PCRTC_H_ACTIVE-1];
int errors;
bit capture_armed;
initial begin
for (int y = 0; y < PCRTC_V_ACTIVE; y++)
for (int x = 0; x < PCRTC_H_ACTIVE; x++) begin
cap_r[y][x] = 8'd0;
cap_g[y][x] = 8'd0;
cap_b[y][x] = 8'd0;
cap_de[y][x] = 1'b0;
end
errors = 0;
capture_armed = 1'b0;
end
always_ff @(posedge clk) begin
if (rst_n && capture_armed && de_o
&& (u_pcrtc.vcnt < PCRTC_V_ACTIVE)
&& (u_pcrtc.hcnt < PCRTC_H_ACTIVE)) begin
cap_r [u_pcrtc.vcnt][u_pcrtc.hcnt] <= r_o;
cap_g [u_pcrtc.vcnt][u_pcrtc.hcnt] <= g_o;
cap_b [u_pcrtc.vcnt][u_pcrtc.hcnt] <= b_o;
cap_de[u_pcrtc.vcnt][u_pcrtc.hcnt] <= 1'b1;
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 drive_priv(input logic [15:0] addr,
input logic [63:0] data);
@(negedge clk);
priv_reg_wr_en = 1'b1;
priv_reg_wr_addr = addr;
priv_reg_wr_data = data;
@(posedge clk);
@(negedge clk);
priv_reg_wr_en = 1'b0;
priv_reg_wr_addr = 16'd0;
priv_reg_wr_data = 64'd0;
endtask
task automatic vram_write32(input logic [31:0] addr,
input logic [31:0] data,
input logic [3:0] be);
@(negedge clk);
vram_we = 1'b1;
vram_waddr = addr;
vram_wdata = data;
vram_wbe = be;
@(posedge clk);
@(negedge clk);
vram_we = 1'b0;
vram_waddr = 32'd0;
vram_wdata = 32'd0;
vram_wbe = 4'b0000;
endtask
function automatic logic [31:0] clut_entry(input logic [7:0] i);
// Same formula as Ch97/Ch98 — A=0xFF, B=i+0x80, G=i+0x40, R=i.
logic [7:0] r8, g8, b8, a8;
r8 = i;
g8 = i + 8'h40;
b8 = i + 8'h80;
a8 = 8'hFF;
return {a8, b8, g8, r8};
endfunction
function automatic logic [63:0] tex0_pack(
input logic [13:0] cbp,
input logic [3:0] cpsm,
input logic csm,
input logic [4:0] csa,
input logic [2:0] cld);
logic [63:0] v;
v = 64'd0;
v[50:37] = cbp;
v[54:51] = cpsm;
v[55] = csm;
v[60:56] = csa;
v[63:61] = cld;
return v;
endfunction
localparam logic [7:0] GIF_TEX0_1 = 8'h06;
localparam logic [15:0] PMODE_OFF = 16'h0000;
localparam logic [15:0] DISPFB1_OFF = 16'h0070;
localparam logic [15:0] DISPLAY1_OFF = 16'h0080;
localparam int SPRITE_W = 4;
localparam int SPRITE_H = 4;
localparam logic [7:0] BASE_IDX = 8'h10;
localparam int ROW_STRIDE = 64; // PSMT8 + FBW=1
localparam int CLUT_CBP = 4; // CBP*256 = 1024 bytes
localparam logic [31:0] CLUT_BASE = 32'd1024;
localparam logic [63:0] DISPFB1_VAL = 64'h0000_0000_0009_8200; // PSMT8/FBW=1/FBP=0
localparam logic [63:0] DISPLAY1_VAL =
(64'(PCRTC_H_ACTIVE - 1) << 32)
| (64'(PCRTC_V_ACTIVE - 1) << 44);
localparam logic [63:0] PMODE_EN1 = 64'h0000_0000_0000_0001;
initial begin
rst_n = 1'b0;
gif_reg_wr_en = 1'b0;
gif_reg_num = 8'd0;
gif_reg_data = 64'd0;
priv_reg_wr_en = 1'b0;
priv_reg_wr_addr = 16'd0;
priv_reg_wr_data = 64'd0;
vram_we = 1'b0;
vram_waddr = 32'd0;
vram_wdata = 32'd0;
vram_wbe = 4'b0000;
repeat (4) @(posedge clk);
rst_n = 1'b1;
repeat (2) @(posedge clk);
// Stage the PSMT8 sprite at VRAM bytes 0..15 (4×4).
for (int y = 0; y < SPRITE_H; y++) begin
logic [7:0] i0, i1, i2, i3;
logic [31:0] data;
int row_base;
i0 = BASE_IDX + 8'(y * SPRITE_W + 0);
i1 = BASE_IDX + 8'(y * SPRITE_W + 1);
i2 = BASE_IDX + 8'(y * SPRITE_W + 2);
i3 = BASE_IDX + 8'(y * SPRITE_W + 3);
data = {i3, i2, i1, i0};
row_base = y * ROW_STRIDE;
vram_write32(row_base, data, 4'b1111);
end
// Stage the 256-entry PSMCT32 CLUT in VRAM at CLUT_BASE.
// Only the entries the sprite uses (0x10..0x1F) carry
// distinguishable values; the rest fill in with their
// own clut_entry() values for completeness.
for (int i = 0; i < 256; i++) begin
logic [31:0] addr;
addr = CLUT_BASE + 32'(i * 4);
vram_write32(addr, clut_entry(8'(i)), 4'b1111);
end
// ----------------------------------------------------------------
// Ch99 audit-medium negative test: a TEX0_1 write with
// CSM=0 (CSM1 swizzle) must NOT trigger a load — CSM1 is
// out of scope at this chapter, and silently treating it
// as CSM2-linear would deposit garbage in clut_stub. We
// arm with CLD=1 + CPSM=PSMCT32 + CSM=0; assert loader
// stays idle for several cycles, AND assert a different
// CBP so any spurious load WOULD have moved data.
// ----------------------------------------------------------------
drive_reg(GIF_TEX0_1, tex0_pack(14'd0, 4'd0, 1'b0, 5'd0, 3'd1));
drive_idle();
repeat (16) @(posedge clk);
if (loader_busy !== 1'b0) begin
$error("CSM=0 TEX0 write started a load (loader_busy=%b)", loader_busy);
errors = errors + 1;
end
// Trigger the loader by writing TEX0_1. CSM=1 (CSM2),
// CSA=0, CLD=1 ("always load"). CPSM=0 (PSMCT32).
drive_reg(GIF_TEX0_1, tex0_pack(14'(CLUT_CBP), 4'd0, 1'b1, 5'd0, 3'd1));
drive_idle();
// Wait for the loader to start, then complete.
wait (loader_busy == 1'b1);
wait (loader_busy == 1'b0);
repeat (4) @(posedge clk);
// (CLUT contents are spot-checked indirectly by the
// per-pixel scanout assertions below — pcrtc drives
// clut_ridx so the TB can't probe it directly.)
// Configure scanout.
drive_priv(DISPFB1_OFF, DISPFB1_VAL);
drive_priv(DISPLAY1_OFF, DISPLAY1_VAL);
drive_priv(PMODE_OFF, PMODE_EN1);
@(posedge u_pcrtc.end_of_frame);
@(posedge clk);
capture_armed = 1'b1;
@(posedge u_pcrtc.end_of_frame);
@(posedge clk);
capture_armed = 1'b0;
// Per-pixel verification.
for (int y = 0; y < PCRTC_V_ACTIVE; y++) begin
for (int x = 0; x < PCRTC_H_ACTIVE; x++) begin
logic [7:0] er, eg, eb;
bit in_sprite;
logic [7:0] idx;
logic [31:0] entry;
in_sprite = (x < SPRITE_W) && (y < SPRITE_H);
if (in_sprite) begin
idx = BASE_IDX + 8'(y * SPRITE_W + x);
end else begin
// Untouched VRAM bytes read as 0 → idx=0; the
// loaded CLUT[0] = clut_entry(0) which is
// non-zero. Match against that, not (0,0,0).
idx = 8'd0;
end
entry = clut_entry(idx);
er = entry[7:0];
eg = entry[15:8];
eb = entry[23:16];
if (!cap_de[y][x]) begin
$error("(%0d,%0d) DE never asserted", x, y);
errors = errors + 1;
end
if (cap_r[y][x] !== er || cap_g[y][x] !== eg || cap_b[y][x] !== eb) begin
$error("(%0d,%0d) got (%02x,%02x,%02x) expected (%02x,%02x,%02x) in_sprite=%0d",
x, y, cap_r[y][x], cap_g[y][x], cap_b[y][x],
er, eg, eb, in_sprite);
errors = errors + 1;
end
end
end
$display("[tb_gs_clut_load] CBP=%0d CLD=1 256-entry load → PSMT8 scanout uses CLUT-decoded RGB",
CLUT_CBP);
if (errors == 0) $display("[tb_gs_clut_load] PASS");
else $display("[tb_gs_clut_load] FAIL");
$finish;
end
initial begin
#5000000;
$error("[tb_gs_clut_load] timeout");
$finish;
end
endmodule : tb_gs_clut_load