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

456 lines
17 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_basic (Ch90, Ch91-rewired)
//
// End-to-end round trip TB:
//
// gs_stub.raster_pixel_emit → vram_stub → gs_pcrtc_stub → r/g/b
//
// This is the FIRST TB where written pixels are read back as
// scan-out video — the previous Ch89 TB only verified storage
// via the debug read port. Here the scanout engine drives its
// own raster timing, generates fb_addr from (hcnt, vcnt), reads
// vram, and produces the per-pixel color we then sample at the
// pcrtc output.
//
// Ch91 — scanout configuration arrives through the privileged
// CPU MMIO port (gs_stub.reg_wr_*), not via TB sideband. We
// write DISPFB1 first (FBP/FBW/PSM), then PMODE.EN1=1 to enable
// scanout. This matches how a real PS2 driver brings up display.
//
// Stream:
// PRIM=SPRITE, FRAME_1 (FBP=0 FBW=1 PSMCT32), RGBAQ=0x0000FF30
// v1=(0,0), v2=(3,3) — 4×4 sprite
//
// FBP=0 keeps the sprite at the start of VRAM. FBW=1 means
// FBW*64=64 pixels per row → row-stride = 64*4 = 256 bytes.
// Sprite addresses: row 0 0..0xc; row 1 0x100..0x10c;
// row 2 0x200..0x20c; row 3 0x300..0x30c.
//
// pcrtc: H_ACTIVE=16, V_ACTIVE=8 active area; FBP=0/FBW=1/PSMCT32
// matched to gs_stub config so (hcnt, vcnt) maps to the same
// fb_addr that gs_stub wrote during raster.
//
// Assertions:
// - Sample r/g/b at every (hcnt, vcnt) inside the active area
// of one full frame after raster + scanout enable.
// - Inside the 4×4 sprite region: (R, G, B) = (0x30, 0xFF, 0x00).
// - Outside the sprite region: (R, G, B) = (0, 0, 0).
// - At least one EV_MODE frame trace fires.
`timescale 1ns/1ps
module tb_gs_scanout_basic;
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;
// ------------------------------------------------------------------
// gs_stub
// ------------------------------------------------------------------
logic gif_reg_wr_en;
logic [7:0] gif_reg_num;
logic [63:0] gif_reg_data;
logic priv_reg_wr_en;
logic [15:0] priv_reg_wr_addr;
logic [63:0] priv_reg_wr_data;
logic [7:0] bg_r, bg_g, bg_b;
logic [63:0] pmode_q, dispfb1_q, display1_q;
logic [63:0] prim_q, rgbaq_q, xyz2_q, xyzf2_q, frame_1_q, zbuf_1_q;
logic prim_complete;
logic [31:0] prim_complete_count;
logic [63:0] prim_v0_q, prim_v1_q, prim_v2_q;
logic [63:0] prim_color_q;
logic [63:0] prim_color_v0_q, prim_color_v1_q, prim_color_v2_q;
trace_pkg::vertex_t prim_v0_decoded_q, prim_v1_decoded_q, prim_v2_decoded_q;
trace_pkg::color_t prim_v0_color_decoded_q, prim_v1_color_decoded_q, prim_v2_color_decoded_q;
logic pixel_emit;
logic [31:0] pixel_emit_count;
logic [11:0] pixel_x_q, pixel_y_q;
logic [63:0] pixel_color_q;
logic [8:0] pixel_fbp_q;
logic [5:0] pixel_fbw_q, pixel_psm_q;
logic [31:0] pixel_fb_addr_q;
logic raster_pixel_emit;
logic [31:0] raster_pixel_emit_count;
logic [11:0] raster_pixel_x_q, raster_pixel_y_q;
logic [63:0] raster_pixel_color_q;
logic [31:0] raster_pixel_fb_addr_q;
logic [3:0] raster_pixel_be_q;
logic [5:0] raster_pixel_psm_q;
logic raster_active;
logic raster_overflow;
logic raster_degenerate;
logic gs_ev_valid;
trace_pkg::subsys_e gs_ev_subsys;
trace_pkg::event_e gs_ev_event;
logic [63:0] gs_ev_arg0, gs_ev_arg1, gs_ev_arg2, gs_ev_arg3;
logic [31:0] gs_ev_flags;
gs_stub u_gs (
.clk(clk), .rst_n(rst_n),
.reg_wr_en (priv_reg_wr_en),
.reg_wr_addr(priv_reg_wr_addr),
.reg_wr_data(priv_reg_wr_data),
.gif_reg_wr_en(gif_reg_wr_en),
.gif_reg_num(gif_reg_num),
.gif_reg_data(gif_reg_data),
.bg_r(bg_r), .bg_g(bg_g), .bg_b(bg_b),
.pmode_q(pmode_q), .dispfb1_q(dispfb1_q),
.display1_q(display1_q),
.prim_q(prim_q), .rgbaq_q(rgbaq_q),
.xyz2_q(xyz2_q), .xyzf2_q(xyzf2_q),
.frame_1_q(frame_1_q), .zbuf_1_q(zbuf_1_q),
.prim_complete(prim_complete),
.prim_complete_count(prim_complete_count),
.prim_v0_q(prim_v0_q), .prim_v1_q(prim_v1_q), .prim_v2_q(prim_v2_q),
.prim_color_q(prim_color_q),
.prim_color_v0_q(prim_color_v0_q),
.prim_color_v1_q(prim_color_v1_q),
.prim_color_v2_q(prim_color_v2_q),
.prim_v0_decoded_q(prim_v0_decoded_q),
.prim_v1_decoded_q(prim_v1_decoded_q),
.prim_v2_decoded_q(prim_v2_decoded_q),
.prim_v0_color_decoded_q(prim_v0_color_decoded_q),
.prim_v1_color_decoded_q(prim_v1_color_decoded_q),
.prim_v2_color_decoded_q(prim_v2_color_decoded_q),
.pixel_emit(pixel_emit),
.pixel_emit_count(pixel_emit_count),
.pixel_x_q(pixel_x_q), .pixel_y_q(pixel_y_q),
.pixel_color_q(pixel_color_q),
.pixel_fbp_q(pixel_fbp_q),
.pixel_fbw_q(pixel_fbw_q),
.pixel_psm_q(pixel_psm_q),
.pixel_fb_addr_q(pixel_fb_addr_q),
.raster_pixel_emit(raster_pixel_emit),
.raster_pixel_emit_count(raster_pixel_emit_count),
.raster_pixel_x_q(raster_pixel_x_q),
.raster_pixel_y_q(raster_pixel_y_q),
.raster_pixel_color_q(raster_pixel_color_q),
.raster_pixel_fb_addr_q(raster_pixel_fb_addr_q),
.raster_pixel_be_q(raster_pixel_be_q),
.raster_pixel_psm_q(raster_pixel_psm_q),
.raster_active(raster_active),
.raster_overflow(raster_overflow),
.raster_degenerate(raster_degenerate),
.ev_valid(gs_ev_valid),
.ev_subsys(gs_ev_subsys),
.ev_event(gs_ev_event),
.ev_arg0(gs_ev_arg0), .ev_arg1(gs_ev_arg1),
.ev_arg2(gs_ev_arg2), .ev_arg3(gs_ev_arg3),
.ev_flags(gs_ev_flags)
);
// ------------------------------------------------------------------
// vram_stub — sized to 4 KiB (sprite addresses fit in the
// first 0x310 bytes; 4 KiB is comfortable margin).
// ------------------------------------------------------------------
logic [31:0] vram_read_addr;
logic [31:0] vram_read_data;
vram_stub #(.BYTES(4096)) u_vram (
.clk(clk), .rst_n(rst_n),
.write_en (raster_pixel_emit),
.write_addr(raster_pixel_fb_addr_q),
.write_data(raster_pixel_color_q[31:0]),
.write_be (raster_pixel_be_q),
.write_mask(32'hFFFF_FFFF),
.read_addr (vram_read_addr),
.read_data (vram_read_data),
.read2_addr(32'd0),
.read2_data()
);
// ------------------------------------------------------------------
// gs_pcrtc_stub — drives vram_read_addr and consumes
// vram_read_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_read_addr),
.vram_read_data(vram_read_data),
.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)
);
// ------------------------------------------------------------------
// Sample the scanned-out frame: when de fires, we know the
// current (hcnt, vcnt) corresponds to vram_read_addr =
// (vcnt * pcrtc_pixels_per_row + hcnt) * 4. We sample
// r/g/b/de into a captured frame buffer indexed by
// (vcnt, hcnt).
//
// We watch hcnt/vcnt indirectly via DE — when DE just rose
// we're at (0,0); when it just fell at line end we know the
// line ended; etc. Cleaner: cross-check via internal pcrtc
// counters using hierarchical access (white-box).
// ------------------------------------------------------------------
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 pcrtc_frame_pulse_count;
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
pcrtc_frame_pulse_count = 0;
errors = 0;
capture_armed = 1'b0;
end
// Capture using pcrtc's internal hcnt/vcnt (white-box). We
// sample one cycle "early" (at posedge clk when hcnt/vcnt
// identify the current pixel and r/g/b are comb-valid) so
// capture aligns with the pixel index that fb_addr was
// computed from.
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
if (rst_n && pcrtc_ev_valid) begin
pcrtc_frame_pulse_count <= pcrtc_frame_pulse_count + 1;
end
end
task automatic step_drive(input logic wr_en,
input logic [7:0] num,
input logic [63:0] data);
@(negedge clk);
gif_reg_wr_en = wr_en;
gif_reg_num = num;
gif_reg_data = data;
@(posedge clk);
endtask
task automatic drive_reg(input logic [7:0] num, input logic [63:0] data);
step_drive(1'b1, num, data);
endtask
task automatic drive_idle();
step_drive(1'b0, 8'd0, 64'd0);
endtask
// Ch91 — privileged-block (CPU MMIO) writes go through the
// separate reg_wr_* port. Scanout config (PMODE / DISPFB1)
// arrives this way, NOT via TB sideband.
task automatic drive_priv(input logic [15:0] addr,
input logic [63:0] data);
@(negedge clk);
priv_reg_wr_en = 1'b1;
priv_reg_wr_addr = addr;
priv_reg_wr_data = data;
@(posedge clk);
@(negedge clk);
priv_reg_wr_en = 1'b0;
priv_reg_wr_addr = 16'd0;
priv_reg_wr_data = 64'd0;
endtask
function automatic logic [63:0] xyz2_data(input logic [11:0] x_int,
input logic [11:0] y_int);
return {32'd0, y_int, 4'd0, x_int, 4'd0};
endfunction
localparam logic [7:0] R_PRIM = 8'h00;
localparam logic [7:0] R_RGBAQ = 8'h01;
localparam logic [7:0] R_XYZ2 = 8'h05;
localparam logic [7:0] R_FRAME_1 = 8'h4C;
// Privileged offsets (CPU MMIO within 0x12000000).
localparam logic [15:0] PMODE_OFF = 16'h0000;
localparam logic [15:0] DISPFB1_OFF = 16'h0070;
localparam logic [15:0] DISPLAY1_OFF = 16'h0080;
localparam logic [63:0] PRIM_SPRITE = 64'd6;
// FRAME_1: FBP[8:0]=0, FBW[5:0]=1, PSM[5:0]=PSMCT32(0).
localparam logic [63:0] FRAME_1_VAL = 64'h0000_0000_0001_0000;
localparam logic [63:0] RGBAQ_VAL = 64'h0000_0000_0000_FF30;
// DISPFB1: FBP=0 [8:0], FBW=1 [14:9], PSM=PSMCT32=0 [19:15].
// FBW=1 sits at bit 9 → 0x200.
localparam logic [63:0] DISPFB1_VAL = 64'h0000_0000_0000_0200;
// PMODE.EN1 = bit 0.
localparam logic [63:0] PMODE_EN1 = 64'h0000_0000_0000_0001;
// DISPLAY1: cover the full active area (DX=0, DY=0,
// DW=H_ACTIVE-1, DH=V_ACTIVE-1).
// DW field = bits [43:32]
// DH field = bits [54:44]
localparam logic [63:0] DISPLAY1_VAL =
(64'(PCRTC_H_ACTIVE - 1) << 32) // DW
| (64'(PCRTC_V_ACTIVE - 1) << 44); // DH
// Expected ABGR after PSMCT32 swizzle (R=0x30, G=0xFF, B=0x00, A=0).
localparam logic [7:0] EXP_R = 8'h30;
localparam logic [7:0] EXP_G = 8'hFF;
localparam logic [7:0] EXP_B = 8'h00;
// Sprite covers (0..3, 0..3) — 4×4 = 16 pixels.
localparam int SPRITE_W = 4;
localparam int SPRITE_H = 4;
initial begin
rst_n = 1'b0;
gif_reg_wr_en = 1'b0;
gif_reg_num = 8'd0;
gif_reg_data = 64'd0;
priv_reg_wr_en = 1'b0;
priv_reg_wr_addr = 16'd0;
priv_reg_wr_data = 64'd0;
repeat (4) @(posedge clk);
rst_n = 1'b1;
repeat (2) @(posedge clk);
// Configure DISPFB1 + DISPLAY1 first (before enabling
// scanout). At this point PMODE.EN1=0 so pcrtc still
// outputs zero — the configure-then-enable order matches
// real driver bring-up. DISPLAY1 covers the full active
// area so this TB sees the same window as pre-Ch92.
drive_priv(DISPFB1_OFF, DISPFB1_VAL);
drive_priv(DISPLAY1_OFF, DISPLAY1_VAL);
// Drive sprite into VRAM.
drive_reg(R_PRIM, PRIM_SPRITE);
drive_reg(R_FRAME_1, FRAME_1_VAL);
drive_reg(R_RGBAQ, RGBAQ_VAL);
drive_reg(R_XYZ2, xyz2_data(12'd0, 12'd0)); // v1
drive_reg(R_XYZ2, xyz2_data(12'd3, 12'd3)); // v2 — close
drive_idle();
// Wait for raster to start (FIFO pop → R_SCAN), then for
// it to fully drain. Without the rising-edge wait, the
// falling-edge wait would fire instantly because at this
// moment the pop hasn't fired yet.
wait (raster_active == 1'b1);
wait (raster_active == 1'b0);
repeat (10) @(posedge clk);
if (raster_pixel_emit_count != 32'd16) begin
$error("expected 16 raster emits, got %0d", raster_pixel_emit_count);
errors = errors + 1;
end
// Cross-check: dispfb1_q must hold what we wrote (FBW=1
// landing at bit 9, PSMCT32 at bits [19:15]=0).
if (dispfb1_q !== DISPFB1_VAL) begin
$error("dispfb1_q=0x%016x (expected 0x%016x)", dispfb1_q, DISPFB1_VAL);
errors = errors + 1;
end
if (pmode_q !== 64'd0) begin
$error("pmode_q=0x%016x before EN1 write (expected 0)", pmode_q);
errors = errors + 1;
end
// Now enable scanout via PMODE.EN1. This is the canonical
// driver bring-up sequence: configure DISPFB1 → render
// into VRAM → set PMODE.EN1.
drive_priv(PMODE_OFF, PMODE_EN1);
if (pmode_q[0] !== 1'b1) begin
$error("pmode_q[0]=%b after EN1 write (expected 1)", pmode_q[0]);
errors = errors + 1;
end
// Drain partial frame — wait for next frame boundary.
@(posedge u_pcrtc.end_of_frame);
@(posedge clk); // step into the new frame's hcnt=0
// Arm capture for one full frame.
capture_armed = 1'b1;
@(posedge u_pcrtc.end_of_frame);
@(posedge clk);
capture_armed = 1'b0;
// Verify the captured frame.
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 during capture window", 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
if (pcrtc_frame_pulse_count < 1) begin
$error("pcrtc EV_MODE frame pulse never fired");
errors = errors + 1;
end
$display("[tb_gs_scanout_basic] frames=%0d sprite=%0dx%0d active=%0dx%0d",
pcrtc_frame_pulse_count, SPRITE_W, SPRITE_H,
PCRTC_H_ACTIVE, PCRTC_V_ACTIVE);
if (errors == 0) $display("[tb_gs_scanout_basic] PASS");
else $display("[tb_gs_scanout_basic] FAIL");
$finish;
end
initial begin
#5000000;
$error("[tb_gs_scanout_basic] timeout");
$finish;
end
endmodule : tb_gs_scanout_basic