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

311 lines
11 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_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