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
+310
View File
@@ -0,0 +1,310 @@
// retroDE_ps2 — tb_gs_scanout_psmt8 (Ch96)
//
// Locks the contract for DISPFB1.PSM=PSMT8 (=0x13) scanout.
// PSMT8 stores 1 byte per pixel in VRAM. Without a CLUT (Ch97
// candidate), gs_pcrtc_stub surfaces the 8-bit index as
// grayscale R=G=B=index so the storage lane is visually
// verifiable.
//
// This TB exercises the PSMT8 path WITH a non-trivial display
// window AND non-trivial horizontal magnification, proving:
// - byte stride = 1 byte/pixel (FBW=1 → 64 bytes/row)
// - DISPLAY1.DX/DY/DW/DH still gate the window
// - DISPLAY1.MAGH/MAGV still scale the VRAM coord lookup
//
// gs_stub raster channel doesn't emit PSMT8 yet (Ch97 will
// extend the write side), so this TB bypasses gs_stub and
// drives vram_stub.write_* directly.
//
// Setup:
// VRAM populated with a 4×4 PSMT8 sprite at indices
// row 0: 0x10 0x11 0x12 0x13 at bytes 0..3
// row 1: 0x14 0x15 0x16 0x17 at bytes 64..67
// row 2: 0x18 0x19 0x1A 0x1B at bytes 128..131
// row 3: 0x1C 0x1D 0x1E 0x1F at bytes 192..195
// Each row's 4 bytes are written as one 32-bit word with
// write_be=4'b1111. Adjacent bytes don't conflict because the
// sprite starts at a 4-byte-aligned address per row.
//
// DISPFB1: FBP=0, FBW=1, PSM=PSMT8 (0x13), DBX=DBY=0.
// DISPLAY1: DX=4, DY=2, DW=7 (8 wide), DH=3 (4 tall),
// MAGH=1 (2× horizontal), MAGV=0 (1× vertical).
// Window covers displayed (4..11, 2..5) = 8×4 pixels.
// With MAGH=1, each VRAM column shows for 2 displayed pixels.
// So a 4-VRAM-wide sprite fills the 8-pixel-wide window.
//
// Expected per displayed (x, y):
// if x ∈ [4,11] AND y ∈ [2,5]:
// vram_x = (x - 4) / 2 (range 0..3)
// vram_y = y - 2 (range 0..3)
// idx = 0x10 + (vram_y * 4 + vram_x)
// RGB = (idx, idx, idx) (grayscale)
// else: RGB = (0, 0, 0)
`timescale 1ns/1ps
module tb_gs_scanout_psmt8;
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;
// TB-driven privileged-register-shaped feeds (no gs_stub).
logic [63:0] pmode_q;
logic [63:0] dispfb1_q;
logic [63:0] display1_q;
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()
);
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,
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
localparam int SPRITE_W = 4;
localparam int SPRITE_H = 4;
localparam logic [7:0] BASE_IDX = 8'h10;
// FBW=1 → 64 bytes/row stride for PSMT8 (1 byte/pixel × 64 px/FBW).
localparam int ROW_STRIDE = 64;
// DISPFB1: FBP=0, FBW=1, PSM=PSMT8 (= 0x13).
// FBW [14:9] = 1 → 0x200
// PSM [19:15] = 0x13 → bits 18,17,16,15 = 1,0,0,1; total bit pattern at 19:15 = 5'b10011 → 0x9_8000
// 5'b10011 << 15 = bit15..bit19 → 0x0009_8000 / wait let me recompute
// PSM is bits [19:15], 5-bit. Value 0x13 = 5'b10011.
// bit15=1, bit16=1, bit17=0, bit18=0, bit19=1
// 0x13 << 15 = 0x13 * 0x8000 = 0x9_8000
localparam logic [63:0] DISPFB1_VAL = 64'h0000_0000_0009_8200;
// DISPLAY1: DX=4, DY=2, DW=7, DH=3, MAGH=1, MAGV=0.
// DX [11:0] = 4
// DY [22:12] = 2 → bit 12
// MAGH [26:23] = 1 → bit 23
// MAGV [28:27] = 0
// DW [43:32] = 7 → bit 32
// DH [54:44] = 3 → bit 44
localparam logic [63:0] DISPLAY1_VAL =
64'(12'd4)
| (64'(11'd2) << 12)
| (64'(4'd1) << 23)
| (64'(12'd7) << 32)
| (64'(11'd3) << 44);
localparam logic [63:0] PMODE_EN1 = 64'h0000_0000_0000_0001;
localparam int WIN_DX = 4;
localparam int WIN_DY = 2;
localparam int WIN_W = 8; // DW + 1
localparam int WIN_H = 4; // DH + 1
localparam int MAGH_FAC = 2; // MAGH + 1
localparam int MAGV_FAC = 1; // MAGV + 1
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;
vram_wbe = 4'b0000;
repeat (4) @(posedge clk);
rst_n = 1'b1;
repeat (2) @(posedge clk);
// Populate VRAM with the 4×4 PSMT8 sprite. Each row's 4
// indices pack into one 32-bit word: data[7:0]=col0,
// [15:8]=col1, [23:16]=col2, [31:24]=col3 (little-endian
// byte order matches vram_stub's mem layout).
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
// Configure scanout. dispfb1_q + display1_q first; then
// PMODE.EN1 last, mirroring the canonical driver order.
dispfb1_q = DISPFB1_VAL;
display1_q = DISPLAY1_VAL;
@(posedge clk);
if (dispfb1_q[19:15] !== 5'h13) begin
$error("DISPFB1.PSM=0x%02x (expected 0x13 PSMT8)", 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;
bit in_window;
int hwin_rel, vwin_rel;
int vram_x, vram_y;
logic [7:0] idx;
bit in_sprite;
in_window = (x >= WIN_DX) && (x < WIN_DX + WIN_W)
&& (y >= WIN_DY) && (y < WIN_DY + WIN_H);
if (in_window) begin
hwin_rel = x - WIN_DX;
vwin_rel = y - WIN_DY;
vram_x = hwin_rel / MAGH_FAC;
vram_y = vwin_rel / MAGV_FAC;
in_sprite = (vram_x < SPRITE_W) && (vram_y < SPRITE_H);
idx = BASE_IDX + 8'(vram_y * SPRITE_W + vram_x);
end else begin
in_sprite = 1'b0;
idx = 8'd0;
end
if (in_sprite) begin
er = idx; eg = idx; eb = idx;
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) in_window=%0d in_sprite=%0d",
x, y, cap_r[y][x], cap_g[y][x], cap_b[y][x],
er, eg, eb, in_window, in_sprite);
errors = errors + 1;
end
end
end
$display("[tb_gs_scanout_psmt8] sprite=%0dx%0d window=(%0d,%0d %0dx%0d) MAGH=%0d MAGV=%0d (factors %0dx %0dx)",
SPRITE_W, SPRITE_H, WIN_DX, WIN_DY, WIN_W, WIN_H,
1, 0, MAGH_FAC, MAGV_FAC);
if (errors == 0) $display("[tb_gs_scanout_psmt8] PASS");
else $display("[tb_gs_scanout_psmt8] FAIL");
$finish;
end
initial begin
#5000000;
$error("[tb_gs_scanout_psmt8] timeout");
$finish;
end
endmodule : tb_gs_scanout_psmt8