ec82764bef
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>
311 lines
11 KiB
Systemverilog
311 lines
11 KiB
Systemverilog
// 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
|