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>
This commit is contained in:
2026-06-29 20:10:50 -04:00
commit ec82764bef
2462 changed files with 2174303 additions and 0 deletions
+269
View File
@@ -0,0 +1,269 @@
// retroDE_ps2 — tb_gs_scanout_psm16 (Ch94)
//
// Locks the contract for DISPFB1.PSM=PSMCT16 (RGB5A1) scanout.
// gs_pcrtc_stub now selects byte-offset shift and color-decode
// path based on the PSM field in DISPFB1: PSMCT32 (=0) reads 4
// bytes/pixel and uses {A, B, G, R} byte order; PSMCT16 (=2)
// reads 2 bytes/pixel and unpacks RGB5A1 with 5→8 bit-replicate
// expansion.
//
// gs_stub's raster channel still emits PSMCT32 only, so this TB
// bypasses gs_stub entirely — it instantiates only `vram_stub`
// (so we have a real RAM) and `gs_pcrtc_stub` (so we exercise
// the scanout PSM path), and drives privileged-register-shaped
// inputs from TB-side regs. That isolates the test to the
// scanout PSM decode.
//
// Setup:
// VRAM populated with a 4×4 RGB5A1 sprite, color
// (R5, G5, B5) = (0x10, 0x18, 0x08) → 16-bit pixel 0x2310,
// row stride 128 bytes (FBW=1 → 64 pixels/row × 2 bytes/pixel).
// PSM = PSMCT16 (DISPFB1[19:15] = 0x02).
// DISPFB1.FBP=0, FBW=1. DBX=DBY=0.
// DISPLAY1 covers full 16×8 active area, MAGH=MAGV=0 (1×).
//
// Expected:
// R8 = {0x10, 0x10[4:2]} = {0x10, 3'b100} = 8'h84
// G8 = {0x18, 0x18[4:2]} = {0x18, 3'b110} = 8'hC6
// B8 = {0x08, 0x08[4:2]} = {0x08, 3'b010} = 8'h42
// At displayed (0..3, 0..3): (R, G, B) = (0x84, 0xC6, 0x42).
// At every other displayed pixel: (0, 0, 0).
`timescale 1ns/1ps
module tb_gs_scanout_psm16;
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;
// Privileged-register-shaped TB regs feeding pcrtc directly.
logic [63:0] pmode_q;
logic [63:0] dispfb1_q;
logic [63:0] display1_q;
// VRAM write side: TB-driven (no gs_stub raster source).
logic vram_we;
logic [31:0] vram_waddr;
logic [31:0] vram_wdata;
// VRAM read side: pcrtc drives the address.
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 (4'b1111),
.write_mask(32'hFFFF_FFFF),
.read_addr (vram_raddr),
.read_data (vram_rdata),
.read2_addr(32'd0),
.read2_data()
);
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'b0),
.clut_csa (5'd0),
.clut_read_idx (),
.clut_read_data(32'd0),
.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 vram_write32(input logic [31:0] addr, input logic [31:0] data);
@(negedge clk);
vram_we = 1'b1;
vram_waddr = addr;
vram_wdata = data;
@(posedge clk);
@(negedge clk);
vram_we = 1'b0;
vram_waddr = 32'd0;
vram_wdata = 32'd0;
endtask
// Build a packed RGB5A1 16-bit value: A[15] B[14:10] G[9:5] R[4:0].
function automatic logic [15:0] psm16_pack(input logic [4:0] r5,
input logic [4:0] g5,
input logic [4:0] b5,
input logic a1);
return {a1, b5, g5, r5};
endfunction
// Sprite color.
localparam logic [4:0] R5 = 5'h10;
localparam logic [4:0] G5 = 5'h18;
localparam logic [4:0] B5 = 5'h08;
// Expected expanded 8-bit values (5→8 bit-replicate).
localparam logic [7:0] EXP_R = {R5, R5[4:2]};
localparam logic [7:0] EXP_G = {G5, G5[4:2]};
localparam logic [7:0] EXP_B = {B5, B5[4:2]};
localparam int SPRITE_W = 4;
localparam int SPRITE_H = 4;
// DISPFB1 PSM = PSMCT16 (5'h02), FBP=0, FBW=1.
// FBP[8:0] = 0
// FBW[14:9] = 1 → 0x200
// PSM[19:15] = 2 → 0x10000
localparam logic [63:0] DISPFB1_VAL = 64'h0000_0000_0001_0200;
// DISPLAY1 covers full active area, MAGH=MAGV=0.
localparam logic [63:0] DISPLAY1_VAL =
(64'(PCRTC_H_ACTIVE - 1) << 32) // DW
| (64'(PCRTC_V_ACTIVE - 1) << 44); // DH
localparam logic [63:0] PMODE_EN1 = 64'h0000_0000_0000_0001;
initial begin
rst_n = 1'b0;
pmode_q = 64'd0;
dispfb1_q = 64'd0;
display1_q = 64'd0;
vram_we = 1'b0;
vram_waddr = 32'd0;
vram_wdata = 32'd0;
repeat (4) @(posedge clk);
rst_n = 1'b1;
repeat (2) @(posedge clk);
// Populate VRAM with the 4×4 PSMCT16 sprite. FBW=1
// means 64 pixels/row × 2 bytes = 128 bytes/row stride.
// Row Y starts at byte Y*128. Each row stores 4 PSMCT16
// pixels = 8 bytes = 2 32-bit writes.
for (int y = 0; y < SPRITE_H; y++) begin
logic [15:0] pix;
logic [31:0] pair;
int row_base;
pix = psm16_pack(R5, G5, B5, 1'b0);
pair = {pix, pix}; // two PSMCT16 pixels packed
row_base = y * 128;
// Pixels (0,1) at byte row_base..row_base+3.
vram_write32(row_base, pair);
// Pixels (2,3) at byte row_base+4..row_base+7.
vram_write32(row_base + 32'd4, pair);
end
// Configure scanout. Reset has dispfb1_q=display1_q=0, so
// we must explicitly write both before enabling EN1.
dispfb1_q = DISPFB1_VAL;
display1_q = DISPLAY1_VAL;
@(posedge clk);
// Cross-check the PSM field landed at [19:15]=2.
if (dispfb1_q[19:15] !== 5'h02) begin
$error("DISPFB1.PSM=%0d (expected 2 = PSMCT16)", dispfb1_q[19:15]);
errors = errors + 1;
end
pmode_q = PMODE_EN1;
@(posedge clk);
@(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;
if (x < SPRITE_W && y < SPRITE_H) begin
er = EXP_R; eg = EXP_G; eb = EXP_B;
end else begin
er = 8'd0; eg = 8'd0; eb = 8'd0;
end
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)",
x, y, cap_r[y][x], cap_g[y][x], cap_b[y][x], er, eg, eb);
errors = errors + 1;
end
end
end
$display("[tb_gs_scanout_psm16] sprite=%0dx%0d RGB5A1=(%02x,%02x,%02x) → RGB8=(%02x,%02x,%02x)",
SPRITE_W, SPRITE_H, R5, G5, B5, EXP_R, EXP_G, EXP_B);
if (errors == 0) $display("[tb_gs_scanout_psm16] PASS");
else $display("[tb_gs_scanout_psm16] FAIL");
$finish;
end
initial begin
#5000000;
$error("[tb_gs_scanout_psm16] timeout");
$finish;
end
endmodule : tb_gs_scanout_psm16