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

358 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_swizzle_psmct32 (Ch119)
//
// Locks the contract for `gs_swizzle_psmct32_stub` — the
// PSMCT32 page/block swizzle math primitive that future
// chapters will wire into the address paths in gs_stub /
// gs_pcrtc_stub / gif_image_xfer_stub. Pre-Ch119 those modules
// all use linear FBW*64-byte-stride addressing; the math here
// produces a different byte layout that matches the real PS2
// GS organization (8 KiB pages, 32 blocks/page in a Z-order-
// like permutation, 8×8 PSMCT32 pixels per block, row-major
// within a block).
//
// Verification strategy:
// 1) Spot-checks against hand-computed addresses for the
// well-defined edge cases — origin, first block boundary
// crossing, second-row-of-blocks, page boundary on x and
// y axes, second page on x.
// 2) Within-block walk: every (xb, yb) ∈ [0..7]×[0..7] of the
// first block produces sequential 4-byte addresses (proves
// the within-block layout is row-major, byte_in_block =
// yb*32 + xb*4).
// 3) Source-table lock: 32 hard-coded address checks (one per
// block in page 0) where the expected block index is taken
// VERBATIM from PCSX2's PSMCT32 block table — NOT derived
// from the in-TB ref_block_idx() function. This is what
// proves the DUT's swizzle_psmct32() table matches the
// canonical source. A copied-wrong table that happened to
// still be a valid permutation of 0..31 would fail this
// phase, while the bijectivity sweep below would pass it.
// 4) Block-swizzle walk: redundant with phase 3, but cross-
// checks ref_block_idx() against the DUT (the bijectivity
// sweep relies on ref_block_idx() being correct).
// 5) Bijectivity sweep: walk every pixel of a 64×32 page and
// assert each address is unique, lands in [0, 8192), and
// matches the in-TB reference function — locks the address
// path's integration once the source table is verified.
`timescale 1ns/1ps
module tb_gs_swizzle_psmct32;
// -----------------------------------------------------------
// DUT
// -----------------------------------------------------------
logic [8:0] fbp;
logic [5:0] fbw;
logic [11:0] x;
logic [11:0] y;
logic [31:0] addr;
gs_swizzle_psmct32_stub u_dut (
.fbp(fbp), .fbw(fbw), .x(x), .y(y), .addr(addr)
);
int errors;
initial errors = 0;
// -----------------------------------------------------------
// Helper: drive (fbp, fbw, x, y) and return the address.
// -----------------------------------------------------------
task automatic compute(
input logic [8:0] fbp_v,
input logic [5:0] fbw_v,
input logic [11:0] x_v,
input logic [11:0] y_v,
output logic [31:0] addr_o);
fbp = fbp_v; fbw = fbw_v; x = x_v; y = y_v;
#1;
addr_o = addr;
endtask
task automatic check(
input logic [8:0] fbp_v,
input logic [5:0] fbw_v,
input logic [11:0] x_v,
input logic [11:0] y_v,
input logic [31:0] expected,
input string tag);
logic [31:0] got;
compute(fbp_v, fbw_v, x_v, y_v, got);
if (got !== expected) begin
$error("[%s] fbp=%0d fbw=%0d (x=%0d,y=%0d) got 0x%08x expected 0x%08x",
tag, fbp_v, fbw_v, x_v, y_v, got, expected);
errors = errors + 1;
end
endtask
// -----------------------------------------------------------
// Reference swizzle table (matching the DUT's encoding).
// Used by the bijectivity sweep + cross-check.
// -----------------------------------------------------------
function automatic int ref_block_idx(input int by, input int bx);
// PCSX2 PSMCT32 block table:
// row by=0: 0 1 4 5 16 17 20 21
// row by=1: 2 3 6 7 18 19 22 23
// row by=2: 8 9 12 13 24 25 28 29
// row by=3:10 11 14 15 26 27 30 31
case ({by[1:0], bx[2:0]})
5'd0: return 0;
5'd1: return 1;
5'd2: return 4;
5'd3: return 5;
5'd4: return 16;
5'd5: return 17;
5'd6: return 20;
5'd7: return 21;
5'd8: return 2;
5'd9: return 3;
5'd10: return 6;
5'd11: return 7;
5'd12: return 18;
5'd13: return 19;
5'd14: return 22;
5'd15: return 23;
5'd16: return 8;
5'd17: return 9;
5'd18: return 12;
5'd19: return 13;
5'd20: return 24;
5'd21: return 25;
5'd22: return 28;
5'd23: return 29;
5'd24: return 10;
5'd25: return 11;
5'd26: return 14;
5'd27: return 15;
5'd28: return 26;
5'd29: return 27;
5'd30: return 30;
default: return 31;
endcase
endfunction
function automatic logic [31:0] ref_addr(
input int fbp_v, input int fbw_v, input int x_v, input int y_v);
int page_x, page_y, page_idx, page_base;
int by, bx, blk_idx, xb, yb;
int addr_v;
page_x = x_v / 64;
page_y = y_v / 32;
page_idx = page_y * fbw_v + page_x;
page_base = fbp_v * 2048 + page_idx * 8192;
by = (y_v % 32) / 8;
bx = (x_v % 64) / 8;
blk_idx = ref_block_idx(by, bx);
xb = x_v % 8;
yb = y_v % 8;
addr_v = page_base + blk_idx * 256 + yb * 32 + xb * 4;
return addr_v[31:0];
endfunction
initial begin
fbp = 9'd0;
fbw = 6'd1;
x = 12'd0;
y = 12'd0;
#1;
// -----------------------------------------------------------
// Spot-checks — handful of well-defined corners.
// -----------------------------------------------------------
// (0, 0): block (0,0) → block_idx=0, byte_in_block=0.
check(9'd0, 6'd1, 12'd0, 12'd0, 32'd0, "origin");
// (1, 0): same block, byte 4.
check(9'd0, 6'd1, 12'd1, 12'd0, 32'd4, "origin+1px");
// (7, 7): last pixel of block (0,0) → byte 7*32 + 7*4 = 252.
check(9'd0, 6'd1, 12'd7, 12'd7, 32'd252, "origin-block-corner");
// (8, 0): block (1,0) → block_idx=1, byte 0 → 256.
check(9'd0, 6'd1, 12'd8, 12'd0, 32'd256, "block-(1,0)-origin");
// (0, 8): block (0,1) → block_idx=2, byte 0 → 512.
check(9'd0, 6'd1, 12'd0, 12'd8, 32'd512, "block-(0,1)-origin");
// (16, 0): block (2,0) → block_idx=4, byte 0 → 1024.
check(9'd0, 6'd1, 12'd16, 12'd0, 32'd1024, "block-(2,0)-origin");
// (32, 0): block (4,0) → block_idx=16, byte 0 → 4096.
check(9'd0, 6'd1, 12'd32, 12'd0, 32'd4096, "block-(4,0)-origin");
// (63, 31): last pixel of last block of page 0 → block (7,3)
// block_idx = 31, xb=7, yb=7 → byte 31*256 + 252 = 8188.
check(9'd0, 6'd1, 12'd63, 12'd31, 32'd8188, "page0-last-pixel");
// (64, 0): start of page 1 (assuming FBW=1) → page_idx=1,
// page_base=8192, block (0,0) → byte 0 → addr 8192.
check(9'd0, 6'd1, 12'd64, 12'd0, 32'd8192, "page1-x-origin");
// (0, 32): start of page-row 1 (assuming FBW=1) → page_idx=1.
check(9'd0, 6'd1, 12'd0, 12'd32, 32'd8192, "page1-y-origin");
// FBP=4 (so FBP*2048 = 8192 = 1 page offset), (0,0):
// addr = 8192.
check(9'd4, 6'd1, 12'd0, 12'd0, 32'd8192, "fbp4-origin");
// -----------------------------------------------------------
// Within-block walk: (xb, yb) ∈ 8×8 of block (0,0). Each
// pixel should land at byte yb*32 + xb*4. Walk in
// row-major and verify sequential.
// -----------------------------------------------------------
for (int yb = 0; yb < 8; yb++) begin
for (int xb = 0; xb < 8; xb++) begin
logic [31:0] expected;
expected = 32'(yb * 32 + xb * 4);
check(9'd0, 6'd1, 12'(xb), 12'(yb), expected, "within-block-row-major");
end
end
// -----------------------------------------------------------
// Source-table lock: 32 hard-coded address checks, one per
// block in page 0, with the expected block index taken
// VERBATIM from PCSX2 GSLocalMemoryFunctions.cpp's PSMCT32
// block table (NOT derived from ref_block_idx). A copied-
// wrong DUT swizzle table — even one that is still a valid
// permutation of 0..31 — would fail this phase, while the
// bijectivity sweep below would happily pass it. So this
// phase locks the DUT's table to the canonical source, and
// the sweep then locks the address path's integration.
//
// PCSX2 PSMCT32 block table (literal values):
// by=0: 0 1 4 5 16 17 20 21
// by=1: 2 3 6 7 18 19 22 23
// by=2: 8 9 12 13 24 25 28 29
// by=3:10 11 14 15 26 27 30 31
// -----------------------------------------------------------
check(9'd0, 6'd1, 12'(0*8), 12'(0*8), 32'(0*256), "src-lock-by0bx0");
check(9'd0, 6'd1, 12'(1*8), 12'(0*8), 32'(1*256), "src-lock-by0bx1");
check(9'd0, 6'd1, 12'(2*8), 12'(0*8), 32'(4*256), "src-lock-by0bx2");
check(9'd0, 6'd1, 12'(3*8), 12'(0*8), 32'(5*256), "src-lock-by0bx3");
check(9'd0, 6'd1, 12'(4*8), 12'(0*8), 32'(16*256), "src-lock-by0bx4");
check(9'd0, 6'd1, 12'(5*8), 12'(0*8), 32'(17*256), "src-lock-by0bx5");
check(9'd0, 6'd1, 12'(6*8), 12'(0*8), 32'(20*256), "src-lock-by0bx6");
check(9'd0, 6'd1, 12'(7*8), 12'(0*8), 32'(21*256), "src-lock-by0bx7");
check(9'd0, 6'd1, 12'(0*8), 12'(1*8), 32'(2*256), "src-lock-by1bx0");
check(9'd0, 6'd1, 12'(1*8), 12'(1*8), 32'(3*256), "src-lock-by1bx1");
check(9'd0, 6'd1, 12'(2*8), 12'(1*8), 32'(6*256), "src-lock-by1bx2");
check(9'd0, 6'd1, 12'(3*8), 12'(1*8), 32'(7*256), "src-lock-by1bx3");
check(9'd0, 6'd1, 12'(4*8), 12'(1*8), 32'(18*256), "src-lock-by1bx4");
check(9'd0, 6'd1, 12'(5*8), 12'(1*8), 32'(19*256), "src-lock-by1bx5");
check(9'd0, 6'd1, 12'(6*8), 12'(1*8), 32'(22*256), "src-lock-by1bx6");
check(9'd0, 6'd1, 12'(7*8), 12'(1*8), 32'(23*256), "src-lock-by1bx7");
check(9'd0, 6'd1, 12'(0*8), 12'(2*8), 32'(8*256), "src-lock-by2bx0");
check(9'd0, 6'd1, 12'(1*8), 12'(2*8), 32'(9*256), "src-lock-by2bx1");
check(9'd0, 6'd1, 12'(2*8), 12'(2*8), 32'(12*256), "src-lock-by2bx2");
check(9'd0, 6'd1, 12'(3*8), 12'(2*8), 32'(13*256), "src-lock-by2bx3");
check(9'd0, 6'd1, 12'(4*8), 12'(2*8), 32'(24*256), "src-lock-by2bx4");
check(9'd0, 6'd1, 12'(5*8), 12'(2*8), 32'(25*256), "src-lock-by2bx5");
check(9'd0, 6'd1, 12'(6*8), 12'(2*8), 32'(28*256), "src-lock-by2bx6");
check(9'd0, 6'd1, 12'(7*8), 12'(2*8), 32'(29*256), "src-lock-by2bx7");
check(9'd0, 6'd1, 12'(0*8), 12'(3*8), 32'(10*256), "src-lock-by3bx0");
check(9'd0, 6'd1, 12'(1*8), 12'(3*8), 32'(11*256), "src-lock-by3bx1");
check(9'd0, 6'd1, 12'(2*8), 12'(3*8), 32'(14*256), "src-lock-by3bx2");
check(9'd0, 6'd1, 12'(3*8), 12'(3*8), 32'(15*256), "src-lock-by3bx3");
check(9'd0, 6'd1, 12'(4*8), 12'(3*8), 32'(26*256), "src-lock-by3bx4");
check(9'd0, 6'd1, 12'(5*8), 12'(3*8), 32'(27*256), "src-lock-by3bx5");
check(9'd0, 6'd1, 12'(6*8), 12'(3*8), 32'(30*256), "src-lock-by3bx6");
check(9'd0, 6'd1, 12'(7*8), 12'(3*8), 32'(31*256), "src-lock-by3bx7");
// -----------------------------------------------------------
// Block-swizzle walk: every (block_x, block_y) within page 0
// — assert the address of pixel (8*block_x, 8*block_y) is
// ref_block_idx(block_y, block_x) * 256. (Redundant with the
// source-table-lock phase above; kept as a self-check that
// ref_block_idx and the DUT agree, which the bijectivity
// sweep then relies on.)
// -----------------------------------------------------------
for (int by = 0; by < 4; by++) begin
for (int bx = 0; bx < 8; bx++) begin
logic [31:0] expected;
expected = 32'(ref_block_idx(by, bx) * 256);
check(9'd0, 6'd1, 12'(bx*8), 12'(by*8), expected,
"block-swizzle-walk");
end
end
// -----------------------------------------------------------
// Bijectivity sweep: walk every pixel in a 64×32 page (page
// 0, FBW=1, FBP=0). Assert (a) every address agrees with
// the reference function, (b) every address falls in
// [0, 8192), and (c) the addresses are pairwise unique
// (we use a 2048-entry "seen" array of word-indexed slots —
// 8192 bytes / 4 = 2048 words).
// -----------------------------------------------------------
begin : sweep
logic seen [0:2047];
for (int i = 0; i < 2048; i++) seen[i] = 1'b0;
for (int yy = 0; yy < 32; yy++) begin
for (int xx = 0; xx < 64; xx++) begin
logic [31:0] got, ref_;
int word_idx;
compute(9'd0, 6'd1, 12'(xx), 12'(yy), got);
ref_ = ref_addr(0, 1, xx, yy);
if (got !== ref_) begin
$error("sweep: (%0d,%0d) DUT=0x%08x ref=0x%08x",
xx, yy, got, ref_);
errors = errors + 1;
end
if (got >= 32'd8192) begin
$error("sweep: (%0d,%0d) addr=0x%08x out of page bounds",
xx, yy, got);
errors = errors + 1;
end
word_idx = got[12:2];
if (seen[word_idx]) begin
$error("sweep: (%0d,%0d) addr=0x%08x duplicate (word_idx=%0d)",
xx, yy, got, word_idx);
errors = errors + 1;
end
seen[word_idx] = 1'b1;
end
end
// Confirm every word slot got hit exactly once.
for (int i = 0; i < 2048; i++) begin
if (!seen[i]) begin
$error("sweep: word_idx %0d (= byte 0x%08x) never reached",
i, i*4);
errors = errors + 1;
end
end
end
// -----------------------------------------------------------
// Multi-page sanity: with FBW=2 (= 128 pixels wide), pixel
// (96, 16) should land in page (1, 0): page_idx=1,
// page_base = 8192. block (4, 2) → swizzle = 24,
// block_base = 8192 + 24*256 = 14336. xb=0, yb=0 → byte 0.
// → addr = 14336.
// -----------------------------------------------------------
check(9'd0, 6'd2, 12'd96, 12'd16, 32'd14336, "fbw2-multi-page");
// -----------------------------------------------------------
// Non-page-aligned FBP coverage: real PS2 allows FBP at any
// 2048-byte boundary (FBP[1:0] != 0 = mid-page in the 8 KiB
// sense). The math `addr = FBP*2048 + page_index*8192 +
// block_idx*256 + byte_in_block` is bit-correct for any FBP.
// FBP=1 → base 2048; FBP=2 → 4096; FBP=3 → 6144.
// (0,0): block 0, byte 0 → addr = FBP*2048.
// -----------------------------------------------------------
check(9'd1, 6'd1, 12'd0, 12'd0, 32'd2048, "fbp1-mid-page-origin");
check(9'd2, 6'd1, 12'd0, 12'd0, 32'd4096, "fbp2-mid-page-origin");
check(9'd3, 6'd1, 12'd0, 12'd0, 32'd6144, "fbp3-mid-page-origin");
// Mid-page FBP + non-trivial block: FBP=1, (16, 8) → block
// (2,1) → swizzle=6 → addr = 2048 + 6*256 + 0 = 3584.
check(9'd1, 6'd1, 12'd16, 12'd8, 32'd3584, "fbp1-mid-page-block-2-1");
// Mid-page FBP + intra-block + page-row crossing: FBP=3,
// FBW=2, (65, 33) → page (1,1) at FBP=3 base. page_index =
// 1*2 + 1 = 3, page_base = 6144 + 3*8192 = 30720. block
// (0,0)+1px right and 1px down inside the block (xb=1,yb=1)
// → 30720 + 0*256 + 1*32 + 1*4 = 30756.
check(9'd3, 6'd2, 12'd65, 12'd33, 32'd30756,
"fbp3-fbw2-page1-1-intra");
$display("[tb_gs_swizzle_psmct32] errors=%0d", errors);
if (errors == 0) $display("[tb_gs_swizzle_psmct32] PASS");
else $display("[tb_gs_swizzle_psmct32] FAIL");
$finish;
end
initial begin
#500000;
$error("[tb_gs_swizzle_psmct32] timeout");
$finish;
end
endmodule : tb_gs_swizzle_psmct32