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>
358 lines
17 KiB
Systemverilog
358 lines
17 KiB
Systemverilog
// 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
|