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

263 lines
10 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_raster_pipeline (Ch88)
//
// White-box TB pinning down the Ch88 pixel-pipeline contract:
//
// * Throughput: 1 candidate pixel/cycle once the pipeline is
// primed. For a fully-inside primitive this means 1 emit/cycle
// for N consecutive cycles, where N is the bbox pixel count.
//
// * Latency: from the v2-close cycle (push) to first observed
// raster_pixel_emit, exactly 5 posedges:
// v_close v2 commits, push_ok=1 (FIFO 0→1)
// v_close+1 R_IDLE+nonempty → pop_ok=1, state ← R_SCAN
// v_close+2 state=R_SCAN, S0 produces pix(0,0); s1 ← (0,0)
// v_close+3 s2 ← (0,0)
// v_close+4 emit register fires (raster_pixel_emit ← 1)
// v_close+5 raster_pixel_emit visible to observer
// i.e., 3 stages of pipeline + 1 cycle for the FIFO turn-
// around + 1 cycle for the registered emit output.
//
// * After last S0 coord, the pipeline drains for 2 more cycles
// so all in-flight pixels still emit (no truncation).
//
// Flow: drive PRIM/FRAME_1/RGBAQ then a single 4×4 SPRITE
// (16 pixels). Capture the cycle index of every raster_pixel_emit
// pulse and assert:
// - exactly 16 pulses
// - every adjacent pair is on consecutive cycles (delta == 1)
// - first pulse lands EXACTLY 3 cycles after the pop_ok cycle
// - raster_overflow stays low
`timescale 1ns/1ps
module tb_gs_raster_pipeline;
logic clk;
logic rst_n;
initial clk = 1'b0;
always #5 clk = ~clk;
// gs_stub inputs
logic gif_reg_wr_en;
logic [7:0] gif_reg_num;
logic [63:0] gif_reg_data;
// gs_stub outputs (most are tied off — we only watch raster_*)
logic [7:0] bg_r, bg_g, bg_b;
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 raster_active;
logic raster_overflow;
logic raster_degenerate;
logic ev_valid;
trace_pkg::subsys_e ev_subsys;
trace_pkg::event_e ev_event;
logic [63:0] ev_arg0, ev_arg1, ev_arg2, ev_arg3;
logic [31:0] ev_flags;
gs_stub u_gs (
.clk(clk), .rst_n(rst_n),
.reg_wr_en(1'b0), .reg_wr_addr(16'd0), .reg_wr_data(64'd0),
.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),
.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_active(raster_active),
.raster_overflow(raster_overflow),
.raster_degenerate(raster_degenerate),
.ev_valid(ev_valid), .ev_subsys(ev_subsys), .ev_event(ev_event),
.ev_arg0(ev_arg0), .ev_arg1(ev_arg1),
.ev_arg2(ev_arg2), .ev_arg3(ev_arg3),
.ev_flags(ev_flags)
);
// Per-cycle index. Increments every posedge clk after rst_n.
int cycle_idx;
initial cycle_idx = 0;
always_ff @(posedge clk) if (rst_n) cycle_idx <= cycle_idx + 1;
// Capture the cycle of the v2 close (last drive_reg call) and
// the cycle every raster_pixel_emit pulse fires.
int v_close_cycle;
int emit_cycles [0:31];
int emit_count;
int errors;
initial begin
v_close_cycle = -1;
emit_count = 0;
errors = 0;
end
always_ff @(posedge clk) begin
if (rst_n && raster_pixel_emit && emit_count < 32) begin
emit_cycles[emit_count] <= cycle_idx;
emit_count <= emit_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
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;
localparam logic [63:0] PRIM_SPRITE = 64'd6;
localparam logic [63:0] FRAME_1_VAL = 64'h0000_0000_000A_0002;
localparam logic [63:0] RGBAQ_VAL = 64'h0000_0000_FF00_30FF;
initial begin
rst_n = 1'b0;
gif_reg_wr_en = 1'b0;
gif_reg_num = 8'd0;
gif_reg_data = 64'd0;
repeat (4) @(posedge clk);
rst_n = 1'b1;
repeat (2) @(posedge clk);
drive_reg(R_PRIM, PRIM_SPRITE);
drive_reg(R_FRAME_1, FRAME_1_VAL);
drive_reg(R_RGBAQ, RGBAQ_VAL);
// 4×4 sprite — bbox=[0..3]×[0..3] = 16 pixels.
drive_reg(R_XYZ2, xyz2_data(12'd0, 12'd0)); // v1
drive_reg(R_XYZ2, xyz2_data(12'd3, 12'd3)); // v2 — close S1
v_close_cycle = cycle_idx; // capture posedge index of v2 close
// Stop driving (deassert gif_reg_wr_en) and let the
// pipeline run. Without this idle, gif_reg_wr_en stays
// high and re-commits the v2 vertex every cycle, kicking
// off extra sprites and overflowing the FIFO.
drive_idle();
repeat (40) @(posedge clk);
// ---- Assertions ----
$display("[tb_gs_raster_pipeline] v_close_cycle=%0d emit_count=%0d raster_pixel_emit_count=%0d raster_overflow=%b",
v_close_cycle, emit_count, raster_pixel_emit_count, raster_overflow);
for (int i = 0; i < emit_count; i++) begin
$display("[tb_gs_raster_pipeline] emit[%0d] @ cyc=%0d", i, emit_cycles[i]);
end
if (emit_count != 16) begin
$error("emit_count=%0d (expected 16 for 4×4 sprite)", emit_count);
errors = errors + 1;
end
if (raster_pixel_emit_count != 32'd16) begin
$error("raster_pixel_emit_count=%0d (expected 16)", raster_pixel_emit_count);
errors = errors + 1;
end
if (raster_overflow !== 1'b0) begin
$error("raster_overflow=%b (expected 0)", raster_overflow);
errors = errors + 1;
end
// Throughput: every consecutive pair of emits must be on
// adjacent cycles (delta == 1). 1 pixel/cycle.
for (int i = 1; i < emit_count; i++) begin
int d;
d = emit_cycles[i] - emit_cycles[i-1];
if (d != 1) begin
$error("throughput break: emit[%0d]@%0d vs emit[%0d]@%0d (delta=%0d, expected 1)",
i-1, emit_cycles[i-1], i, emit_cycles[i], d);
errors = errors + 1;
end
end
// Latency: 5 posedges from v_close to first observed
// raster_pixel_emit (see header for breakdown).
if (emit_count > 0) begin
int expected_first;
int actual_first;
expected_first = v_close_cycle + 5;
actual_first = emit_cycles[0];
if (actual_first != expected_first) begin
$error("first-emit latency: emit[0]@cyc=%0d (expected %0d = v_close+5)",
actual_first, expected_first);
errors = errors + 1;
end
end
if (errors == 0) $display("[tb_gs_raster_pipeline] PASS");
else $display("[tb_gs_raster_pipeline] FAIL");
$finish;
end
initial begin
#5000000;
$error("[tb_gs_raster_pipeline] timeout");
$finish;
end
endmodule : tb_gs_raster_pipeline