// retroDE_ps2 — tb_gs_raster_swizzle_psmct16 (Ch128)
//
// Third write-side wiring of the Ch125 PSMCT16 swizzle math —
// this time into gs_stub's raster emit channel (the per-pixel
// store path that backs SPRITE/TRI primitives). With the new
// `PSMCT16_SWIZZLE=1` parameter on gs_stub, the per-pixel VRAM
// byte address goes through `gs_swizzle_psmct16_stub` instead of
// the legacy linear formula `(FBW*64*y + x) * 2`. PSMCT32 is
// gated by its own `PSMCT32_SWIZZLE` parameter (Ch122); PSMT8 /
// PSMT4 raster emits always stay on the linear path. Default
// off → every existing PSMCT16 raster TB (Ch95 etc.) is
// unaffected. Mirrors Ch122's PSMCT32 raster wiring shape.
//
// Topology — keeps the proof end-to-end (mirrors Ch127's
// cross-check pattern):
//
//   TB drives PRIM/FRAME_1/RGBAQ/XYZ2 via gif_reg_wr_*
//        →  gs_stub#(PSMCT16_SWIZZLE=1)         (raster emit)
//        →  vram_stub                            (PSMCT16 store)
//        →  gs_pcrtc_stub#(PSMCT16_SWIZZLE=1)   (swizzled scanout)
//        →  r/g/b
//
// Phase 1 (write-side lock): drive a 16×4 SPRITE that fills the
// rect (0,0)..(15,3) — 64 PSMCT16 pixels packed at 2 bytes/pixel.
// PSMCT16 blocks are 16 px wide × 8 px tall, so x∈[0..15] is
// entirely block_x_in_page=0 and y∈[0..3] is entirely block_y=0;
// the rect lives in block (0,0) of page (0,0). However, the
// within-block columnTable16 distributes those 64 halfwords
// across all 16 xb columns × 4 yb rows of column-table rows
// 0..3, exercising every entry of the column table for the
// upper half of the block. Single per-primitive RGBAQ color
// (R5=0x15, G5=0x0A, B5=0x18) is enough to lock the address
// layout: every halfword the swizzle says should be written is
// verified to hold the RGB5A1 word; bytes outside the swizzled
// write set stay 0 — proves the gate is live since a fall-
// through to linear addressing (PSMCT16 stride = FBW*64*2 = 128)
// would put the sprite halfword at byte 128 (linear y=1 row
// start), which is strictly outside the swizzled set [0..126].
//
// Phase 2 (end-to-end agreement): without redrawing, enable the
// Ch126 swizzled-pcrtc path, capture the full 16×4 frame, assert
// every visible pixel reads back the SPRITE's RGB after PCRTC's
// 5→8 bit-replicate. Both gs_stub (Ch128) and gs_pcrtc_stub
// (Ch126) instantiate the same gs_swizzle_psmct16_stub; a
// successful capture proves the two integrations agree at byte
// level — what raster wrote at swizzled addresses comes out on
// r/g/b at the same (x, y).
//
// Phase 3 (non-origin raster): re-arm gs_stub with FRAME_1 set
// to FBP=4, FBW=2 and draw an 8×4 SPRITE at (60, 4)..(67, 7).
// This pins three contracts Phase 1 can't:
//   (a) `ras_fbp` reaches the swizzle's `fbp` input (FBP=0 in
//       Phase 1 would have masked a tied-zero regression).
//   (b) `ras_fbw` reaches the swizzle's `fbw` input (FBW=1 would
//       have masked a tied-one regression).
//   (c) The swizzle gets the FULL absolute pixel coords (s2_x_q,
//       s2_y_q) rather than bbox-local — Phase 1's sprite
//       started at (0,0) so absolute and local were equal there.
//       The Phase-3 rect crosses the PAGE-x boundary at x=64
//       (page (0,0) for x∈[60..63], page (1,0) for x∈[64..67])
//       so `page_index` changes mid-row. Within-block column-
//       table coords (xb=12..3, yb=4..7) also cover a different
//       region of columnTable16 than Phase 1 (which covered
//       yb=0..3). Strict linear-vs-swizzled separator: under
//       linear the (60, 4) pixel lands at byte
//         FBP*2048 + 4*FBW*64*2 + 60*2
//         = 8192 + 1024 + 120
//         = 9336.
//       Under swizzled the Phase-3 write set lives in pages
//       (0,0) and (1,0) of FBP=4, in specific blocks within
//       those pages — byte 9336 stays 0 because that linear-
//       only address is strictly outside the swizzled set.

`timescale 1ns/1ps

module tb_gs_raster_swizzle_psmct16;

    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;

    // ------------------------------------------------------------
    // gs_stub I/O
    // ------------------------------------------------------------
    logic        gif_reg_wr_en;
    logic [7:0]  gif_reg_num;
    logic [63:0] gif_reg_data;

    logic        priv_reg_wr_en;
    logic [15:0] priv_reg_wr_addr;
    logic [63:0] priv_reg_wr_data;

    logic [7:0]          bg_r, bg_g, bg_b;
    logic [63:0]         pmode_q, dispfb1_q, display1_q;
    logic [63:0]         prim_q, rgbaq_q, xyz2_q, xyzf2_q, frame_1_q, zbuf_1_q;
    logic [63:0]         tex0_1_q;
    logic [13:0]         tex0_1_cbp_q;
    logic [3:0]          tex0_1_cpsm_q;
    logic                tex0_1_csm_q;
    logic [4:0]          tex0_1_csa_q;
    logic [2:0]          tex0_1_cld_q;
    logic                tex0_1_wr_q;
    logic [63:0]         bitbltbuf_q, trxpos_q, trxreg_q, trxdir_q;
    logic                trxdir_wr_q;
    logic                prim_complete;
    logic [31:0]         prim_complete_count;
    logic [63:0]         prim_v0_q, prim_v1_q, prim_v2_q;
    logic [63:0]         prim_color_q;
    logic [63:0]         prim_color_v0_q, prim_color_v1_q, prim_color_v2_q;
    trace_pkg::vertex_t  prim_v0_decoded_q, prim_v1_decoded_q, prim_v2_decoded_q;
    trace_pkg::color_t   prim_v0_color_decoded_q, prim_v1_color_decoded_q, prim_v2_color_decoded_q;
    logic                pixel_emit;
    logic [31:0]         pixel_emit_count;
    logic [11:0]         pixel_x_q, pixel_y_q;
    logic [63:0]         pixel_color_q;
    logic [8:0]          pixel_fbp_q;
    logic [5:0]          pixel_fbw_q, pixel_psm_q;
    logic [31:0]         pixel_fb_addr_q;
    logic                raster_pixel_emit;
    logic [31:0]         raster_pixel_emit_count;
    logic [11:0]         raster_pixel_x_q, raster_pixel_y_q;
    logic [63:0]         raster_pixel_color_q;
    logic [31:0]         raster_pixel_fb_addr_q;
    logic [3:0]          raster_pixel_be_q;
    logic [31:0]         raster_pixel_mask_q;
    logic [5:0]          raster_pixel_psm_q;
    logic                raster_active;
    logic                raster_overflow;
    logic                raster_degenerate;
    logic                gs_ev_valid;
    trace_pkg::subsys_e  gs_ev_subsys;
    trace_pkg::event_e   gs_ev_event;
    logic [63:0]         gs_ev_arg0, gs_ev_arg1, gs_ev_arg2, gs_ev_arg3;
    logic [31:0]         gs_ev_flags;

    gs_stub #(
        .PSMCT32_SWIZZLE(1'b0),
        .PSMCT16_SWIZZLE(1'b1)
    ) u_gs (
        .clk(clk), .rst_n(rst_n),
        .reg_wr_en  (priv_reg_wr_en),
        .reg_wr_addr(priv_reg_wr_addr),
        .reg_wr_data(priv_reg_wr_data),
        .gif_reg_wr_en(gif_reg_wr_en),
        .gif_reg_num(gif_reg_num),
        .gif_reg_data(gif_reg_data),
        .bg_r(bg_r), .bg_g(bg_g), .bg_b(bg_b),
        .pmode_q(pmode_q), .dispfb1_q(dispfb1_q), .display1_q(display1_q),
        .prim_q(prim_q), .rgbaq_q(rgbaq_q),
        .xyz2_q(xyz2_q), .xyzf2_q(xyzf2_q),
        .frame_1_q(frame_1_q), .zbuf_1_q(zbuf_1_q),
        .tex0_1_q(tex0_1_q),
        .tex0_1_cbp_q(tex0_1_cbp_q),
        .tex0_1_cpsm_q(tex0_1_cpsm_q),
        .tex0_1_csm_q(tex0_1_csm_q),
        .tex0_1_csa_q(tex0_1_csa_q),
        .tex0_1_cld_q(tex0_1_cld_q),
        .tex0_1_wr_q(tex0_1_wr_q),
        .bitbltbuf_q(bitbltbuf_q),
        .trxpos_q(trxpos_q),
        .trxreg_q(trxreg_q),
        .trxdir_q(trxdir_q),
        .trxdir_wr_q(trxdir_wr_q),
        .prim_complete(prim_complete),
        .prim_complete_count(prim_complete_count),
        .prim_v0_q(prim_v0_q), .prim_v1_q(prim_v1_q), .prim_v2_q(prim_v2_q),
        .prim_color_q(prim_color_q),
        .prim_color_v0_q(prim_color_v0_q),
        .prim_color_v1_q(prim_color_v1_q),
        .prim_color_v2_q(prim_color_v2_q),
        .prim_v0_decoded_q(prim_v0_decoded_q),
        .prim_v1_decoded_q(prim_v1_decoded_q),
        .prim_v2_decoded_q(prim_v2_decoded_q),
        .prim_v0_color_decoded_q(prim_v0_color_decoded_q),
        .prim_v1_color_decoded_q(prim_v1_color_decoded_q),
        .prim_v2_color_decoded_q(prim_v2_color_decoded_q),
        .pixel_emit(pixel_emit),
        .pixel_emit_count(pixel_emit_count),
        .pixel_x_q(pixel_x_q), .pixel_y_q(pixel_y_q),
        .pixel_color_q(pixel_color_q),
        .pixel_fbp_q(pixel_fbp_q),
        .pixel_fbw_q(pixel_fbw_q),
        .pixel_psm_q(pixel_psm_q),
        .pixel_fb_addr_q(pixel_fb_addr_q),
        .raster_pixel_emit(raster_pixel_emit),
        .raster_pixel_emit_count(raster_pixel_emit_count),
        .raster_pixel_x_q(raster_pixel_x_q),
        .raster_pixel_y_q(raster_pixel_y_q),
        .raster_pixel_color_q(raster_pixel_color_q),
        .raster_pixel_fb_addr_q(raster_pixel_fb_addr_q),
        .raster_pixel_be_q(raster_pixel_be_q),
        .raster_pixel_mask_q(raster_pixel_mask_q),
        .raster_pixel_psm_q(raster_pixel_psm_q),
        .raster_active(raster_active),
        .raster_overflow(raster_overflow),
        .raster_degenerate(raster_degenerate),
        .ev_valid(gs_ev_valid),
        .ev_subsys(gs_ev_subsys),
        .ev_event(gs_ev_event),
        .ev_arg0(gs_ev_arg0), .ev_arg1(gs_ev_arg1),
        .ev_arg2(gs_ev_arg2), .ev_arg3(gs_ev_arg3),
        .ev_flags(gs_ev_flags)
    );

    // ------------------------------------------------------------
    // vram_stub. Two readers:
    //   read  → driven by pcrtc (Phase 2 scanout)
    //   read2 → driven by TB    (Phase 1 byte readback)
    // ------------------------------------------------------------
    logic [31:0] vram_read_addr;
    logic [31:0] vram_read_data;
    logic [31:0] vram_read2_addr;
    logic [31:0] vram_read2_data;

    vram_stub #(.BYTES(65536)) u_vram (
        .clk(clk), .rst_n(rst_n),
        .write_en  (raster_pixel_emit),
        .write_addr(raster_pixel_fb_addr_q),
        .write_data(raster_pixel_color_q[31:0]),
        .write_be  (raster_pixel_be_q),
        .write_mask(raster_pixel_mask_q),
        .read_addr (vram_read_addr),
        .read_data (vram_read_data),
        .read2_addr(vram_read2_addr),
        .read2_data(vram_read2_data)
    );

    // ------------------------------------------------------------
    // gs_pcrtc_stub with the Ch126 PSMCT16_SWIZZLE gate ON (and
    // the Ch120 PSMCT32_SWIZZLE gate explicitly OFF — this TB
    // exercises only the CT16 scanout path).
    // ------------------------------------------------------------
    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;

    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'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_read_addr),
        .vram_read_data(vram_read_data),
        .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 (mirrors Ch120 / Ch121 pattern).
    // ------------------------------------------------------------
    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

    // ------------------------------------------------------------
    // GIF / priv reg drive helpers.
    // ------------------------------------------------------------
    task automatic step_drive(input logic        wr_en,
                              input logic [7:0]  num,
                              input logic [63:0] data);
        @(negedge clk);
        gif_reg_wr_en = wr_en;
        gif_reg_num   = num;
        gif_reg_data  = data;
        @(posedge clk);
    endtask

    task automatic drive_reg(input logic [7:0] num, input logic [63:0] data);
        step_drive(1'b1, num, data);
    endtask

    task automatic drive_idle();
        step_drive(1'b0, 8'd0, 64'd0);
    endtask

    task automatic drive_priv(input logic [15:0] addr,
                              input logic [63:0] data);
        @(negedge clk);
        priv_reg_wr_en   = 1'b1;
        priv_reg_wr_addr = addr;
        priv_reg_wr_data = data;
        @(posedge clk);
        @(negedge clk);
        priv_reg_wr_en   = 1'b0;
        priv_reg_wr_addr = 16'd0;
        priv_reg_wr_data = 64'd0;
    endtask

    function automatic logic [63:0] xyz2_data(input logic [11:0] x_int,
                                              input logic [11:0] y_int);
        return {32'd0, y_int, 4'd0, x_int, 4'd0};
    endfunction

    // ------------------------------------------------------------
    // PSMCT16 swizzle reference — block + column tables verbatim
    // from PCSX2 (Ch125 source-locked). Mirrors Ch125/126/127.
    // ------------------------------------------------------------
    function automatic int ref_block_idx16(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_idx16(input int yb, input int xb);
        case ({yb[2:0], xb[3:0]})
            // yb=0
            7'd0:   return 0;   7'd1:   return 2;   7'd2:   return 8;   7'd3:   return 10;
            7'd4:   return 16;  7'd5:   return 18;  7'd6:   return 24;  7'd7:   return 26;
            7'd8:   return 1;   7'd9:   return 3;   7'd10:  return 9;   7'd11:  return 11;
            7'd12:  return 17;  7'd13:  return 19;  7'd14:  return 25;  7'd15:  return 27;
            // yb=1
            7'd16:  return 4;   7'd17:  return 6;   7'd18:  return 12;  7'd19:  return 14;
            7'd20:  return 20;  7'd21:  return 22;  7'd22:  return 28;  7'd23:  return 30;
            7'd24:  return 5;   7'd25:  return 7;   7'd26:  return 13;  7'd27:  return 15;
            7'd28:  return 21;  7'd29:  return 23;  7'd30:  return 29;  7'd31:  return 31;
            // yb=2
            7'd32:  return 32;  7'd33:  return 34;  7'd34:  return 40;  7'd35:  return 42;
            7'd36:  return 48;  7'd37:  return 50;  7'd38:  return 56;  7'd39:  return 58;
            7'd40:  return 33;  7'd41:  return 35;  7'd42:  return 41;  7'd43:  return 43;
            7'd44:  return 49;  7'd45:  return 51;  7'd46:  return 57;  7'd47:  return 59;
            // yb=3
            7'd48:  return 36;  7'd49:  return 38;  7'd50:  return 44;  7'd51:  return 46;
            7'd52:  return 52;  7'd53:  return 54;  7'd54:  return 60;  7'd55:  return 62;
            7'd56:  return 37;  7'd57:  return 39;  7'd58:  return 45;  7'd59:  return 47;
            7'd60:  return 53;  7'd61:  return 55;  7'd62:  return 61;  7'd63:  return 63;
            // yb=4
            7'd64:  return 64;  7'd65:  return 66;  7'd66:  return 72;  7'd67:  return 74;
            7'd68:  return 80;  7'd69:  return 82;  7'd70:  return 88;  7'd71:  return 90;
            7'd72:  return 65;  7'd73:  return 67;  7'd74:  return 73;  7'd75:  return 75;
            7'd76:  return 81;  7'd77:  return 83;  7'd78:  return 89;  7'd79:  return 91;
            // yb=5
            7'd80:  return 68;  7'd81:  return 70;  7'd82:  return 76;  7'd83:  return 78;
            7'd84:  return 84;  7'd85:  return 86;  7'd86:  return 92;  7'd87:  return 94;
            7'd88:  return 69;  7'd89:  return 71;  7'd90:  return 77;  7'd91:  return 79;
            7'd92:  return 85;  7'd93:  return 87;  7'd94:  return 93;  7'd95:  return 95;
            // yb=6
            7'd96:  return 96;  7'd97:  return 98;  7'd98:  return 104; 7'd99:  return 106;
            7'd100: return 112; 7'd101: return 114; 7'd102: return 120; 7'd103: return 122;
            7'd104: return 97;  7'd105: return 99;  7'd106: return 105; 7'd107: return 107;
            7'd108: return 113; 7'd109: return 115; 7'd110: return 121; 7'd111: return 123;
            // yb=7
            7'd112: return 100; 7'd113: return 102; 7'd114: return 108; 7'd115: return 110;
            7'd116: return 116; 7'd117: return 118; 7'd118: return 124; 7'd119: return 126;
            7'd120: return 101; 7'd121: return 103; 7'd122: return 109; 7'd123: return 111;
            7'd124: return 117; 7'd125: return 119; 7'd126: return 125; default: return 127;
        endcase
    endfunction

    function automatic logic [31:0] ref_addr16(
        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, hw_idx;
        int addr_v;
        page_x   = x_v / 64;
        page_y   = y_v / 64;
        page_idx = page_y * fbw_v + page_x;
        page_base = fbp_v * 2048 + page_idx * 8192;
        by  = (y_v % 64) / 8;
        bx  = (x_v % 64) / 16;
        blk_idx = ref_block_idx16(by, bx);
        xb = x_v % 16;
        yb = y_v % 8;
        hw_idx = ref_col_idx16(yb, xb);
        addr_v = page_base + blk_idx * 256 + hw_idx * 2;
        return addr_v[31:0];
    endfunction

    // ------------------------------------------------------------
    // Constants — single-color 16×4 SPRITE in PSMCT16.
    //
    //  RGBAQ field layout (low 32 bits): R[7:0], G[15:8],
    //  B[23:16], A[31:24]. The Ch95 raster channel packs
    //  ABGR → RGB5A1 by taking the high 5 bits of each channel:
    //  R5=R[7:3], G5=G[7:3], B5=B[7:3], A1=A[7]. Halfword stored
    //  in vram_stub: {A1, B5, G5, R5} (little-endian halfword).
    //
    //  Phase 1 color: R=0xAA, G=0x50, B=0xC0, A=0x00.
    //    R5 = 0xAA[7:3] = 0b10101 = 0x15
    //    G5 = 0x50[7:3] = 0b01010 = 0x0A
    //    B5 = 0xC0[7:3] = 0b11000 = 0x18
    //    A1 = 0
    //    halfword = {0, 0x18, 0x0A, 0x15} = 0x6155
    //  PCRTC 5→8 bit-replicate at scanout (`{ch5, ch5[4:2]}`):
    //    R8 = {0x15, 0b101} = 0xAD
    //    G8 = {0x0A, 0b010} = 0x52
    //    B8 = {0x18, 0b110} = 0xC6
    // ------------------------------------------------------------
    localparam logic [7:0]  R_PRIM     = 8'h00;
    localparam logic [7:0]  R_RGBAQ    = 8'h01;
    localparam logic [7:0]  R_XYZ2     = 8'h05;
    localparam logic [7:0]  R_FRAME_1  = 8'h4C;

    localparam logic [63:0] PRIM_SPRITE = 64'd6;
    // FRAME_1: FBP[8:0]=0, FBW[5:0]=1 (at bit 16), PSM=PSMCT16(=0x02 at bit 24).
    //   FBW=1 → bits[21:16]=0x01 → 0x0001_0000.
    //   PSM=2 → bits[29:24]=0x02 → 0x0200_0000.
    localparam logic [63:0] FRAME_1_VAL = 64'h0000_0000_0201_0000;
    // RGBAQ: R=0xAA, G=0x50, B=0xC0, A=0x00 → low 32 = {A,B,G,R}.
    localparam logic [63:0] RGBAQ_VAL   = 64'h0000_0000_00C0_50AA;
    // PSMCT16 halfword at byte_addr: 0x6155.
    localparam logic [15:0] PSMCT16_HW  = 16'h6155;
    // 5→8 bit-replicate expanded RGB at PCRTC output.
    localparam logic [7:0]  EXP_R = 8'hAD;
    localparam logic [7:0]  EXP_G = 8'h52;
    localparam logic [7:0]  EXP_B = 8'hC6;

    localparam int IMG_W = 16;
    localparam int IMG_H = 4;

    // ------------------------------------------------------------
    // Phase 3 — non-origin SPRITE in PSMCT16.
    //
    // FRAME_1: FBP=4 (= 8192-byte base, one page after Phase 1's
    //   image — no overlap), FBW=2 (= 128 px per row, two PSMCT16
    //   pages wide), PSM=PSMCT16.
    //     FRAME_1 layout: FBP[8:0], FBW[5:0]@16, PSM[5:0]@24.
    //     FBP=4 → bits[8:0]=0x004; FBW=2 → bits[21:16]=0x02 →
    //     0x0002_0000; PSM=2 → bits[29:24]=0x02 → 0x0200_0000.
    //
    // SPRITE rect: v1=(60, 4), v2=(67, 7) — 8×4 = 32 pixels.
    //   Crosses the PAGE-x boundary at x=64 (page (0,0) for
    //   x∈[60..63], page (1,0) for x∈[64..67]) so page_index
    //   changes mid-row. Within block-x: x∈[60..63] is xb=12..15
    //   inside block (0,3) of page (0,0); x∈[64..67] is xb=0..3
    //   inside block (0,0) of page (1,0). Within-block columns
    //   yb=4..7 cover columnTable16 rows 4..7 (different from
    //   Phase 1's yb=0..3). Strict non-origin coords — makes
    //   ras_fbp, ras_fbw, AND absolute s2_x_q/s2_y_q all
    //   load-bearing in the swizzle's input mux.
    //
    // RGBAQ for Phase 3: distinct color from Phase 1's so any
    // wrong-byte commit can be told apart.
    //   R=0x70, G=0xE0, B=0x38, A=0xFF → low 32 = 0xFF38E070.
    //   R5 = 0x70[7:3] = 0b01110 = 0x0E
    //   G5 = 0xE0[7:3] = 0b11100 = 0x1C
    //   B5 = 0x38[7:3] = 0b00111 = 0x07
    //   A1 = 1
    //   halfword = {1, 0x07, 0x1C, 0x0E} = 0x9F8E
    // 5→8 bit-replicate at scanout:
    //   R8 = {0x0E=0b01110, 0b011} = 0x73
    //   G8 = {0x1C=0b11100, 0b111} = 0xE7
    //   B8 = {0x07=0b00111, 0b001} = 0x39
    // ------------------------------------------------------------
    // FBP=4 → bits[8:0]=0x004; FBW=2 → bits[21:16]=0x02; PSM=2 →
    // bits[29:24]=0x02. Combined: 0x0000_0000_0202_0004.
    localparam logic [63:0] P3_FRAME_1_VAL = 64'h0000_0000_0202_0004;
    localparam logic [63:0] P3_RGBAQ_VAL   = 64'h0000_0000_FF38_E070;
    localparam logic [15:0] P3_PSMCT16_HW  = 16'h9F8E;
    localparam logic [7:0]  P3_EXP_R = 8'h73;
    localparam logic [7:0]  P3_EXP_G = 8'hE7;
    localparam logic [7:0]  P3_EXP_B = 8'h39;
    localparam int P3_FBP = 4;
    localparam int P3_FBW = 2;
    localparam int P3_X0  = 60;
    localparam int P3_Y0  = 4;
    localparam int P3_X1  = 67;
    localparam int P3_Y1  = 7;
    localparam int P3_W   = (P3_X1 - P3_X0 + 1);  // 8
    localparam int P3_H   = (P3_Y1 - P3_Y0 + 1);  // 4
    localparam int P3_PIXELS = P3_W * P3_H;       // 32

    initial begin
        rst_n            = 1'b0;
        gif_reg_wr_en    = 1'b0;
        gif_reg_num      = 8'd0;
        gif_reg_data     = 64'd0;
        priv_reg_wr_en   = 1'b0;
        priv_reg_wr_addr = 16'd0;
        priv_reg_wr_data = 64'd0;
        vram_read2_addr  = 32'd0;

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

        // ------------------------------------------------------------
        // Drive the 16×4 SPRITE. PRIM=SPRITE → v1+v2 define rect.
        // v1=(0,0), v2=(IMG_W-1, IMG_H-1) covers all 64 pixels
        // (inclusive bounding box). FBP=0, FBW=1, PSMCT16.
        // ------------------------------------------------------------
        drive_reg(R_PRIM,    PRIM_SPRITE);
        drive_reg(R_FRAME_1, FRAME_1_VAL);
        drive_reg(R_RGBAQ,   RGBAQ_VAL);
        drive_reg(R_XYZ2,    xyz2_data(12'd0, 12'd0));            // v1
        drive_reg(R_XYZ2,    xyz2_data(12'(IMG_W - 1),
                                       12'(IMG_H - 1)));          // v2 — close
        drive_idle();

        // Wait for raster to start, then drain.
        wait (raster_active == 1'b1);
        wait (raster_active == 1'b0);
        repeat (10) @(posedge clk);

        if (raster_pixel_emit_count != 32'(IMG_W * IMG_H)) begin
            $error("expected %0d raster emits, got %0d",
                   IMG_W * IMG_H, raster_pixel_emit_count);
            errors = errors + 1;
        end

        // ------------------------------------------------------------
        // Phase 1 — write-side lock. Verify each sprite halfword
        // landed at its SWIZZLED byte address. PSMCT16 = 2 bytes/
        // pixel, so each readback is a 16-bit halfword selected
        // from the 32-bit word via addr[1].
        // ------------------------------------------------------------
        for (int yy = 0; yy < IMG_H; yy++) begin
            for (int xx = 0; xx < IMG_W; xx++) begin
                logic [31:0] addr;
                logic [15:0] got;
                addr = ref_addr16(0, 1, xx, yy);
                vram_read2_addr = addr & ~32'd3;
                #1;
                got = addr[1] ? vram_read2_data[31:16]
                              : vram_read2_data[15:0];
                if (got !== PSMCT16_HW) begin
                    $error("[swizzled-hw] (%0d,%0d) @addr 0x%08x got 0x%04x expected 0x%04x",
                           xx, yy, addr, got, PSMCT16_HW);
                    errors = errors + 1;
                end
            end
        end

        // ------------------------------------------------------------
        // Strict linear-vs-swizzled separators (PSMCT16).
        //
        //   Linear addresses for a 16×4 PSMCT16 sprite at FBW=1,
        //   FBP=0 (stride = FBW*64*2 = 128):
        //     y=0  → bytes 0..30
        //     y=1  → bytes 128..158
        //     y=2  → bytes 256..286
        //     y=3  → bytes 384..414
        //
        //   Swizzled addresses span only block (0,0) of page 0
        //   (image is 16 wide × 4 tall, all in by=0..3, bx=0;
        //   columnTable16 yb=0..3 covers hw indices 0..63):
        //     swizzled writes bytes [0..126].
        //
        //   Bytes that linear hits but swizzled does NOT include
        //   byte 128 (linear y=1 row start) and byte 256 (linear
        //   y=2). If raster had silently fallen through to linear,
        //   byte 128 would hold the SPRITE halfword. Assert it
        //   stays 0.
        // ------------------------------------------------------------
        vram_read2_addr = 32'd128;
        #1;
        if (vram_read2_data !== 32'd0) begin
            $error("[linear-y1-byte-still-zero] @byte 0x80 got 0x%08x expected 0",
                   vram_read2_data);
            errors = errors + 1;
        end
        vram_read2_addr = 32'd256;
        #1;
        if (vram_read2_data !== 32'd0) begin
            $error("[linear-y2-byte-still-zero] @byte 0x100 got 0x%08x expected 0",
                   vram_read2_data);
            errors = errors + 1;
        end

        // ------------------------------------------------------------
        // Phase 2 — end-to-end agreement: enable swizzled scanout,
        // capture the frame, assert each visible pixel matches the
        // sprite RGB after PSMCT16's 5→8 bit-replicate at PCRTC.
        //
        //   DISPFB1: FBP=0, FBW=1, PSM=PSMCT16 (=2).
        //     FBW [14:9]   = 1 → 0x0200
        //     PSM [19:15]  = 2 → 0x10000
        //   DISPLAY1: DX=0, DY=0, DW=15, DH=3, MAGH=MAGV=0.
        //   PMODE.EN1 = 1.
        // ------------------------------------------------------------
        drive_priv(16'h0070, 64'h0000_0000_0001_0200);
        drive_priv(16'h0080, 64'h0030_000F_0000_0000);
        drive_priv(16'h0000, 64'h0000_0000_0000_0001);

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

        for (int yy = 0; yy < PCRTC_V_ACTIVE; yy++) begin
            for (int xx = 0; xx < PCRTC_H_ACTIVE; xx++) begin
                if (!cap_de[yy][xx]) begin
                    $error("[scanout] (%0d,%0d) DE never asserted", xx, yy);
                    errors = errors + 1;
                end else if (cap_r[yy][xx] !== EXP_R
                          || cap_g[yy][xx] !== EXP_G
                          || cap_b[yy][xx] !== EXP_B) begin
                    $error("[scanout] (%0d,%0d) got R=%02x G=%02x B=%02x expected R=%02x G=%02x B=%02x",
                           xx, yy,
                           cap_r[yy][xx], cap_g[yy][xx], cap_b[yy][xx],
                           EXP_R, EXP_G, EXP_B);
                    errors = errors + 1;
                end
            end
        end

        // ------------------------------------------------------------
        // Phase 3 — non-origin SPRITE.
        //
        // Re-arms gs_stub raster with FBP=4, FBW=2, PSMCT16, and a
        // sprite at (60, 4)..(67, 7) — 8×4 = 32 pixels. This pins
        // THREE contracts of the Ch128 PSMCT16 raster wiring that
        // Phase 1's origin sprite can't distinguish from a buggy
        // implementation:
        //   (a) `ras_fbp` reaches the swizzle module's `fbp` port
        //       (Phase 1 used FBP=0, so a regression that tied
        //       fbp=0 would still pass).
        //   (b) `ras_fbw` reaches the swizzle module's `fbw` port
        //       (Phase 1 used FBW=1; a regression that tied fbw=1
        //       would still pass).
        //   (c) The swizzle gets the FULL absolute pixel coords
        //       `(s2_x_q, s2_y_q)`, not bbox-local ones — Phase 1's
        //       sprite started at (0,0) so absolute and local
        //       agreed; a regression that fed bbox-local coords
        //       would land Phase 3's pixels at wrong addresses.
        //
        // Page-x boundary crossing: at FBW=2 (PSMCT16 page = 64 px
        // wide), x∈[60..63] is in page (0,0); x∈[64..67] is in
        // page (1,0). page_index changes mid-row — a regression
        // that drops `ras_fbw` from page_index would collapse
        // both halves to the same page. Within-block coords also
        // exercise columnTable16 rows yb=4..7, a different region
        // of the PSMCT16 column table than Phase 1's yb=0..3.
        // ------------------------------------------------------------
        drive_reg(R_PRIM,    PRIM_SPRITE);
        drive_reg(R_FRAME_1, P3_FRAME_1_VAL);
        drive_reg(R_RGBAQ,   P3_RGBAQ_VAL);
        drive_reg(R_XYZ2,    xyz2_data(12'(P3_X0), 12'(P3_Y0)));   // v1
        drive_reg(R_XYZ2,    xyz2_data(12'(P3_X1), 12'(P3_Y1)));   // v2 — close
        drive_idle();

        wait (raster_active == 1'b1);
        wait (raster_active == 1'b0);
        repeat (10) @(posedge clk);

        // Total emit count after Phase 3: 64 (Phase 1) + 32 (Phase 3 = 8×4).
        if (raster_pixel_emit_count != 32'(IMG_W * IMG_H + P3_PIXELS)) begin
            $error("after P3: expected %0d raster emits, got %0d",
                   IMG_W * IMG_H + P3_PIXELS, raster_pixel_emit_count);
            errors = errors + 1;
        end

        // Phase 3 per-pixel halfword readback at swizzled
        // addresses (PSMCT16 = 2 bytes/pixel; addr[1] selects the
        // halfword inside the 32-bit word).
        for (int yy = P3_Y0; yy <= P3_Y1; yy++) begin
            for (int xx = P3_X0; xx <= P3_X1; xx++) begin
                logic [31:0] addr;
                logic [15:0] got;
                addr = ref_addr16(P3_FBP, P3_FBW, xx, yy);
                vram_read2_addr = addr & ~32'd3;
                #1;
                got = addr[1] ? vram_read2_data[31:16]
                              : vram_read2_data[15:0];
                if (got !== P3_PSMCT16_HW) begin
                    $error("[p3-swizzled-hw] (%0d,%0d) @addr 0x%08x got 0x%04x expected 0x%04x",
                           xx, yy, addr, got, P3_PSMCT16_HW);
                    errors = errors + 1;
                end
            end
        end

        // P3 strict separator: the LINEAR formula for P3 places
        //   pixel(60, 4) at byte:
        //     FBP*2048 + (FBW*64*2)*y + x*2
        //     = 4*2048 + (2*64*2)*4 + 60*2
        //     = 8192 + 1024 + 120
        //     = 9336
        // This byte is NOT touched by any swizzled write in P3.
        // The Phase-3 swizzled set lands in:
        //   page (0,0) of FBP=4, block (0,3) (block_idx=10) →
        //     base 10752, bytes 10914..11006 (32 halfwords for
        //     xb=12..15, yb=4..7).
        //   page (1,0) of FBP=4, block (0,0) (block_idx=0) →
        //     base 16384, bytes 16512..16604 (32 halfwords for
        //     xb=0..3, yb=4..7).
        // Byte 9336 sits between page 0's start (8192) and the
        // block-(0,3) base (10752) — strictly outside the
        // swizzled write set. If raster had fallen through to
        // linear, byte 9336 would hold P3's halfword.
        vram_read2_addr = 32'd9336;
        #1;
        if (vram_read2_data !== 32'd0) begin
            $error("[p3-linear-byte-still-zero] @byte 0x%08x got 0x%08x expected 0",
                   32'd9336, vram_read2_data);
            errors = errors + 1;
        end

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

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

endmodule : tb_gs_raster_swizzle_psmct16
