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

509 lines
19 KiB
Systemverilog

// retroDE_ps2 — tb_gs_tex0_clut (Ch98)
//
// Locks the contract for gs_stub's TEX0_1 latch + CLUT-side
// field decoders (CBP/CPSM/CSM/CSA/CLD). Ch97 added the CLUT
// lookup path through pcrtc with a TB-driven sideband
// `clut_csa`; Ch98 wires that field through the GIF-A+D TEX0_1
// register the way a real PS2 driver would.
//
// Two phases:
// Phase 1 — register-decode verification. Drive a known TEX0_1
// pattern via gs_stub.gif_reg_*, then assert that
// tex0_1_q[63:0] holds the same payload AND the
// sub-field decoder outputs (CBP/CPSM/CSM/CSA/CLD)
// match the bit ranges in the contract.
// Phase 2 — end-to-end CSA-from-TEX0 → pcrtc lookup. Wire
// pcrtc.clut_csa from gs_stub.tex0_1_csa_q (instead
// of a TB-side reg). Drive TEX0_1 with CSA=1, set
// VRAM[0] = 0x00, program CLUT[0x10] with a known
// entry, and verify pcrtc r/g/b at displayed (0,0)
// matches CLUT[0x10] — proves CSA flowed from the
// GIF reg into the CLUT lookup math.
//
// Out of scope (deferred): CBP/CPSM are extracted but pcrtc
// doesn't yet load from VRAM[CBP] — the TB still programs the
// clut_stub directly. CLD is latched but doesn't trigger any
// load. Those are Ch99+ work.
`timescale 1ns/1ps
module tb_gs_tex0_clut;
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 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),
.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; not wired to gs_stub raster).
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;
vram_stub #(.BYTES(4096)) 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(32'd0),
.read2_data()
);
// CLUT (TB-direct write port — Ch99 candidate is wiring this
// to a VRAM→CLUT load triggered by TEX0_1.CLD).
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)
);
// pcrtc — clut_csa is wired from gs_stub.tex0_1_csa_q (Ch98)
// instead of a TB-side reg. clut_enable stays TB-driven for
// now (Ch99 candidate: derive from PSM + CLD + load done).
logic clut_enable;
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 (clut_enable),
.clut_csa (tex0_1_csa_q), // ← gs_stub TEX0_1 decoded field
.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;
// Ch98 audit-low — capture EV_WRITE events from gs_stub so
// we can assert TEX0_1 lands with the documented stable
// selector (= 7) instead of the default-0 fall-through.
int tex0_ev_write_count;
logic [63:0] last_tex0_ev_arg0;
logic [63:0] last_tex0_ev_arg1;
logic [63:0] last_tex0_ev_arg2;
initial begin
tex0_ev_write_count = 0;
last_tex0_ev_arg0 = 64'd0;
last_tex0_ev_arg1 = 64'd0;
last_tex0_ev_arg2 = 64'd0;
end
always_ff @(posedge clk) begin
if (rst_n && gs_ev_valid
&& gs_ev_event == trace_pkg::EV_WRITE
&& gs_ev_arg0[7:0] == 8'h06) begin
tex0_ev_write_count <= tex0_ev_write_count + 1;
last_tex0_ev_arg0 <= gs_ev_arg0;
last_tex0_ev_arg1 <= gs_ev_arg1;
last_tex0_ev_arg2 <= gs_ev_arg2;
end
end
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
task automatic clut_write(input logic [7:0] idx,
input logic [31:0] entry);
@(negedge clk);
clut_we = 1'b1;
clut_widx = idx;
clut_wdata = entry;
@(posedge clk);
@(negedge clk);
clut_we = 1'b0;
clut_widx = 8'd0;
clut_wdata = 32'd0;
endtask
function automatic logic [31:0] clut_entry(input logic [7:0] i);
// Reuse the Ch97 contract: 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;
// Privileged offsets.
localparam logic [15:0] PMODE_OFF = 16'h0000;
localparam logic [15:0] DISPFB1_OFF = 16'h0070;
localparam logic [15:0] DISPLAY1_OFF = 16'h0080;
// Phase-1 pattern: distinct values per field.
localparam logic [13:0] EXP_CBP = 14'h1234;
localparam logic [3:0] EXP_CPSM = 4'h0; // PSMCT32
localparam logic EXP_CSM = 1'b1; // CSM2 (linear)
localparam logic [4:0] EXP_CSA = 5'h05;
localparam logic [2:0] EXP_CLD = 3'h2; // load mode 2
// Phase-2 config.
localparam logic [63:0] DISPFB1_VAL = 64'h0000_0000_0009_8200; // FBP=0, FBW=1, PSM=PSMT8
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;
clut_we = 1'b0;
clut_widx = 8'd0;
clut_wdata = 32'd0;
clut_enable = 1'b0;
repeat (4) @(posedge clk);
rst_n = 1'b1;
repeat (2) @(posedge clk);
// ----------------------------------------------------------------
// Phase 1 — TEX0_1 register decode verification.
// ----------------------------------------------------------------
begin
logic [63:0] payload;
payload = tex0_pack(EXP_CBP, EXP_CPSM, EXP_CSM, EXP_CSA, EXP_CLD);
drive_reg(GIF_TEX0_1, payload);
drive_idle();
if (tex0_1_q !== payload) begin $error("tex0_1_q=0x%016x (expected 0x%016x)", tex0_1_q, payload); errors = errors + 1; end
if (tex0_1_cbp_q !== EXP_CBP) begin $error("CBP=0x%04x (expected 0x%04x)", tex0_1_cbp_q, EXP_CBP); errors = errors + 1; end
if (tex0_1_cpsm_q !== EXP_CPSM) begin $error("CPSM=0x%01x (expected 0x%01x)", tex0_1_cpsm_q, EXP_CPSM); errors = errors + 1; end
if (tex0_1_csm_q !== EXP_CSM) begin $error("CSM=%b (expected %b)", tex0_1_csm_q, EXP_CSM); errors = errors + 1; end
if (tex0_1_csa_q !== EXP_CSA) begin $error("CSA=0x%02x (expected 0x%02x)", tex0_1_csa_q, EXP_CSA); errors = errors + 1; end
if (tex0_1_cld_q !== EXP_CLD) begin $error("CLD=0x%01x (expected 0x%01x)", tex0_1_cld_q, EXP_CLD); errors = errors + 1; end
end
$display("[tb_gs_tex0_clut] phase 1 register decode: tex0_1_q=0x%016x CBP=0x%04x CPSM=%01x CSM=%b CSA=%02x CLD=%01x",
tex0_1_q, tex0_1_cbp_q, tex0_1_cpsm_q, tex0_1_csm_q, tex0_1_csa_q, tex0_1_cld_q);
// Settle one more cycle so the EV_WRITE pulse from the
// drive_reg posedge has propagated into the TB-side
// observer's NB-updated count.
@(posedge clk);
// Ch98 audit-low: TEX0_1 writes must produce EV_WRITE
// events with the documented per-register selector value
// (7), not the default-0 unknown fall-through.
if (tex0_ev_write_count < 1) begin
$error("EV_WRITE for TEX0_1 (reg# 0x06) never observed");
errors = errors + 1;
end else begin
if (last_tex0_ev_arg0[7:0] !== 8'h06) begin
$error("TEX0_1 EV_WRITE arg0[7:0]=0x%02x (expected 0x06)", last_tex0_ev_arg0[7:0]);
errors = errors + 1;
end
if (last_tex0_ev_arg2 !== 64'd7) begin
$error("TEX0_1 EV_WRITE selector arg2=0x%016x (expected 7)", last_tex0_ev_arg2);
errors = errors + 1;
end
end
// ----------------------------------------------------------------
// Phase 2 — TEX0_1.CSA flows into pcrtc.clut_csa, and a
// VRAM index of 0x00 with CSA=1 looks up CLUT[0x10].
// ----------------------------------------------------------------
// Re-program TEX0_1 with CSA=1 (other CLUT fields zero).
drive_reg(GIF_TEX0_1, tex0_pack(14'd0, 4'd0, 1'b1, 5'd1, 3'd0));
drive_idle();
if (tex0_1_csa_q !== 5'd1) begin
$error("phase 2: tex0_1_csa_q=0x%02x (expected 0x01)", tex0_1_csa_q);
errors = errors + 1;
end
// Program CLUT[0x10] so the lookup has something to find.
clut_write(8'h10, clut_entry(8'h10));
// VRAM[0] = 0x00 (PSMT8 index byte 0). FBW=1 → 64 bytes/row.
vram_write32(32'd0, 32'h00_00_00_00, 4'b0001);
// Configure scanout (no gs_stub raster involved).
drive_priv(DISPFB1_OFF, DISPFB1_VAL);
drive_priv(DISPLAY1_OFF, DISPLAY1_VAL);
clut_enable = 1'b1;
@(posedge clk);
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;
begin
logic [31:0] entry_p2;
entry_p2 = clut_entry(8'h10);
if (cap_r[0][0] !== entry_p2[7:0]
|| cap_g[0][0] !== entry_p2[15:8]
|| cap_b[0][0] !== entry_p2[23:16]) begin
$error("[phase 2] (0,0) got (%02x,%02x,%02x) expected CLUT[0x10]=(%02x,%02x,%02x) — TEX0.CSA flow into pcrtc broken",
cap_r[0][0], cap_g[0][0], cap_b[0][0],
entry_p2[7:0], entry_p2[15:8], entry_p2[23:16]);
errors = errors + 1;
end
end
$display("[tb_gs_tex0_clut] phase 2 TEX0.CSA=1 → pcrtc.clut_csa=1 → idx 0x00 reads CLUT[0x10]: cap=(%02x,%02x,%02x)",
cap_r[0][0], cap_g[0][0], cap_b[0][0]);
if (errors == 0) $display("[tb_gs_tex0_clut] PASS");
else $display("[tb_gs_tex0_clut] FAIL");
$finish;
end
initial begin
#5000000;
$error("[tb_gs_tex0_clut] timeout");
$finish;
end
endmodule : tb_gs_tex0_clut