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>
270 lines
9.3 KiB
Systemverilog
270 lines
9.3 KiB
Systemverilog
// 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
|