// 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