// retroDE_ps2 — tb_gs_scanout_swizzle_psmt4 (Ch138)
//
// Read-side wiring of the Ch137 PSMT4 swizzle math primitive into
// PCRTC scanout. PSMT4 framebuffer reads now go through
// gs_swizzle_psmt4_stub when the new `PSMT4_SWIZZLE=1` parameter
// is set on gs_pcrtc_stub. Mirrors Ch120/Ch126/Ch132 read-side-
// first patterns but adds the PSMT4-specific twist: the swizzle
// module outputs both an absolute byte address AND a `nibble_hi`
// selector (PSMT4 = 4 bits/pixel = 2 pixels per byte, and the
// canonical PCSX2 column table reorders nibbles within a block,
// so pixel_index[0] is no longer the right selector under the
// swizzled layout).
//
// Topology (no gs_stub — keeps the read-side proof isolated;
// write-side gates Ch139/Ch140 are future chapters):
//
//   TB.preload  →  vram_stub  →  gs_pcrtc_stub#(PSMT4_SWIZZLE=1) → r/g/b
//
// CLUT is disabled (clut_enable=0) so PCRTC's PSMT4 grayscale
// fallback (Ch103) surfaces the nibble as `r=g=b={nibble, nibble}`
// at scanout. That makes the captured 8-bit RGB equal to
// `{nibble, nibble}` for whatever nibble the swizzled-byte +
// nibble_hi pair points at — so a misaddressed pixel surfaces
// as the wrong gray value.
//
// PHASE 1 — origin (FBP=0, FBW=2, DBX=DBY=0; FBW must be even
//           for PSMT4 — PCSX2 asserts (bw & 1) == 0).
//   16×4 region. Per-pixel nibble pattern uses the LOW 4 bits of a
//   well-mixed value:
//     `nibble_at(x, y) = ((y << 1) ^ x) & 4'h7`
//   ∈ [0..7]. The diversity-restricted-to-7 range keeps every
//   nibble distinct across `(y, x)` pairs in the 16×4 active area
//   when combined with the swizzled position:
//     - y=0: 0,1,2,3,4,5,6,7,0,1,2,3,4,5,6,7
//     - y=1: 2,3,0,1,6,7,4,5,2,3,0,1,6,7,4,5
//     - y=2: 4,5,6,7,0,1,2,3,4,5,6,7,0,1,2,3
//     - y=3: 6,7,4,5,2,3,0,1,6,7,4,5,2,3,0,1
//   The pattern is unique per (y, x) within each 4-px row group
//   but repeats every 8 px horizontally, which is fine for a
//   per-pixel verify because the verifier checks (xx, yy) →
//   nibble_at(xx, yy) directly.
//   x∈[0..15] is entirely block_x_in_page=0 (PSMT4 block is 32 px
//   wide); y∈[0..3] is entirely block_y_in_page=0 (16 px tall).
//   So the 16×4 image lives in block (0,0) of page (0,0) and
//   exercises within-block columnTable4 entries for yb=0..3,
//   xb=0..15 — covers the first 64 of the 512 within-block
//   nibble positions.
//
//   Strict separator inside Phase 1: at FBP=0/FBW=2, PSMT4
//   linear formula puts pixel (0, 1) at byte = (1*FBW*64 + 0) /
//   2 = 64. Under SWIZZLED, pixel (0, 1) is at
//   columnTable4[1][0]/2 = 16/2 = 8. So byte 64 is OUTSIDE the
//   Phase-1 swizzled write set when the gate is on. Pre-color
//   byte 64 with sentinel 0xCC (so a linear fall-through would
//   read nibbles 0xC and 0xC, surfacing as gray=0xCC, which is
//   unproducible by the Phase-1 nibble pattern (only nibbles
//   0..7 appear)).
//
// PHASE 2 — non-origin (FBP=4, FBW=4, DBX=120, DBY=126).
//   Effective coords range x∈[120..135], y∈[126..129]. This
//   makes every layer of the address chain non-zero AND crosses
//   PSMT4's own page boundaries:
//     • FBP=4 → byte_base = 8192.
//     • FBW=4 → bw_pg=2, page_index uses both x and y bits.
//     • page_x crosses 0→1 at effective_x=128 (PSMT4 page is 128
//       wide — same boundary as PSMT8).
//     • page_y crosses 0→1 at effective_y=128 (PSMT4 page is 128
//       tall — different from PSMT8's 64-tall pages).
//     • At page (0,0): block_x = (120..127 % 128)/32 = 3,
//                      block_y = (126..127 % 128)/16 = 7.
//                      blockTable4[7][3] = 31 → block_base
//                      8192+31*256 = 16128.
//     • At page (1,0): block_x = (128..135 % 128)/32 = 0,
//                      block_y = 7. blockTable4[7][0] = 21 →
//                      block_base 16384+21*256 = 21760.
//     • At page (0,1): block_x = 3, block_y = 0.
//                      blockTable4[0][3] = 10 → block_base
//                      24576+10*256 = 27136.
//     • At page (1,1): block_x = 0, block_y = 0.
//                      blockTable4[0][0] = 0 → block_base
//                      32768.
//   All 4 corner pages of FBP=4/FBW=4 visited, each with a
//   distinct blockTable4 lookup. A regression that tied any of
//   {dispfb_fbp, dbx, dby, FBW, block_x, block_y, page_index,
//   bw_pg=FBW/2, swizzle nibble_hi} to zero would NOT survive
//   Phase 2.
//
//   Strict P2 separator: linear formula puts pixel (120, 126) at
//   byte = FBP*2048 + (126*FBW*64 + 120) / 2 = 8192 + (32256 +
//   120)/2 = 8192 + 16188 = 24380. The Phase-2 swizzled write
//   set lives in 4 disjoint chunks at 16128+/256, 21760+/256,
//   27136+/256, 32768+/256 — byte 24380 falls between the
//   second and third chunks (outside any). Pre-color byte 24380
//   with sentinel 0xDD (gray=0xDD, unproducible by Phase 2's
//   pattern which uses nibbles 0..7).

`timescale 1ns/1ps

module tb_gs_scanout_swizzle_psmt4;

    localparam int PCRTC_H_ACTIVE = 16;
    localparam int PCRTC_V_ACTIVE = 4;

    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 (per-byte BE).
    // -----------------------------------------------------------
    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 Ch138 PSMT4_SWIZZLE gate ON (and the
    // Ch120/Ch126/Ch132 gates explicitly OFF — this TB exercises
    // only the PSMT4 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'b0),
        .PSMT4_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

    // -----------------------------------------------------------
    // PSMT4 swizzle reference (verbatim from PCSX2 Ch137 source-
    // lock). Mirrors gs_swizzle_psmt4_stub byte-exactly. Only
    // need a handful of helpers here for the preload + verify.
    // -----------------------------------------------------------
    function automatic int ref_block_idx4(input int by, input int bx);
        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

    function automatic int ref_col_idx4(input int yb, input int xb);
        case ({yb[3:0], xb[4:0]})
            // yb=0
            9'd0:   return   0; 9'd1:   return   8; 9'd2:   return  32; 9'd3:   return  40;
            9'd4:   return  64; 9'd5:   return  72; 9'd6:   return  96; 9'd7:   return 104;
            9'd8:   return   2; 9'd9:   return  10; 9'd10:  return  34; 9'd11:  return  42;
            9'd12:  return  66; 9'd13:  return  74; 9'd14:  return  98; 9'd15:  return 106;
            9'd16:  return   4; 9'd17:  return  12; 9'd18:  return  36; 9'd19:  return  44;
            9'd20:  return  68; 9'd21:  return  76; 9'd22:  return 100; 9'd23:  return 108;
            9'd24:  return   6; 9'd25:  return  14; 9'd26:  return  38; 9'd27:  return  46;
            9'd28:  return  70; 9'd29:  return  78; 9'd30:  return 102; 9'd31:  return 110;
            // yb=1
            9'd32:  return  16; 9'd33:  return  24; 9'd34:  return  48; 9'd35:  return  56;
            9'd36:  return  80; 9'd37:  return  88; 9'd38:  return 112; 9'd39:  return 120;
            9'd40:  return  18; 9'd41:  return  26; 9'd42:  return  50; 9'd43:  return  58;
            9'd44:  return  82; 9'd45:  return  90; 9'd46:  return 114; 9'd47:  return 122;
            9'd48:  return  20; 9'd49:  return  28; 9'd50:  return  52; 9'd51:  return  60;
            9'd52:  return  84; 9'd53:  return  92; 9'd54:  return 116; 9'd55:  return 124;
            9'd56:  return  22; 9'd57:  return  30; 9'd58:  return  54; 9'd59:  return  62;
            9'd60:  return  86; 9'd61:  return  94; 9'd62:  return 118; 9'd63:  return 126;
            // yb=2
            9'd64:  return  65; 9'd65:  return  73; 9'd66:  return  97; 9'd67:  return 105;
            9'd68:  return   1; 9'd69:  return   9; 9'd70:  return  33; 9'd71:  return  41;
            9'd72:  return  67; 9'd73:  return  75; 9'd74:  return  99; 9'd75:  return 107;
            9'd76:  return   3; 9'd77:  return  11; 9'd78:  return  35; 9'd79:  return  43;
            9'd80:  return  69; 9'd81:  return  77; 9'd82:  return 101; 9'd83:  return 109;
            9'd84:  return   5; 9'd85:  return  13; 9'd86:  return  37; 9'd87:  return  45;
            9'd88:  return  71; 9'd89:  return  79; 9'd90:  return 103; 9'd91:  return 111;
            9'd92:  return   7; 9'd93:  return  15; 9'd94:  return  39; 9'd95:  return  47;
            // yb=3
            9'd96:  return  81; 9'd97:  return  89; 9'd98:  return 113; 9'd99:  return 121;
            9'd100: return  17; 9'd101: return  25; 9'd102: return  49; 9'd103: return  57;
            9'd104: return  83; 9'd105: return  91; 9'd106: return 115; 9'd107: return 123;
            9'd108: return  19; 9'd109: return  27; 9'd110: return  51; 9'd111: return  59;
            9'd112: return  85; 9'd113: return  93; 9'd114: return 117; 9'd115: return 125;
            9'd116: return  21; 9'd117: return  29; 9'd118: return  53; 9'd119: return  61;
            9'd120: return  87; 9'd121: return  95; 9'd122: return 119; 9'd123: return 127;
            9'd124: return  23; 9'd125: return  31; 9'd126: return  55; 9'd127: return  63;
            // yb=7 (Phase 2's effective_y∈[126..127] → y%16∈[14..15], block_y=7,
            //       so within-block yb is 14..15. Phase 2 also uses yb=0..1 for
            //       y∈[128..129] (page_y=1, block_y=0, yb=128%16=0 / 129%16=1).
            //       Since Phase 2's yb values are 14, 15, 0, 1 — yb=14, 15 entries
            //       are needed. The Phase 1 lookup uses yb=0..3 only. Provide
            //       yb=14..15 here (yb=4..13 omitted as they're unused by either
            //       phase — would be a SystemVerilog default fall-through if hit).
            // yb=14: 385 393 417 425 449 457 481 489 387 395 419 427 451 459 483 491
            //        389 397 421 429 453 461 485 493 391 399 423 431 455 463 487 495
            9'd448: return 385; 9'd449: return 393; 9'd450: return 417; 9'd451: return 425;
            9'd452: return 449; 9'd453: return 457; 9'd454: return 481; 9'd455: return 489;
            9'd456: return 387; 9'd457: return 395; 9'd458: return 419; 9'd459: return 427;
            9'd460: return 451; 9'd461: return 459; 9'd462: return 483; 9'd463: return 491;
            9'd464: return 389; 9'd465: return 397; 9'd466: return 421; 9'd467: return 429;
            9'd468: return 453; 9'd469: return 461; 9'd470: return 485; 9'd471: return 493;
            9'd472: return 391; 9'd473: return 399; 9'd474: return 423; 9'd475: return 431;
            9'd476: return 455; 9'd477: return 463; 9'd478: return 487; 9'd479: return 495;
            // yb=15: 401 409 433 441 465 473 497 505 403 411 435 443 467 475 499 507
            //        405 413 437 445 469 477 501 509 407 415 439 447 471 479 503 511
            9'd480: return 401; 9'd481: return 409; 9'd482: return 433; 9'd483: return 441;
            9'd484: return 465; 9'd485: return 473; 9'd486: return 497; 9'd487: return 505;
            9'd488: return 403; 9'd489: return 411; 9'd490: return 435; 9'd491: return 443;
            9'd492: return 467; 9'd493: return 475; 9'd494: return 499; 9'd495: return 507;
            9'd496: return 405; 9'd497: return 413; 9'd498: return 437; 9'd499: return 445;
            9'd500: return 469; 9'd501: return 477; 9'd502: return 501; 9'd503: return 509;
            9'd504: return 407; 9'd505: return 415; 9'd506: return 439; 9'd507: return 447;
            9'd508: return 471; 9'd509: return 479; 9'd510: return 503; default: return 511;
        endcase
    endfunction

    typedef struct packed {
        logic [31:0] addr;
        logic        nibble_hi;
    } psmt4_pos_t;

    function automatic psmt4_pos_t ref_pos_psmt4(
        input int fbp_v, input int fbw_v, input int x_v, input int y_v);
        psmt4_pos_t r;
        int page_x, page_y, bw_pg_v, page_idx, page_base;
        int by, bx, blk_idx, xb, yb, nibble_idx;
        int addr_v;
        page_x   = x_v / 128;
        page_y   = y_v / 128;
        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 % 128) / 16;
        bx  = (x_v % 128) / 32;
        blk_idx = ref_block_idx4(by, bx);
        xb = x_v % 32;
        yb = y_v % 16;
        nibble_idx = ref_col_idx4(yb, xb);
        addr_v = page_base + blk_idx * 256 + (nibble_idx >> 1);
        r.addr      = addr_v[31:0];
        r.nibble_hi = 1'(nibble_idx & 1);
        return r;
    endfunction

    // -----------------------------------------------------------
    // Per-pixel nibble pattern.
    //   nibble_at(x, y) = ((y << 1) ^ x) & 4'h7  ∈ [0..7]
    // -----------------------------------------------------------
    function automatic logic [3:0] nibble_at(input int x, input int y);
        return 4'(((y << 1) ^ x) & 4'h7);
    endfunction

    // PCRTC PSMT4 grayscale fallback: r=g=b={nibble, nibble}.
    function automatic logic [7:0] gray_at(input int x, input int y);
        logic [3:0] n;
        n = nibble_at(x, y);
        return {n, n};
    endfunction

    // -----------------------------------------------------------
    // VRAM byte preload helper. PSMT4 packs 2 pixels per byte —
    // even pixels in the low nibble, odd pixels in the high
    // nibble UNDER LINEAR. UNDER SWIZZLED, the swizzle module's
    // `nibble_hi` output picks high vs low. We preload by writing
    // each (low_nibble, high_nibble) byte at its swizzled address.
    // Since adjacent swizzled-pixel pairs may not share a byte
    // (unlike linear), we accumulate per-byte nibble assignments
    // by reading-modifying-writing through a temporary VRAM
    // shadow.
    //
    // To avoid the read-modify-write complication, we just write
    // each pixel's nibble individually with the right BE + value.
    // For nibble_hi=0 (low nibble of the byte at addr): write
    // {28'd0, nibble} with be selecting the byte and the low
    // nibble of that byte. For nibble_hi=1: same but high nibble.
    // We use a TB-side shadow array to accumulate the byte's two
    // nibbles before issuing a single byte write.
    // -----------------------------------------------------------
    logic [7:0] byte_shadow [0:65535];
    initial begin
        for (int i = 0; i < 65536; i++) byte_shadow[i] = 8'd0;
    end

    task automatic accumulate_nibble(input logic [31:0] addr,
                                     input logic        hi,
                                     input logic [3:0]  nibble);
        if (hi)
            byte_shadow[addr[15:0]] = {nibble, byte_shadow[addr[15:0]][3:0]};
        else
            byte_shadow[addr[15:0]] = {byte_shadow[addr[15:0]][7:4], nibble};
    endtask

    task automatic flush_byte(input logic [31:0] addr);
        logic [3:0]  be;
        logic [31:0] data;
        logic [7:0]  byte_v;
        byte_v = byte_shadow[addr[15:0]];
        case (addr[1:0])
            2'b00: begin be = 4'b0001; data = {24'd0, byte_v};                 end
            2'b01: begin be = 4'b0010; data = {16'd0, byte_v, 8'd0};           end
            2'b10: begin be = 4'b0100; data = {8'd0,  byte_v, 16'd0};          end
            2'b11: begin be = 4'b1000; data = {byte_v,        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

    // Sentinel byte writer (writes a full byte at given addr).
    task automatic write_sentinel(input logic [31:0] addr,
                                  input logic [7:0]  b);
        byte_shadow[addr[15:0]] = b;
        flush_byte(addr);
    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).
        // ===========================================================
        // Accumulate nibbles into byte_shadow at swizzled addresses.
        for (int yy = 0; yy < PCRTC_V_ACTIVE; yy++) begin
            for (int xx = 0; xx < PCRTC_H_ACTIVE; xx++) begin
                psmt4_pos_t pos;
                pos = ref_pos_psmt4(0, 2, xx, yy);
                accumulate_nibble(pos.addr, pos.nibble_hi, nibble_at(xx, yy));
            end
        end
        // Flush each unique byte. The 16×4 region at FBP=0/FBW=2
        // maps to 64 nibbles in block (0,0); pairs of nibbles
        // share bytes, so 32 byte writes total (or fewer where
        // accumulate_nibble combined two nibbles into a byte).
        // Simpler: flush every potentially-touched byte in
        // [0..127] (block (0,0) extent).
        for (int b = 0; b < 128; b++) begin
            if (byte_shadow[b] !== 8'd0) flush_byte(32'(b));
        end

        // Sentinel preload at byte 64 (linear y=1 row start at
        // FBW=2 → byte 64). Unproducible in Phase 1 since pattern
        // uses nibbles ∈ [0..7]; sentinel 0xCC → both nibbles 0xC.
        write_sentinel(32'd64, 8'hCC);

        // DISPFB1: FBP=0, FBW=2, PSM=PSMT4 (=0x14).
        //   FBW [14:9]   = 2   → 0x0400
        //   PSM [19:15]  = 0x14 → 0x14<<15 = 0xA_0000
        dispfb1_q = 64'h0000_0000_000A_0400;
        // DISPLAY1: DX=0, DY=0, DW=15, DH=3, MAGH=MAGV=0.
        display1_q = 64'h0030_000F_0000_0000;
        // PMODE.EN1=1.
        pmode_q = 64'h0000_0000_0000_0001;

        repeat (40) @(posedge clk);
        capture_armed = 1'b1;
        repeat (300) @(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 = gray_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=126).
        // ===========================================================
        // (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 at swizzled addresses for the Phase-2 effective
        //     coord region. Use SAME nibble pattern (nibble_at) — the
        //     verify side computes expected directly from (xx, yy)
        //     so the two stay in lockstep.
        for (int yy = 0; yy < PCRTC_V_ACTIVE; yy++) begin
            for (int xx = 0; xx < PCRTC_H_ACTIVE; xx++) begin
                int ex, ey;
                psmt4_pos_t pos;
                ex   = 120 + xx;
                ey   = 126 + yy;
                pos  = ref_pos_psmt4(4, 4, ex, ey);
                accumulate_nibble(pos.addr, pos.nibble_hi, nibble_at(xx, yy));
            end
        end
        // Flush bytes touched by Phase 2. Sweep the byte_shadow
        // for ALL non-zero bytes above the Phase-1 region to
        // avoid hard-coding chunk ranges that depend on
        // blockTable4 lookups (one such miscalculation cost an
        // hour during initial bring-up). Phase 1 wrote bytes
        // [0..127]; Phase 2's chunks are all > 8192. We scan
        // [256..65535] — bytes 128..255 are guaranteed-zero
        // gaps from Phase 1's strict-separator design.
        for (int b = 256; b < 65536; b++) begin
            if (byte_shadow[b] !== 8'd0) flush_byte(32'(b));
        end

        // (d) Sentinel at byte 24380 (linear-only — outside all 4
        //     swizzled chunks). gray=0xDD unproducible by Phase 2's
        //     [0..7]-nibble pattern.
        write_sentinel(32'd24380, 8'hDD);

        // (e) Re-drive priv regs for Phase 2.
        //     DISPFB1: FBP=4, FBW=4, PSM=PSMT4 (=0x14),
        //              DBX[42:32]=120, DBY[53:43]=126.
        //       FBP[8:0]   = 4
        //       FBW[14:9]  = 4   → 0x0800
        //       PSM[19:15] = 0x14 → 0xA_0000
        //       DBX[42:32] = 120 → 120 << 32 = 0x0000_0078_0000_0000
        //       DBY[53:43] = 126 → 126 << 43 = 0x0003_F000_0000_0000
        dispfb1_q = 64'h0003_F078_000A_0804;
        display1_q = 64'h0030_000F_0000_0000;
        pmode_q   = 64'h0000_0000_0000_0001;

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

        // Phase 2 verify — each captured pixel matches preloaded
        // nibble at its EFFECTIVE coord (DBX+xx, DBY+yy). Since
        // we used `nibble_at(xx, yy)` for both source and verify,
        // the gray value is the same as Phase 1's per-(xx, 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 = 126 + yy;
                if (!cap_de[yy][xx]) begin
                    $error("[P2] (%0d,%0d) DE never asserted", xx, yy);
                    errors = errors + 1;
                end else begin
                    e = gray_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_psmt4] errors=%0d", errors);
        if (errors == 0) $display("[tb_gs_scanout_swizzle_psmt4] PASS");
        else             $display("[tb_gs_scanout_swizzle_psmt4] FAIL");
        $finish;
    end

    initial begin
        #5000000;
        $error("[tb_gs_scanout_swizzle_psmt4] timeout");
        $finish;
    end

endmodule : tb_gs_scanout_swizzle_psmt4
