// retroDE_ps2 — tb_gs_texture_swizzle_psmt4_clut (Ch298)
//
// Focused TB for SWIZZLED PSMT4 texture sampling. gs_texture_unit, built with
// PSMT4_SWIZZLE=1, must compute the texel byte address + nibble_hi through
// gs_swizzle_psmt4_stub (real PS2 block layout) instead of the linear
// gs_texel_addr — then run the SAME downstream byte-lane / nibble / CLUT path.
//
// The architect's required proofs:
//
//   PROOF 1 — SWIZZLED ADDRESS IS CORRECT (incl. ACROSS BLOCK / PAGE
//             BOUNDARIES). A TB-side reference of the EXACT same swizzle
//             formula bakes a recognizable logical index pattern into VRAM at
//             swizzled addresses; sampling with PSMT4_SWIZZLE=1 must read back
//             the ORIGINAL logical index for each (u,v). We deliberately
//             include (u,v) that cross 32-px block columns, 16-px block rows,
//             and the 128-px page boundary — exactly where the swizzle layout
//             diverges from a linear row-major one.
//
//   PROOF 2 — SWIZZLED != LINEAR for the SAME VRAM bytes. A second DUT with
//             PSMT4_SWIZZLE=0 samples the identical (swizzled-baked) VRAM and
//             the identical (u,v). For coords that land in a different
//             block/page, the linear sampler reads a DIFFERENT byte/nibble, so
//             its output index differs from the swizzled one. We assert at
//             least one across-boundary coord where swizzled != linear (proves
//             the swizzle is actually applied, not a no-op).
//
//   PROOF 3 — CLUT indirection on the swizzled path: index -> clut_stub[index]
//             -> PSMCT32 color on tex_color (DECAL), for 4-bit indices 0..15.
//
//   PROOF 4 — SEL_DELAY=1 (registered-read) alignment for the SWIZZLED
//             selectors. Under TEX_RD_REGISTERED the swizzled byte-lane
//             low bits AND nibble_hi advance while the read is in flight, so
//             they must travel the SAME SEL_DELAY pipe. A registered-read DUT
//             (RD_LATENCY=1, SEL_DELAY=1) re-pairs them; a misaligned nibble
//             would smear odd<->even.
//
// No EE/BIOS scaffolding — pure sampler unit test against a comb VRAM model.

`timescale 1ns/1ps

module tb_gs_texture_swizzle_psmt4_clut;

    localparam logic [5:0] PSMT4 = 6'h14;

    logic clk;
    logic rst_n;
    initial clk = 1'b0;
    always #5 clk = ~clk;

    // ---- combinational VRAM model (word-addressed view) ----
    // Large enough to span >1 PSMT4 page (8192 B/page) so page-boundary
    // coords land in real, distinct addresses.
    localparam int VRAM_BYTES = 65536;
    logic [7:0] vram [0:VRAM_BYTES-1];

    logic [31:0] tex_rd_addr;
    logic [31:0] tex_rd_data;
    assign tex_rd_data = { vram[(tex_rd_addr & ~32'd3) + 3],
                           vram[(tex_rd_addr & ~32'd3) + 2],
                           vram[(tex_rd_addr & ~32'd3) + 1],
                           vram[(tex_rd_addr & ~32'd3) + 0] };

    // ---- texture geometry ----
    // FBW must be even for PSMT4 swizzle (gs_swizzle uses bw_pg = FBW>>1).
    // TBW=2 -> 128 texels/row -> exactly one page wide. A 160x144 texture
    // therefore spans 2 page-rows (y 0..127 = page row 0, 128..143 = row 1)
    // and is 1.25 pages wide-ish — but for sampling we choose coords that
    // exercise across-block AND across-page boundaries explicitly below.
    localparam [31:0] T4_BASE = 32'h0000_4000;  // 2048-aligned (FBP=8)
    localparam [13:0] T4_TBW  = 14'd2;           // 128 texels/row, even
    localparam int    ROW_TEXELS = 128;          // TBW*64

    // ---- TB-side reference of the EXACT gs_swizzle_psmt4_stub formula ----
    // Returns the byte address + nibble_hi for (u,v). Must match the RTL.
    // blockTable4[by][bx] and columnTable4[yb][xb] reproduced from the same
    // PCSX2 source the RTL stub cites.
    function automatic int blocktbl4(input int by, input int bx);
        // [by(0..7)][bx(0..3)]
        case ({by[2:0], bx[1:0]})
            5'd0:  return 0;   5'd1:  return 2;   5'd2:  return 8;   5'd3:  return 10;
            5'd4:  return 1;   5'd5:  return 3;   5'd6:  return 9;   5'd7:  return 11;
            5'd8:  return 4;   5'd9:  return 6;   5'd10: return 12;  5'd11: return 14;
            5'd12: return 5;   5'd13: return 7;   5'd14: return 13;  5'd15: return 15;
            5'd16: return 16;  5'd17: return 18;  5'd18: return 24;  5'd19: return 26;
            5'd20: return 17;  5'd21: return 19;  5'd22: return 25;  5'd23: return 27;
            5'd24: return 20;  5'd25: return 22;  5'd26: return 28;  5'd27: return 30;
            5'd28: return 21;  5'd29: return 23;  5'd30: return 29;  default: return 31;
        endcase
    endfunction

    // columnTable4[yb(0..15)][xb(0..31)] -> nibble-within-block 0..511.
    // Stored as an unpacked array filled in initial; the values are the same
    // permutation the RTL's col_idx_psmt4 returns.
    int coltbl4 [0:15][0:31];

    task automatic init_coltbl4;
        // yb=0
        coltbl4[0][0]=0;   coltbl4[0][1]=8;   coltbl4[0][2]=32;  coltbl4[0][3]=40;
        coltbl4[0][4]=64;  coltbl4[0][5]=72;  coltbl4[0][6]=96;  coltbl4[0][7]=104;
        coltbl4[0][8]=2;   coltbl4[0][9]=10;  coltbl4[0][10]=34; coltbl4[0][11]=42;
        coltbl4[0][12]=66; coltbl4[0][13]=74; coltbl4[0][14]=98; coltbl4[0][15]=106;
        coltbl4[0][16]=4;  coltbl4[0][17]=12; coltbl4[0][18]=36; coltbl4[0][19]=44;
        coltbl4[0][20]=68; coltbl4[0][21]=76; coltbl4[0][22]=100;coltbl4[0][23]=108;
        coltbl4[0][24]=6;  coltbl4[0][25]=14; coltbl4[0][26]=38; coltbl4[0][27]=46;
        coltbl4[0][28]=70; coltbl4[0][29]=78; coltbl4[0][30]=102;coltbl4[0][31]=110;
        // yb=1
        coltbl4[1][0]=16;  coltbl4[1][1]=24;  coltbl4[1][2]=48;  coltbl4[1][3]=56;
        coltbl4[1][4]=80;  coltbl4[1][5]=88;  coltbl4[1][6]=112; coltbl4[1][7]=120;
        coltbl4[1][8]=18;  coltbl4[1][9]=26;  coltbl4[1][10]=50; coltbl4[1][11]=58;
        coltbl4[1][12]=82; coltbl4[1][13]=90; coltbl4[1][14]=114;coltbl4[1][15]=122;
        coltbl4[1][16]=20; coltbl4[1][17]=28; coltbl4[1][18]=52; coltbl4[1][19]=60;
        coltbl4[1][20]=84; coltbl4[1][21]=92; coltbl4[1][22]=116;coltbl4[1][23]=124;
        coltbl4[1][24]=22; coltbl4[1][25]=30; coltbl4[1][26]=54; coltbl4[1][27]=62;
        coltbl4[1][28]=86; coltbl4[1][29]=94; coltbl4[1][30]=118;coltbl4[1][31]=126;
        // yb=2
        coltbl4[2][0]=65;  coltbl4[2][1]=73;  coltbl4[2][2]=97;  coltbl4[2][3]=105;
        coltbl4[2][4]=1;   coltbl4[2][5]=9;   coltbl4[2][6]=33;  coltbl4[2][7]=41;
        coltbl4[2][8]=67;  coltbl4[2][9]=75;  coltbl4[2][10]=99; coltbl4[2][11]=107;
        coltbl4[2][12]=3;  coltbl4[2][13]=11; coltbl4[2][14]=35; coltbl4[2][15]=43;
        coltbl4[2][16]=69; coltbl4[2][17]=77; coltbl4[2][18]=101;coltbl4[2][19]=109;
        coltbl4[2][20]=5;  coltbl4[2][21]=13; coltbl4[2][22]=37; coltbl4[2][23]=45;
        coltbl4[2][24]=71; coltbl4[2][25]=79; coltbl4[2][26]=103;coltbl4[2][27]=111;
        coltbl4[2][28]=7;  coltbl4[2][29]=15; coltbl4[2][30]=39; coltbl4[2][31]=47;
        // yb=3
        coltbl4[3][0]=81;  coltbl4[3][1]=89;  coltbl4[3][2]=113; coltbl4[3][3]=121;
        coltbl4[3][4]=17;  coltbl4[3][5]=25;  coltbl4[3][6]=49;  coltbl4[3][7]=57;
        coltbl4[3][8]=83;  coltbl4[3][9]=91;  coltbl4[3][10]=115;coltbl4[3][11]=123;
        coltbl4[3][12]=19; coltbl4[3][13]=27; coltbl4[3][14]=51; coltbl4[3][15]=59;
        coltbl4[3][16]=85; coltbl4[3][17]=93; coltbl4[3][18]=117;coltbl4[3][19]=125;
        coltbl4[3][20]=21; coltbl4[3][21]=29; coltbl4[3][22]=53; coltbl4[3][23]=61;
        coltbl4[3][24]=87; coltbl4[3][25]=95; coltbl4[3][26]=119;coltbl4[3][27]=127;
        coltbl4[3][28]=23; coltbl4[3][29]=31; coltbl4[3][30]=55; coltbl4[3][31]=63;
        // yb=4
        coltbl4[4][0]=192; coltbl4[4][1]=200; coltbl4[4][2]=224; coltbl4[4][3]=232;
        coltbl4[4][4]=128; coltbl4[4][5]=136; coltbl4[4][6]=160; coltbl4[4][7]=168;
        coltbl4[4][8]=194; coltbl4[4][9]=202; coltbl4[4][10]=226;coltbl4[4][11]=234;
        coltbl4[4][12]=130;coltbl4[4][13]=138;coltbl4[4][14]=162;coltbl4[4][15]=170;
        coltbl4[4][16]=196;coltbl4[4][17]=204;coltbl4[4][18]=228;coltbl4[4][19]=236;
        coltbl4[4][20]=132;coltbl4[4][21]=140;coltbl4[4][22]=164;coltbl4[4][23]=172;
        coltbl4[4][24]=198;coltbl4[4][25]=206;coltbl4[4][26]=230;coltbl4[4][27]=238;
        coltbl4[4][28]=134;coltbl4[4][29]=142;coltbl4[4][30]=166;coltbl4[4][31]=174;
        // yb=5
        coltbl4[5][0]=208; coltbl4[5][1]=216; coltbl4[5][2]=240; coltbl4[5][3]=248;
        coltbl4[5][4]=144; coltbl4[5][5]=152; coltbl4[5][6]=176; coltbl4[5][7]=184;
        coltbl4[5][8]=210; coltbl4[5][9]=218; coltbl4[5][10]=242;coltbl4[5][11]=250;
        coltbl4[5][12]=146;coltbl4[5][13]=154;coltbl4[5][14]=178;coltbl4[5][15]=186;
        coltbl4[5][16]=212;coltbl4[5][17]=220;coltbl4[5][18]=244;coltbl4[5][19]=252;
        coltbl4[5][20]=148;coltbl4[5][21]=156;coltbl4[5][22]=180;coltbl4[5][23]=188;
        coltbl4[5][24]=214;coltbl4[5][25]=222;coltbl4[5][26]=246;coltbl4[5][27]=254;
        coltbl4[5][28]=150;coltbl4[5][29]=158;coltbl4[5][30]=182;coltbl4[5][31]=190;
        // yb=6
        coltbl4[6][0]=129; coltbl4[6][1]=137; coltbl4[6][2]=161; coltbl4[6][3]=169;
        coltbl4[6][4]=193; coltbl4[6][5]=201; coltbl4[6][6]=225; coltbl4[6][7]=233;
        coltbl4[6][8]=131; coltbl4[6][9]=139; coltbl4[6][10]=163;coltbl4[6][11]=171;
        coltbl4[6][12]=195;coltbl4[6][13]=203;coltbl4[6][14]=227;coltbl4[6][15]=235;
        coltbl4[6][16]=133;coltbl4[6][17]=141;coltbl4[6][18]=165;coltbl4[6][19]=173;
        coltbl4[6][20]=197;coltbl4[6][21]=205;coltbl4[6][22]=229;coltbl4[6][23]=237;
        coltbl4[6][24]=135;coltbl4[6][25]=143;coltbl4[6][26]=167;coltbl4[6][27]=175;
        coltbl4[6][28]=199;coltbl4[6][29]=207;coltbl4[6][30]=231;coltbl4[6][31]=239;
        // yb=7
        coltbl4[7][0]=145; coltbl4[7][1]=153; coltbl4[7][2]=177; coltbl4[7][3]=185;
        coltbl4[7][4]=209; coltbl4[7][5]=217; coltbl4[7][6]=241; coltbl4[7][7]=249;
        coltbl4[7][8]=147; coltbl4[7][9]=155; coltbl4[7][10]=179;coltbl4[7][11]=187;
        coltbl4[7][12]=211;coltbl4[7][13]=219;coltbl4[7][14]=243;coltbl4[7][15]=251;
        coltbl4[7][16]=149;coltbl4[7][17]=157;coltbl4[7][18]=181;coltbl4[7][19]=189;
        coltbl4[7][20]=213;coltbl4[7][21]=221;coltbl4[7][22]=245;coltbl4[7][23]=253;
        coltbl4[7][24]=151;coltbl4[7][25]=159;coltbl4[7][26]=183;coltbl4[7][27]=191;
        coltbl4[7][28]=215;coltbl4[7][29]=223;coltbl4[7][30]=247;coltbl4[7][31]=255;
        // yb=8
        coltbl4[8][0]=256; coltbl4[8][1]=264; coltbl4[8][2]=288; coltbl4[8][3]=296;
        coltbl4[8][4]=320; coltbl4[8][5]=328; coltbl4[8][6]=352; coltbl4[8][7]=360;
        coltbl4[8][8]=258; coltbl4[8][9]=266; coltbl4[8][10]=290;coltbl4[8][11]=298;
        coltbl4[8][12]=322;coltbl4[8][13]=330;coltbl4[8][14]=354;coltbl4[8][15]=362;
        coltbl4[8][16]=260;coltbl4[8][17]=268;coltbl4[8][18]=292;coltbl4[8][19]=300;
        coltbl4[8][20]=324;coltbl4[8][21]=332;coltbl4[8][22]=356;coltbl4[8][23]=364;
        coltbl4[8][24]=262;coltbl4[8][25]=270;coltbl4[8][26]=294;coltbl4[8][27]=302;
        coltbl4[8][28]=326;coltbl4[8][29]=334;coltbl4[8][30]=358;coltbl4[8][31]=366;
        // yb=9
        coltbl4[9][0]=272; coltbl4[9][1]=280; coltbl4[9][2]=304; coltbl4[9][3]=312;
        coltbl4[9][4]=336; coltbl4[9][5]=344; coltbl4[9][6]=368; coltbl4[9][7]=376;
        coltbl4[9][8]=274; coltbl4[9][9]=282; coltbl4[9][10]=306;coltbl4[9][11]=314;
        coltbl4[9][12]=338;coltbl4[9][13]=346;coltbl4[9][14]=370;coltbl4[9][15]=378;
        coltbl4[9][16]=276;coltbl4[9][17]=284;coltbl4[9][18]=308;coltbl4[9][19]=316;
        coltbl4[9][20]=340;coltbl4[9][21]=348;coltbl4[9][22]=372;coltbl4[9][23]=380;
        coltbl4[9][24]=278;coltbl4[9][25]=286;coltbl4[9][26]=310;coltbl4[9][27]=318;
        coltbl4[9][28]=342;coltbl4[9][29]=350;coltbl4[9][30]=374;coltbl4[9][31]=382;
        // yb=10
        coltbl4[10][0]=321; coltbl4[10][1]=329; coltbl4[10][2]=353; coltbl4[10][3]=361;
        coltbl4[10][4]=257; coltbl4[10][5]=265; coltbl4[10][6]=289; coltbl4[10][7]=297;
        coltbl4[10][8]=323; coltbl4[10][9]=331; coltbl4[10][10]=355;coltbl4[10][11]=363;
        coltbl4[10][12]=259;coltbl4[10][13]=267;coltbl4[10][14]=291;coltbl4[10][15]=299;
        coltbl4[10][16]=325;coltbl4[10][17]=333;coltbl4[10][18]=357;coltbl4[10][19]=365;
        coltbl4[10][20]=261;coltbl4[10][21]=269;coltbl4[10][22]=293;coltbl4[10][23]=301;
        coltbl4[10][24]=327;coltbl4[10][25]=335;coltbl4[10][26]=359;coltbl4[10][27]=367;
        coltbl4[10][28]=263;coltbl4[10][29]=271;coltbl4[10][30]=295;coltbl4[10][31]=303;
        // yb=11
        coltbl4[11][0]=337; coltbl4[11][1]=345; coltbl4[11][2]=369; coltbl4[11][3]=377;
        coltbl4[11][4]=273; coltbl4[11][5]=281; coltbl4[11][6]=305; coltbl4[11][7]=313;
        coltbl4[11][8]=339; coltbl4[11][9]=347; coltbl4[11][10]=371;coltbl4[11][11]=379;
        coltbl4[11][12]=275;coltbl4[11][13]=283;coltbl4[11][14]=307;coltbl4[11][15]=315;
        coltbl4[11][16]=341;coltbl4[11][17]=349;coltbl4[11][18]=373;coltbl4[11][19]=381;
        coltbl4[11][20]=277;coltbl4[11][21]=285;coltbl4[11][22]=309;coltbl4[11][23]=317;
        coltbl4[11][24]=343;coltbl4[11][25]=351;coltbl4[11][26]=375;coltbl4[11][27]=383;
        coltbl4[11][28]=279;coltbl4[11][29]=287;coltbl4[11][30]=311;coltbl4[11][31]=319;
        // yb=12
        coltbl4[12][0]=448; coltbl4[12][1]=456; coltbl4[12][2]=480; coltbl4[12][3]=488;
        coltbl4[12][4]=384; coltbl4[12][5]=392; coltbl4[12][6]=416; coltbl4[12][7]=424;
        coltbl4[12][8]=450; coltbl4[12][9]=458; coltbl4[12][10]=482;coltbl4[12][11]=490;
        coltbl4[12][12]=386;coltbl4[12][13]=394;coltbl4[12][14]=418;coltbl4[12][15]=426;
        coltbl4[12][16]=452;coltbl4[12][17]=460;coltbl4[12][18]=484;coltbl4[12][19]=492;
        coltbl4[12][20]=388;coltbl4[12][21]=396;coltbl4[12][22]=420;coltbl4[12][23]=428;
        coltbl4[12][24]=454;coltbl4[12][25]=462;coltbl4[12][26]=486;coltbl4[12][27]=494;
        coltbl4[12][28]=390;coltbl4[12][29]=398;coltbl4[12][30]=422;coltbl4[12][31]=430;
        // yb=13
        coltbl4[13][0]=464; coltbl4[13][1]=472; coltbl4[13][2]=496; coltbl4[13][3]=504;
        coltbl4[13][4]=400; coltbl4[13][5]=408; coltbl4[13][6]=432; coltbl4[13][7]=440;
        coltbl4[13][8]=466; coltbl4[13][9]=474; coltbl4[13][10]=498;coltbl4[13][11]=506;
        coltbl4[13][12]=402;coltbl4[13][13]=410;coltbl4[13][14]=434;coltbl4[13][15]=442;
        coltbl4[13][16]=468;coltbl4[13][17]=476;coltbl4[13][18]=500;coltbl4[13][19]=508;
        coltbl4[13][20]=404;coltbl4[13][21]=412;coltbl4[13][22]=436;coltbl4[13][23]=444;
        coltbl4[13][24]=470;coltbl4[13][25]=478;coltbl4[13][26]=502;coltbl4[13][27]=510;
        coltbl4[13][28]=406;coltbl4[13][29]=414;coltbl4[13][30]=438;coltbl4[13][31]=446;
        // yb=14
        coltbl4[14][0]=385; coltbl4[14][1]=393; coltbl4[14][2]=417; coltbl4[14][3]=425;
        coltbl4[14][4]=449; coltbl4[14][5]=457; coltbl4[14][6]=481; coltbl4[14][7]=489;
        coltbl4[14][8]=387; coltbl4[14][9]=395; coltbl4[14][10]=419;coltbl4[14][11]=427;
        coltbl4[14][12]=451;coltbl4[14][13]=459;coltbl4[14][14]=483;coltbl4[14][15]=491;
        coltbl4[14][16]=389;coltbl4[14][17]=397;coltbl4[14][18]=421;coltbl4[14][19]=429;
        coltbl4[14][20]=453;coltbl4[14][21]=461;coltbl4[14][22]=485;coltbl4[14][23]=493;
        coltbl4[14][24]=391;coltbl4[14][25]=399;coltbl4[14][26]=423;coltbl4[14][27]=431;
        coltbl4[14][28]=455;coltbl4[14][29]=463;coltbl4[14][30]=487;coltbl4[14][31]=495;
        // yb=15
        coltbl4[15][0]=401; coltbl4[15][1]=409; coltbl4[15][2]=433; coltbl4[15][3]=441;
        coltbl4[15][4]=465; coltbl4[15][5]=473; coltbl4[15][6]=497; coltbl4[15][7]=505;
        coltbl4[15][8]=403; coltbl4[15][9]=411; coltbl4[15][10]=435;coltbl4[15][11]=443;
        coltbl4[15][12]=467;coltbl4[15][13]=475;coltbl4[15][14]=499;coltbl4[15][15]=507;
        coltbl4[15][16]=405;coltbl4[15][17]=413;coltbl4[15][18]=437;coltbl4[15][19]=445;
        coltbl4[15][20]=469;coltbl4[15][21]=477;coltbl4[15][22]=501;coltbl4[15][23]=509;
        coltbl4[15][24]=407;coltbl4[15][25]=415;coltbl4[15][26]=439;coltbl4[15][27]=447;
        coltbl4[15][28]=471;coltbl4[15][29]=479;coltbl4[15][30]=503;coltbl4[15][31]=511;
    endtask

    // reference swizzled byte address + nibble for (u,v) at our texture.
    task automatic ref_swizzle(input int uu, input int vv,
                               output logic [31:0] baddr, output logic nib_hi);
        int page_x, page_y, by, bx, xb, yb, bw_pg;
        int page_index, nibble_idx, block_idx;
        longint page_base, block_base;
        page_x = uu >> 7;
        page_y = vv >> 7;
        by     = (vv >> 4) & 3'h7;
        bx     = (uu >> 5) & 2'h3;
        xb     = uu & 5'h1f;
        yb     = vv & 4'hf;
        bw_pg  = T4_TBW >> 1;
        page_index = page_y * bw_pg + page_x;
        page_base  = (longint'(T4_BASE)) + longint'(page_index) * 8192;
        block_idx  = blocktbl4(by, bx);
        block_base = page_base + longint'(block_idx) * 256;
        nibble_idx = coltbl4[yb][xb];
        baddr  = 32'(block_base + (nibble_idx >> 1));
        nib_hi = nibble_idx[0];
    endtask

    // ---- two DUTs sharing the comb VRAM model ----
    logic        in_valid;
    logic [10:0] u, v;
    logic [31:0] tbp0_base_bytes;
    logic [13:0] tbw;
    logic [5:0]  psm;

    // swizzled DUT
    logic [31:0] sw_rd_addr;
    logic [7:0]  sw_clut_idx;
    logic [31:0] sw_clut_data;
    logic [31:0] sw_tex_color;
    logic [31:0] sw_rd_data;
    assign sw_rd_data = { vram[(sw_rd_addr & ~32'd3) + 3],
                          vram[(sw_rd_addr & ~32'd3) + 2],
                          vram[(sw_rd_addr & ~32'd3) + 1],
                          vram[(sw_rd_addr & ~32'd3) + 0] };

    gs_texture_unit #(.PSMT4_SWIZZLE(1'b1), .RD_LATENCY(1), .SEL_DELAY(0)) dut_sw (
        .clk(clk), .rst_n(rst_n),
        .in_valid(in_valid), .u(u), .v(v),
        .tbp0_base_bytes(tbp0_base_bytes), .tbw(tbw), .psm(psm),
        .tex_rd_en(), .tex_rd_addr(sw_rd_addr), .tex_rd_data(sw_rd_data),
        .clut_rd_idx(sw_clut_idx), .clut_rd_data(sw_clut_data),
        .out_valid(), .tex_color(sw_tex_color)
    );

    // linear DUT (PSMT4_SWIZZLE=0) sampling the SAME VRAM bytes.
    logic [31:0] li_rd_addr;
    logic [7:0]  li_clut_idx;
    logic [31:0] li_clut_data;
    logic [31:0] li_tex_color;
    logic [31:0] li_rd_data;
    assign li_rd_data = { vram[(li_rd_addr & ~32'd3) + 3],
                          vram[(li_rd_addr & ~32'd3) + 2],
                          vram[(li_rd_addr & ~32'd3) + 1],
                          vram[(li_rd_addr & ~32'd3) + 0] };

    gs_texture_unit #(.PSMT4_SWIZZLE(1'b0), .RD_LATENCY(1), .SEL_DELAY(0)) dut_li (
        .clk(clk), .rst_n(rst_n),
        .in_valid(in_valid), .u(u), .v(v),
        .tbp0_base_bytes(tbp0_base_bytes), .tbw(tbw), .psm(psm),
        .tex_rd_en(), .tex_rd_addr(li_rd_addr), .tex_rd_data(li_rd_data),
        .clut_rd_idx(li_clut_idx), .clut_rd_data(li_clut_data),
        .out_valid(), .tex_color(li_tex_color)
    );

    // ---- CLUT ----
    logic        clut_we;
    logic [7:0]  clut_widx;
    logic [31:0] clut_wdata;
    clut_stub u_clut (
        .clk(clk), .rst_n(rst_n),
        .write_en(clut_we), .write_idx(clut_widx), .write_data(clut_wdata),
        .read_idx(8'd0), .read_data(),
        .tex_read_idx(sw_clut_idx), .tex_read_data(sw_clut_data)
    );

    task automatic clut_write(input logic [7:0] idx, input logic [31:0] data);
        @(negedge clk); clut_we=1'b1; clut_widx=idx; clut_wdata=data;
        @(negedge clk); clut_we=1'b0;
    endtask

    // recognizable logical index pattern. (u+v) ramp gives a smooth gradient
    // that is unmistakably scrambled if read with the wrong (linear) layout.
    function automatic logic [3:0] idx_pattern(input int x, input int y);
        return 4'((x + y) % 16);
    endfunction

    // Palette: distinct PSMCT32 per index.
    function automatic logic [31:0] pal(input logic [3:0] i);
        return 32'hFF000000 | ({28'd0, i} * 32'h00112233) | 32'h00000010;
    endfunction

    // Bake the logical index pattern into VRAM at SWIZZLED addresses, so a
    // swizzled READ recovers the original pattern. Two texels share a byte;
    // write each nibble individually (read-modify-write into the model).
    task automatic upload_swizzled(input int w, input int h);
        logic [31:0] ba; logic nh; logic [3:0] idx; logic [7:0] cur;
        for (int yy = 0; yy < h; yy++) begin
            for (int xx = 0; xx < w; xx++) begin
                ref_swizzle(xx, yy, ba, nh);
                idx = idx_pattern(xx, yy);
                cur = vram[ba];
                if (nh) cur = {idx, cur[3:0]};
                else    cur = {cur[7:4], idx};
                vram[ba] = cur;
            end
        end
    endtask

    // ---- sample helpers (comb, SEL_DELAY=0) ----
    task automatic sample(input int su, input int sv);
        @(negedge clk); in_valid=1'b1; u=11'(su); v=11'(sv); #2;
    endtask

    int errors;
    int sw_ne_li;     // count of across-boundary coords where swz != linear
    bit sel_done;
    logic [31:0] ba; logic nh;
    logic [3:0]  expidx;

    initial begin
        errors = 0; sw_ne_li = 0; sel_done = 1'b0;
        rst_n = 1'b0; in_valid=1'b0; u=0; v=0;
        tbp0_base_bytes = T4_BASE; tbw = T4_TBW; psm = PSMT4;
        clut_we=1'b0; clut_widx=0; clut_wdata=0;
        for (int i=0;i<VRAM_BYTES;i++) vram[i]=8'd0;
        init_coltbl4();
        // texture spans 160 wide x 144 tall -> crosses the 128-px page
        // boundary in BOTH x and y, plus all block boundaries within a page.
        upload_swizzled(160, 144);

        repeat (4) @(posedge clk); rst_n=1'b1; repeat (2) @(posedge clk);
        for (int i=0;i<16;i++) clut_write(8'(i), pal(4'(i)));

        // ============================================================
        // PROOF 1 + PROOF 3 — swizzled read recovers logical pattern,
        // CLUT maps index->color. Sweep a grid that crosses block (x=32,
        // y=16) AND page (x=128, y=128) boundaries.
        // ============================================================
        for (int yy = 0; yy < 144; yy += 7) begin
            for (int xx = 0; xx < 160; xx += 5) begin
                expidx = idx_pattern(xx, yy);
                ref_swizzle(xx, yy, ba, nh);
                sample(xx, yy);
                // address the DUT issued must equal the swizzled byte addr
                // (word-aligned) we baked at.
                if ((sw_rd_addr & ~32'd3) !== (ba & ~32'd3)) begin
                    $error("[P1 addr] (%0d,%0d) dut_addr=0x%08x ref=0x%08x",
                           xx, yy, sw_rd_addr, ba); errors++;
                end
                if (sw_clut_idx !== {4'd0, expidx}) begin
                    $error("[P1 idx] (%0d,%0d) got idx=0x%02x exp 0x%01x",
                           xx, yy, sw_clut_idx, expidx); errors++;
                end
                if (sw_tex_color !== pal(expidx)) begin
                    $error("[P3 color] (%0d,%0d) idx=0x%01x got=%08x exp=%08x",
                           xx, yy, expidx, sw_tex_color, pal(expidx)); errors++;
                end
            end
        end

        // ============================================================
        // PROOF 2 — swizzled != linear for the SAME bytes. For coords that
        // land in a DIFFERENT block than the linear layout would address,
        // the linear DUT reads a different nibble -> different index. Count
        // such coords; require at least a healthy number (proves not a no-op).
        // We compare the resolved INDEX (the meaningful sampler output).
        // ============================================================
        for (int yy = 0; yy < 144; yy += 3) begin
            for (int xx = 0; xx < 160; xx += 3) begin
                sample(xx, yy);
                if (sw_clut_idx !== li_clut_idx) sw_ne_li++;
            end
        end
        if (sw_ne_li < 32) begin
            $error("[P2] swizzled vs linear index differs in only %0d coords — swizzle may be a no-op", sw_ne_li);
            errors++;
        end

        // explicit across-page-boundary pair: (0,0) is page (0,0) block 0;
        // (130,2) is page (1,0). Linear addressing of (130,2) lands in the
        // SAME page-0 region (just a few bytes in), so the swizzled vs linear
        // address MUST differ for the page-crossing coord.
        sample(130, 2);
        begin
            logic [31:0] sw_a, li_a;
            sw_a = sw_rd_addr & ~32'd3;
            li_a = li_rd_addr & ~32'd3;
            if (sw_a === li_a) begin
                $error("[P2 page] (130,2) swizzled addr == linear addr 0x%08x (page boundary not honored)", sw_a);
                errors++;
            end
            // swizzled must be in page 1 (>= base+8192); linear stays near base.
            if (sw_a < (T4_BASE + 8192)) begin
                $error("[P2 page] (130,2) swizzled addr 0x%08x not in page 1 (>=0x%08x)", sw_a, T4_BASE+8192);
                errors++;
            end
        end

        wait (sel_done == 1'b1);
        $display("[tb_gs_texture_swizzle_psmt4_clut] swizzled samples checked, swz!=lin coords=%0d errors=%0d",
                 sw_ne_li, errors);
        if (errors == 0) $display("[tb_gs_texture_swizzle_psmt4_clut] PASS");
        else             $display("[tb_gs_texture_swizzle_psmt4_clut] FAIL");
        $finish;
    end

    // ================================================================
    // PROOF 4 — SEL_DELAY=1 registered-read alignment for SWIZZLED selectors.
    // ================================================================
    logic        d2_in_valid;
    logic [10:0] d2_u, d2_v;
    logic [31:0] d2_rd_addr;
    logic [31:0] d2_rd_data_q;
    logic [7:0]  d2_clut_idx;
    logic [31:0] d2_clut_data;
    logic [31:0] d2_tex_color;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) d2_rd_data_q <= 32'd0;
        else d2_rd_data_q <= { vram[(d2_rd_addr & ~32'd3) + 3],
                               vram[(d2_rd_addr & ~32'd3) + 2],
                               vram[(d2_rd_addr & ~32'd3) + 1],
                               vram[(d2_rd_addr & ~32'd3) + 0] };
    end

    gs_texture_unit #(.PSMT4_SWIZZLE(1'b1), .RD_LATENCY(1), .SEL_DELAY(1)) dut2 (
        .clk(clk), .rst_n(rst_n),
        .in_valid(d2_in_valid), .u(d2_u), .v(d2_v),
        .tbp0_base_bytes(T4_BASE), .tbw(T4_TBW), .psm(PSMT4),
        .tex_rd_en(), .tex_rd_addr(d2_rd_addr), .tex_rd_data(d2_rd_data_q),
        .clut_rd_idx(d2_clut_idx), .clut_rd_data(d2_clut_data),
        .out_valid(), .tex_color(d2_tex_color)
    );

    clut_stub u_clut2 (
        .clk(clk), .rst_n(rst_n),
        .write_en(clut_we), .write_idx(clut_widx), .write_data(clut_wdata),
        .read_idx(8'd0), .read_data(),
        .tex_read_idx(d2_clut_idx), .tex_read_data(d2_clut_data)
    );

    int errors2;

    initial begin
        errors2 = 0;
        d2_in_valid=1'b0; d2_u=0; d2_v=0;
        wait (rst_n == 1'b1);
        repeat (60) @(posedge clk);   // palette + main scan underway

        // HELD-address sweep across a row crossing block & page boundaries:
        // address registered, data + pipe aligned, then verify recovered idx.
        for (int xx = 0; xx < 160; xx += 11) begin
            logic [3:0] ei;
            @(negedge clk); d2_in_valid=1'b1; d2_u=11'(xx); d2_v=11'(3);
            @(posedge clk);            // address registered into read
            @(negedge clk); #2;        // data + SEL_DELAY pipe aligned
            ei = idx_pattern(xx, 3);
            if (d2_clut_idx !== {4'd0, ei}) begin
                $error("[P4 SEL_DELAY=1 held] x=%0d got idx=0x%02x exp 0x%01x (swizzled nib/byte mis-pipe)",
                       xx, d2_clut_idx, ei); errors2++;
            end
        end
        d2_in_valid=1'b0;

        if (errors2 == 0) $display("[tb_gs_texture_swizzle_psmt4_clut] SEL_DELAY=1 swizzle-align PASS");
        else              $display("[tb_gs_texture_swizzle_psmt4_clut] SEL_DELAY=1 swizzle-align FAIL errors=%0d", errors2);
        errors = errors + errors2;
        sel_done = 1'b1;
    end

    initial begin
        #5000000;
        $error("[tb_gs_texture_swizzle_psmt4_clut] TIMEOUT");
        $finish;
    end

endmodule : tb_gs_texture_swizzle_psmt4_clut
