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