// retroDE_ps2 — tb_gs_scanout_swizzle_psmt8 (Ch132)
//
// Read-side wiring of the Ch131 PSMT8 swizzle math primitive into
// PCRTC scanout. PSMT8 framebuffer reads now go through
// gs_swizzle_psmt8_stub when the new `PSMT8_SWIZZLE=1` parameter
// is set on gs_pcrtc_stub. This is the PSMT8 counterpart of
// Ch120's tb_gs_scanout_swizzle_psmct32 + Ch126's
// tb_gs_scanout_swizzle_psmct16, the gentlest possible first
// integration step for the Ch131 primitive: read-only path, no
// write-side blast radius.
//
// Topology (no gs_stub — keeps the read-side proof isolated;
// write-side gates are future chapters):
//
//   TB.preload  →  vram_stub  →  gs_pcrtc_stub#(PSMT8_SWIZZLE=1) → r/g/b
//
// Two-phase verification (Phase 1 = origin / column-table walk;
// Phase 2 = non-origin / non-zero-everywhere — same shape as
// Ch126 to make page_x, page_y, FBP, FBW, block_x, and block_y
// all load-bearing in the PCRTC wiring).
//
// PHASE 1 — origin (FBP=0, FBW=2, DBX=DBY=0; FBW must be even
//           for PSMT8 — PCSX2 asserts (bw & 1) == 0).
//   16×8 region preloaded at swizzled PSMT8 addresses; x∈[0..15]
//   is entirely block_x_in_page=0 so the within-block test
//   exercises ALL 16 xb positions of `columnTable8` across yb
//   rows 0..7. Each pixel gets a unique 8-bit index value
//   `idx = (y[2:0] << 4) | x[3:0]` ∈ [0x00..0x7F]. PCRTC's PSMT8
//   grayscale fallback (clut_enable=0) produces R=G=B=idx, so
//   misaddressed bytes surface as wrong RGB. Locks columnTable8 +
//   the within-block / single-block path.
//
//   Strict separators inside Phase 1:
//     (a) y=1, x=0 — PSMT8 linear y=1 starts at byte 128 (FBW=2 →
//         128 byte stride/row). Swizzled lands at byte 8
//         (columnTable8[1][0]=8 directly — no *2 scale, since
//         PSMT8 is 1 byte/pixel).
//     (b) x=8, y=0 — Linear gives byte 8; swizzled gives byte 2
//         (columnTable8[0][8]=2).
//
// PHASE 2 — non-origin (FBP=4, FBW=4, DBX=120, DBY=60).
//   Effective coords range x∈[120..135], y∈[60..67]:
//     • FBP=4 → byte_base = 8192 (non-zero).
//     • FBW=4 → bw_pg = 2, so page_index = page_y*2 + page_x uses
//       BOTH page-x AND page-y bits.
//     • page_x crosses 0→1 at effective_x=128 (proves x[7] reaches
//       the page-x lane of the PSMT8 swizzle — different boundary
//       from CT16/CT32's x[6]).
//     • page_y crosses 0→1 at effective_y=64.
//     • block_x at page_x=0 is 7 (x∈[120..127] / 16 = 7);
//       block_x at page_x=1 is 0 (x∈[128..135] %128 / 16 = 0).
//     • block_y at page_y=0 is 3 (y∈[60..63] / 16 = 3);
//       block_y at page_y=1 is 0 (y∈[64..67] %64 / 16 = 0).
//     • All 4 pages (0,0)/(1,0)/(0,1)/(1,1) are visited, each
//       with a distinct (block_y, block_x) pair → 4 distinct
//       blockTable8 lookups: [3][7]=31, [3][0]=10, [0][7]=21,
//       [0][0]=0.
//   A regression that tied any of {dispfb_fbp, dbx, dby, FBW,
//   block_x, block_y, page_index, bw_pg=FBW/2} to zero in the
//   PCRTC wiring would NOT survive Phase 2.
//
//   Strict separator for Phase 2: linear formula puts effective
//   coord (ex, ey) at byte = FBP*2048 + (ey*FBW*64 + ex). For the
//   Phase-2 effective region (ex∈[120..135], ey∈[60..67]) only
//   8 short row-spans of 16 bytes each are actually sampled by
//   the linear formula:
//     ey=60: 23672..23687     ey=64: 24696..24711
//     ey=61: 23928..23943     ey=65: 24952..24967
//     ey=62: 24184..24199     ey=66: 25208..25223
//     ey=63: 24440..24455     ey=67: 25464..25479
//   The Phase-2 swizzled write set lives in 4 disjoint block-sized
//   ranges, all OUTSIDE the linear sampled set:
//     • page (0,0) block (3,7): bytes 16128..16383
//     • page (1,0) block (3,0): bytes 18944..19199
//     • page (0,1) block (0,7): bytes 29952..30207
//     • page (1,1) block (0,0): bytes 32768..33023
//   We pick byte 24696 (the start of the ey=64 linear row, which
//   IS sampled — corresponds to linear coord (ex=120, ey=64)) and
//   pre-color it with sentinel 0xFF. A fall-through to linear would
//   surface as RGB(0xFF, 0xFF, 0xFF) at captured (xx=0, yy=4).
//   Phase 2's unique pattern uses idx ∈ [0x00..0x7F] so 0xFF is
//   unproducible — a fall-through would surface as a per-pixel
//   verify failure at that exact slot.
//
// Swizzle math (mirrored from gs_swizzle_psmt8_stub):
//   page_x = x>>7; page_y = y>>6
//   bw_pg  = FBW>>1
//   page_index = page_y*bw_pg + page_x
//   page_base  = FBP*2048 + page_index*8192
//   block_idx  = blockTable8[(y%64)/16][(x%128)/16]
//   addr       = page_base + block_idx*256 + columnTable8[y%16][x%16]

`timescale 1ns/1ps

module tb_gs_scanout_swizzle_psmt8;

    localparam int PCRTC_H_ACTIVE = 16;
    localparam int PCRTC_V_ACTIVE = 8;

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

    // -----------------------------------------------------------
    // Privileged-register feeds (TB-direct, no gs_stub).
    // -----------------------------------------------------------
    logic [63:0] pmode_q;
    logic [63:0] dispfb1_q;
    logic [63:0] display1_q;

    // -----------------------------------------------------------
    // VRAM with TB-driven write port.
    // -----------------------------------------------------------
    logic        vram_we;
    logic [31:0] vram_waddr;
    logic [31:0] vram_wdata;
    logic [3:0]  vram_wbe;
    logic [31:0] vram_raddr;
    logic [31:0] vram_rdata;

    vram_stub #(.BYTES(65536)) u_vram (
        .clk(clk), .rst_n(rst_n),
        .write_en  (vram_we),
        .write_addr(vram_waddr),
        .write_data(vram_wdata),
        .write_be  (vram_wbe),
        .write_mask(32'hFFFF_FFFF),
        .read_addr (vram_raddr),
        .read_data (vram_rdata),
        .read2_addr(32'd0),
        .read2_data()
    );

    logic       hsync_o, vsync_o, de_o;
    logic [7:0] r_o, g_o, b_o;
    logic                pcrtc_ev_valid;
    trace_pkg::subsys_e  pcrtc_ev_subsys;
    trace_pkg::event_e   pcrtc_ev_event;
    logic [63:0]         pcrtc_ev_arg0, pcrtc_ev_arg1;
    logic [63:0]         pcrtc_ev_arg2, pcrtc_ev_arg3;
    logic [31:0]         pcrtc_ev_flags;

    // -----------------------------------------------------------
    // PCRTC with the new Ch132 PSMT8_SWIZZLE gate ON (and the
    // Ch120 PSMCT32_SWIZZLE / Ch126 PSMCT16_SWIZZLE gates
    // explicitly OFF — this TB exercises only the PSMT8 path).
    // -----------------------------------------------------------
    gs_pcrtc_stub #(
        .H_ACTIVE(PCRTC_H_ACTIVE), .H_FRONT(1), .H_SYNC(1), .H_BACK(1),
        .V_ACTIVE(PCRTC_V_ACTIVE), .V_FRONT(1), .V_SYNC(1), .V_BACK(1),
        .PSMCT32_SWIZZLE(1'b0),
        .PSMCT16_SWIZZLE(1'b0),
        .PSMT8_SWIZZLE  (1'b1)
    ) u_pcrtc (
        .clk(clk), .rst_n(rst_n),
        .pmode_q       (pmode_q),
        .dispfb1_q     (dispfb1_q),
        .display1_q    (display1_q),
        .vram_read_addr(vram_raddr),
        .vram_read_data(vram_rdata),
        .clut_enable   (1'b0),
        .clut_csa      (5'd0),
        .clut_read_idx (),
        .clut_read_data(32'd0),
        .hsync(hsync_o), .vsync(vsync_o), .de(de_o),
        .r(r_o), .g(g_o), .b(b_o),
        .ev_valid(pcrtc_ev_valid),
        .ev_subsys(pcrtc_ev_subsys),
        .ev_event(pcrtc_ev_event),
        .ev_arg0(pcrtc_ev_arg0), .ev_arg1(pcrtc_ev_arg1),
        .ev_arg2(pcrtc_ev_arg2), .ev_arg3(pcrtc_ev_arg3),
        .ev_flags(pcrtc_ev_flags)
    );

    // -----------------------------------------------------------
    // Frame capture.
    // -----------------------------------------------------------
    logic [7:0] cap_r [0:PCRTC_V_ACTIVE-1][0:PCRTC_H_ACTIVE-1];
    logic [7:0] cap_g [0:PCRTC_V_ACTIVE-1][0:PCRTC_H_ACTIVE-1];
    logic [7:0] cap_b [0:PCRTC_V_ACTIVE-1][0:PCRTC_H_ACTIVE-1];
    logic       cap_de[0:PCRTC_V_ACTIVE-1][0:PCRTC_H_ACTIVE-1];

    int  errors;
    bit  capture_armed;

    initial begin
        for (int y = 0; y < PCRTC_V_ACTIVE; y++)
            for (int x = 0; x < PCRTC_H_ACTIVE; x++) begin
                cap_r[y][x]  = 8'd0;
                cap_g[y][x]  = 8'd0;
                cap_b[y][x]  = 8'd0;
                cap_de[y][x] = 1'b0;
            end
        errors        = 0;
        capture_armed = 1'b0;
    end

    always_ff @(posedge clk) begin
        if (rst_n && capture_armed && de_o
            && (u_pcrtc.vcnt < PCRTC_V_ACTIVE)
            && (u_pcrtc.hcnt < PCRTC_H_ACTIVE)) begin
            cap_r [u_pcrtc.vcnt][u_pcrtc.hcnt] <= r_o;
            cap_g [u_pcrtc.vcnt][u_pcrtc.hcnt] <= g_o;
            cap_b [u_pcrtc.vcnt][u_pcrtc.hcnt] <= b_o;
            cap_de[u_pcrtc.vcnt][u_pcrtc.hcnt] <= 1'b1;
        end
    end

    // -----------------------------------------------------------
    // PSMT8 swizzle reference (verbatim from PCSX2 + Ch131
    // verification — locked at byte level by tb_gs_swizzle_psmt8).
    // -----------------------------------------------------------
    function automatic int ref_block_idx8(input int by, input int bx);
        case ({by[1:0], bx[2:0]})
            // by=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;
            // by=1
            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;
            // by=2
            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;
            // by=3
            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 int ref_col_idx8(input int yb, input int xb);
        case ({yb[3:0], xb[3:0]})
            // yb=0
            8'd0:   return   0; 8'd1:   return   4; 8'd2:   return  16; 8'd3:   return  20;
            8'd4:   return  32; 8'd5:   return  36; 8'd6:   return  48; 8'd7:   return  52;
            8'd8:   return   2; 8'd9:   return   6; 8'd10:  return  18; 8'd11:  return  22;
            8'd12:  return  34; 8'd13:  return  38; 8'd14:  return  50; 8'd15:  return  54;
            // yb=1
            8'd16:  return   8; 8'd17:  return  12; 8'd18:  return  24; 8'd19:  return  28;
            8'd20:  return  40; 8'd21:  return  44; 8'd22:  return  56; 8'd23:  return  60;
            8'd24:  return  10; 8'd25:  return  14; 8'd26:  return  26; 8'd27:  return  30;
            8'd28:  return  42; 8'd29:  return  46; 8'd30:  return  58; 8'd31:  return  62;
            // yb=2
            8'd32:  return  33; 8'd33:  return  37; 8'd34:  return  49; 8'd35:  return  53;
            8'd36:  return   1; 8'd37:  return   5; 8'd38:  return  17; 8'd39:  return  21;
            8'd40:  return  35; 8'd41:  return  39; 8'd42:  return  51; 8'd43:  return  55;
            8'd44:  return   3; 8'd45:  return   7; 8'd46:  return  19; 8'd47:  return  23;
            // yb=3
            8'd48:  return  41; 8'd49:  return  45; 8'd50:  return  57; 8'd51:  return  61;
            8'd52:  return   9; 8'd53:  return  13; 8'd54:  return  25; 8'd55:  return  29;
            8'd56:  return  43; 8'd57:  return  47; 8'd58:  return  59; 8'd59:  return  63;
            8'd60:  return  11; 8'd61:  return  15; 8'd62:  return  27; 8'd63:  return  31;
            // yb=4
            8'd64:  return  96; 8'd65:  return 100; 8'd66:  return 112; 8'd67:  return 116;
            8'd68:  return  64; 8'd69:  return  68; 8'd70:  return  80; 8'd71:  return  84;
            8'd72:  return  98; 8'd73:  return 102; 8'd74:  return 114; 8'd75:  return 118;
            8'd76:  return  66; 8'd77:  return  70; 8'd78:  return  82; 8'd79:  return  86;
            // yb=5
            8'd80:  return 104; 8'd81:  return 108; 8'd82:  return 120; 8'd83:  return 124;
            8'd84:  return  72; 8'd85:  return  76; 8'd86:  return  88; 8'd87:  return  92;
            8'd88:  return 106; 8'd89:  return 110; 8'd90:  return 122; 8'd91:  return 126;
            8'd92:  return  74; 8'd93:  return  78; 8'd94:  return  90; 8'd95:  return  94;
            // yb=6
            8'd96:  return  65; 8'd97:  return  69; 8'd98:  return  81; 8'd99:  return  85;
            8'd100: return  97; 8'd101: return 101; 8'd102: return 113; 8'd103: return 117;
            8'd104: return  67; 8'd105: return  71; 8'd106: return  83; 8'd107: return  87;
            8'd108: return  99; 8'd109: return 103; 8'd110: return 115; 8'd111: return 119;
            // yb=7
            8'd112: return  73; 8'd113: return  77; 8'd114: return  89; 8'd115: return  93;
            8'd116: return 105; 8'd117: return 109; 8'd118: return 121; 8'd119: return 125;
            8'd120: return  75; 8'd121: return  79; 8'd122: return  91; 8'd123: return  95;
            8'd124: return 107; 8'd125: return 111; 8'd126: return 123; 8'd127: return 127;
            // yb=8
            8'd128: return 128; 8'd129: return 132; 8'd130: return 144; 8'd131: return 148;
            8'd132: return 160; 8'd133: return 164; 8'd134: return 176; 8'd135: return 180;
            8'd136: return 130; 8'd137: return 134; 8'd138: return 146; 8'd139: return 150;
            8'd140: return 162; 8'd141: return 166; 8'd142: return 178; 8'd143: return 182;
            // yb=9
            8'd144: return 136; 8'd145: return 140; 8'd146: return 152; 8'd147: return 156;
            8'd148: return 168; 8'd149: return 172; 8'd150: return 184; 8'd151: return 188;
            8'd152: return 138; 8'd153: return 142; 8'd154: return 154; 8'd155: return 158;
            8'd156: return 170; 8'd157: return 174; 8'd158: return 186; 8'd159: return 190;
            // yb=10
            8'd160: return 161; 8'd161: return 165; 8'd162: return 177; 8'd163: return 181;
            8'd164: return 129; 8'd165: return 133; 8'd166: return 145; 8'd167: return 149;
            8'd168: return 163; 8'd169: return 167; 8'd170: return 179; 8'd171: return 183;
            8'd172: return 131; 8'd173: return 135; 8'd174: return 147; 8'd175: return 151;
            // yb=11
            8'd176: return 169; 8'd177: return 173; 8'd178: return 185; 8'd179: return 189;
            8'd180: return 137; 8'd181: return 141; 8'd182: return 153; 8'd183: return 157;
            8'd184: return 171; 8'd185: return 175; 8'd186: return 187; 8'd187: return 191;
            8'd188: return 139; 8'd189: return 143; 8'd190: return 155; 8'd191: return 159;
            // yb=12
            8'd192: return 224; 8'd193: return 228; 8'd194: return 240; 8'd195: return 244;
            8'd196: return 192; 8'd197: return 196; 8'd198: return 208; 8'd199: return 212;
            8'd200: return 226; 8'd201: return 230; 8'd202: return 242; 8'd203: return 246;
            8'd204: return 194; 8'd205: return 198; 8'd206: return 210; 8'd207: return 214;
            // yb=13
            8'd208: return 232; 8'd209: return 236; 8'd210: return 248; 8'd211: return 252;
            8'd212: return 200; 8'd213: return 204; 8'd214: return 216; 8'd215: return 220;
            8'd216: return 234; 8'd217: return 238; 8'd218: return 250; 8'd219: return 254;
            8'd220: return 202; 8'd221: return 206; 8'd222: return 218; 8'd223: return 222;
            // yb=14
            8'd224: return 193; 8'd225: return 197; 8'd226: return 209; 8'd227: return 213;
            8'd228: return 225; 8'd229: return 229; 8'd230: return 241; 8'd231: return 245;
            8'd232: return 195; 8'd233: return 199; 8'd234: return 211; 8'd235: return 215;
            8'd236: return 227; 8'd237: return 231; 8'd238: return 243; 8'd239: return 247;
            // yb=15
            8'd240: return 201; 8'd241: return 205; 8'd242: return 217; 8'd243: return 221;
            8'd244: return 233; 8'd245: return 237; 8'd246: return 249; 8'd247: return 253;
            8'd248: return 203; 8'd249: return 207; 8'd250: return 219; 8'd251: return 223;
            8'd252: return 235; 8'd253: return 239; 8'd254: return 251; default: return 255;
        endcase
    endfunction

    function automatic logic [31:0] ref_addr8(
        input int fbp_v, input int fbw_v, input int x_v, input int y_v);
        int page_x, page_y, bw_pg_v, page_idx, page_base;
        int by, bx, blk_idx, xb, yb, byte_idx;
        int addr_v;
        page_x   = x_v / 128;
        page_y   = y_v / 64;
        bw_pg_v  = fbw_v / 2;
        page_idx = page_y * bw_pg_v + page_x;
        page_base = fbp_v * 2048 + page_idx * 8192;
        by  = (y_v % 64)  / 16;
        bx  = (x_v % 128) / 16;
        blk_idx = ref_block_idx8(by, bx);
        xb = x_v % 16;
        yb = y_v % 16;
        byte_idx = ref_col_idx8(yb, xb);
        addr_v = page_base + blk_idx * 256 + byte_idx;
        return addr_v[31:0];
    endfunction

    // -----------------------------------------------------------
    // PSMT8 unique-pixel index. `idx_at(x, y) = (y[2:0]<<4) |
    // x[3:0]` ∈ [0x00..0x7F]. Within each 16×8 region this is
    // unique per (xx, yy), so a misaddress shows up as wrong RGB.
    // PCRTC's PSMT8 grayscale fallback (clut_enable=0, Ch96)
    // returns R=G=B=idx, so the captured RGB is the index byte.
    // -----------------------------------------------------------
    function automatic logic [7:0] idx_at(input int x, input int y);
        return 8'(((y & 7) << 4) | (x & 15));
    endfunction

    // -----------------------------------------------------------
    // Helper: write a single byte to vram_stub at byte address
    // `addr` with value `b`. Uses byte-enable to leave neighbors
    // intact in the 32-bit word.
    // -----------------------------------------------------------
    task automatic vram_write_byte(input logic [31:0] addr,
                                   input logic [7:0] b);
        logic [3:0] be;
        logic [31:0] data;
        case (addr[1:0])
            2'b00: begin be = 4'b0001; data = {24'd0, b};                 end
            2'b01: begin be = 4'b0010; data = {16'd0, b, 8'd0};           end
            2'b10: begin be = 4'b0100; data = {8'd0,  b, 16'd0};          end
            2'b11: begin be = 4'b1000; data = {b,     24'd0};             end
            default: begin be = 4'b0000; data = 32'd0; end
        endcase
        @(negedge clk);
        vram_we    = 1'b1;
        vram_waddr = addr & ~32'd3;
        vram_wdata = data;
        vram_wbe   = be;
        @(posedge clk);
        @(negedge clk);
        vram_we    = 1'b0;
        vram_waddr = 32'd0;
        vram_wdata = 32'd0;
        vram_wbe   = 4'b0000;
    endtask

    initial begin
        rst_n         = 1'b0;
        pmode_q       = 64'd0;
        dispfb1_q     = 64'd0;
        display1_q    = 64'd0;
        vram_we       = 1'b0;
        vram_waddr    = 32'd0;
        vram_wdata    = 32'd0;
        vram_wbe      = 4'b0000;

        repeat (4) @(posedge clk);
        rst_n = 1'b1;
        repeat (4) @(posedge clk);

        // ===========================================================
        // PHASE 1 — origin (FBP=0, FBW=2, DBX=DBY=0).
        // ===========================================================
        for (int yy = 0; yy < PCRTC_V_ACTIVE; yy++) begin
            for (int xx = 0; xx < PCRTC_H_ACTIVE; xx++) begin
                logic [31:0] addr;
                addr = ref_addr8(0, 2, xx, yy);
                vram_write_byte(addr, idx_at(xx, yy));
            end
        end

        // DISPFB1: FBP=0, FBW=2, PSM=PSMT8 (=0x13).
        //   FBW [14:9]   = 2   → 0x0400
        //   PSM [19:15]  = 0x13 → 0x13<<15 = 0x9_8000
        dispfb1_q = 64'h0000_0000_0009_8400;

        // DISPLAY1: DX=0, DY=0, DW=15, DH=7, MAGH=MAGV=0.
        display1_q = 64'h0070_000F_0000_0000;

        // PMODE: EN1=1.
        pmode_q = 64'h0000_0000_0000_0001;

        repeat (40) @(posedge clk);
        capture_armed = 1'b1;
        repeat (500) @(posedge clk);

        // Phase 1 verify.
        for (int yy = 0; yy < PCRTC_V_ACTIVE; yy++) begin
            for (int xx = 0; xx < PCRTC_H_ACTIVE; xx++) begin
                logic [7:0] e;
                if (!cap_de[yy][xx]) begin
                    $error("[P1] (%0d,%0d) DE never asserted", xx, yy);
                    errors = errors + 1;
                end else begin
                    e = idx_at(xx, yy);
                    if (cap_r[yy][xx] !== e
                        || cap_g[yy][xx] !== e
                        || cap_b[yy][xx] !== e) begin
                        $error("[P1] (%0d,%0d) got R=%02x G=%02x B=%02x expected gray=%02x",
                               xx, yy,
                               cap_r[yy][xx], cap_g[yy][xx], cap_b[yy][xx], e);
                        errors = errors + 1;
                    end
                end
            end
        end

        // ===========================================================
        // PHASE 2 — non-origin (FBP=4, FBW=4, DBX=120, DBY=60).
        // ===========================================================

        // (a) Disable scanout briefly to avoid torn-frame capture.
        @(negedge clk);
        capture_armed = 1'b0;
        pmode_q       = 64'd0;
        @(posedge clk);

        // (b) Clear capture buffers.
        for (int yy = 0; yy < PCRTC_V_ACTIVE; yy++) begin
            for (int xx = 0; xx < PCRTC_H_ACTIVE; xx++) begin
                cap_r [yy][xx] = 8'd0;
                cap_g [yy][xx] = 8'd0;
                cap_b [yy][xx] = 8'd0;
                cap_de[yy][xx] = 1'b0;
            end
        end

        // (c) Preload VRAM at the SWIZZLED address for each pixel
        // of the Phase 2 effective-coord region.
        for (int yy = 0; yy < PCRTC_V_ACTIVE; yy++) begin
            for (int xx = 0; xx < PCRTC_H_ACTIVE; xx++) begin
                int          ex, ey;
                logic [31:0] addr;
                ex   = 120 + xx;
                ey   = 60  + yy;
                addr = ref_addr8(4, 4, ex, ey);
                vram_write_byte(addr, idx_at(xx, yy));
            end
        end

        // (d) Sentinel preload at byte 24696 — start of the linear
        //     ey=64 row (= FBP*2048 + 64*FBW*64 + 120 = 8192 + 16384 +
        //     120 = 24696), which IS sampled by the linear formula
        //     for captured (xx=0, yy=4). Outside all 4 swizzled
        //     write-set blocks. Sentinel 0xFF is unproducible by the
        //     Phase-2 idx pattern (idx ∈ [0x00..0x7F]) so a linear
        //     fall-through would surface as a per-pixel verify
        //     failure at that exact slot.
        vram_write_byte(32'd24696, 8'hFF);

        // (e) Re-drive priv regs for Phase 2.
        //     DISPFB1: FBP=4, FBW=4, PSM=PSMT8 (=0x13),
        //              DBX[42:32]=120, DBY[53:43]=60.
        //       FBP[8:0]   = 4
        //       FBW[14:9]  = 4   → 0x0800
        //       PSM[19:15] = 0x13 → 0x13<<15 = 0x9_8000
        //       DBX[42:32] = 120 → 120 << 32 = 0x0000_0078_0000_0000
        //       DBY[53:43] = 60  → 60 << 43 = 0x0001_E000_0000_0000
        dispfb1_q = 64'h0001_E078_0009_8804;
        // DISPLAY1: DX=DY=0, DW=15, DH=7, MAGH=MAGV=0.
        display1_q = 64'h0070_000F_0000_0000;
        pmode_q   = 64'h0000_0000_0000_0001;

        repeat (40) @(posedge clk);
        capture_armed = 1'b1;
        repeat (500) @(posedge clk);

        // (f) Phase 2 verify — each captured pixel matches the
        //     preloaded value at its EFFECTIVE coord (DBX+xx, DBY+yy).
        for (int yy = 0; yy < PCRTC_V_ACTIVE; yy++) begin
            for (int xx = 0; xx < PCRTC_H_ACTIVE; xx++) begin
                int         ex, ey;
                logic [7:0] e;
                ex = 120 + xx;
                ey = 60  + yy;
                if (!cap_de[yy][xx]) begin
                    $error("[P2] (%0d,%0d) DE never asserted", xx, yy);
                    errors = errors + 1;
                end else begin
                    e = idx_at(xx, yy);
                    if (cap_r[yy][xx] !== e
                        || cap_g[yy][xx] !== e
                        || cap_b[yy][xx] !== e) begin
                        $error("[P2] hcnt=%0d vcnt=%0d (ex=%0d,ey=%0d) got R=%02x G=%02x B=%02x expected gray=%02x",
                               xx, yy, ex, ey,
                               cap_r[yy][xx], cap_g[yy][xx], cap_b[yy][xx], e);
                        errors = errors + 1;
                    end
                end
            end
        end

        $display("[tb_gs_scanout_swizzle_psmt8] errors=%0d", errors);
        if (errors == 0) $display("[tb_gs_scanout_swizzle_psmt8] PASS");
        else             $display("[tb_gs_scanout_swizzle_psmt8] FAIL");
        $finish;
    end

    initial begin
        #1000000;
        $error("[tb_gs_scanout_swizzle_psmt8] timeout");
        $finish;
    end

endmodule : tb_gs_scanout_swizzle_psmt8
