// retroDE_ps2 — tb_gs_image_xfer_swizzle_psmt8 (Ch133)
//
// Write-side wiring of the Ch131 PSMT8 swizzle math primitive into
// gif_image_xfer_stub. PSMT8 TRXDIR uploads now route their per-
// pixel VRAM byte address through gs_swizzle_psmt8_stub when the
// new `PSMT8_SWIZZLE=1` parameter is set. PSMCT32 is gated by
// PSMCT32_SWIZZLE (Ch121), PSMCT16 by PSMCT16_SWIZZLE (Ch127);
// PSMT4 always linear. Default 0 keeps the legacy linear path for
// every existing PSMT8 image-xfer TB (Ch117 etc.). Mirrors the
// Ch127 wiring shape but for the third PSM, and uses the Ch132
// swizzled-pcrtc as the read-side cross-check.
//
// Topology:
//
//   TB.gif_reg_wr_*  →  gs_stub (TRX latches only)
//                     → gif_image_xfer_stub#(PSMT8_SWIZZLE=1)
//                     → vram_stub
//                     → gs_pcrtc_stub#(PSMT8_SWIZZLE=1) → r/g/b
//
// Three-phase verification (mirrors Ch127 audit-closed shape).
//
// PHASE 1 (write-side lock): TRXDIR upload of a 16×8 PSMT8 image
// at DBP=0, DBW=2, DSAX=DSAY=0. Each pixel has a unique 8-bit
// index value `idx_at(x, y) = (y[2:0] << 4) | x[3:0]` ∈
// [0x00..0x7F]. After upload, TB reads VRAM via the second read
// port at the SWIZZLED byte address (computed by an in-TB
// ref_addr8() that mirrors gs_swizzle_psmt8_stub — block table +
// column table both transcribed verbatim from the Ch131-locked
// PCSX2 source) and asserts each pixel byte landed where the
// swizzle says.
//
// Strict linear-vs-swizzled separator: at FBP=0/DBW=2, the 16×8
// PSMT8 image lives entirely in block (0,0) of page 0. Within-
// block byte indices for yb=0..7, xb=0..15 cover values 0..127
// (column-table rows 0..7, max 127). Under LINEAR formula, row
// stride = DBW*64 = 128, so y=1 row starts at byte 128, which is
// strictly outside the swizzled write set [0..127]. Assert byte
// 128 stays 0 (and byte 256 = linear y=2 row start).
//
// PHASE 2 (end-to-end agreement): without changing VRAM, enable
// the scanout path (PMODE/DISPFB1/DISPLAY1) with PSM=PSMT8 and
// capture a full 16×8 frame. Both the upload (Ch133) and the
// scanout (Ch132) instantiate the same gs_swizzle_psmt8_stub, so
// this phase proves the two integrations agree at byte level.
// PCRTC's PSMT8 grayscale fallback (clut_enable=0, Ch96) returns
// R=G=B=idx, so the captured RGB IS the index byte.
//
// PHASE 3 (non-origin transfer): re-arm with DBP=8, DBW=2,
// DSAX=12, DSAY=10, RRW=8, RRH=8. Effective coords (12..19,
// 10..17) cross block_x=0→1 at effective_x=16 AND block_y=0→1 at
// effective_y=16, exercising both block-table dimensions inside a
// single non-origin upload. All 4 corner blocks of page (0,0) at
// DBP=8 are visited (blockTable8[0][0]=0, [0][1]=1, [1][0]=2,
// [1][1]=3 → block bases 2048/2304/2560/2816). Pins three
// contracts the origin transfer can't:
//   (a) `dest_base_q (= DBP*256)` is correctly ADDED ON TOP of the
//       swizzle output (DBP=0 in P1 would mask a missing-add).
//   (b) The swizzle is fed the FULL effective coords (DSAX=DSAY=0
//       in P1 would mask a "feeds only cur_x/cur_y" regression).
//   (c) Both block_x and block_y propagate through blockTable8 —
//       block_x=0 throughout Phase 1 would mask a tied-block_x
//       regression.
//
// Phase 3 strict separator: linear formula puts effective coord
// (12, 10) at byte:
//   DBP*256 + 10*DBW*64 + 12 = 2048 + 1280 + 12 = 3340
// The Phase-3 swizzled write set lives in [2048..3071] (4 corner
// blocks at DBP=8); byte 3340 is strictly outside. Assert byte
// 3340 stays 0 — proves a fall-through to linear would have
// stomped it.

`timescale 1ns/1ps

module tb_gs_image_xfer_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;

    // ------------------------------------------------------------
    // gs_stub — only used here for its TRX-register latches +
    // trxdir_wr_q pulse. Everything else (raster, prim, scanout)
    // is unused.
    // ------------------------------------------------------------
    logic        gif_reg_wr_en;
    logic [7:0]  gif_reg_num;
    logic [63:0] gif_reg_data;

    logic [63:0] tb_pmode_q;
    logic [63:0] tb_dispfb1_q;
    logic [63:0] tb_display1_q;

    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 u_gs (
        .clk(clk), .rst_n(rst_n),
        .reg_wr_en(1'b0), .reg_wr_addr(16'd0), .reg_wr_data(64'd0),
        .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)
    );

    // ------------------------------------------------------------
    // gif_image_xfer_stub with the new Ch133 PSMT8_SWIZZLE gate ON
    // (and the Ch121 PSMCT32_SWIZZLE / Ch127 PSMCT16_SWIZZLE gates
    // explicitly OFF — this TB exercises only the PSMT8 path).
    // ------------------------------------------------------------
    logic         xfer_data_valid;
    logic [127:0] xfer_data_qword;
    logic         xfer_data_last;
    logic         xfer_data_ready;
    logic         xfer_we;
    logic [31:0]  xfer_waddr;
    logic [31:0]  xfer_wdata;
    logic [3:0]   xfer_wbe;
    logic [31:0]  xfer_wmask;
    logic         xfer_busy;

    gif_image_xfer_stub #(
        .PSMCT32_SWIZZLE(1'b0),
        .PSMCT16_SWIZZLE(1'b0),
        .PSMT8_SWIZZLE  (1'b1)
    ) u_xfer (
        .clk(clk), .rst_n(rst_n),
        .trxdir_wr_pulse(trxdir_wr_q),
        .trxdir(trxdir_q),
        .bitbltbuf(bitbltbuf_q),
        .trxpos(trxpos_q),
        .trxreg(trxreg_q),
        .data_valid(xfer_data_valid),
        .data_qword(xfer_data_qword),
        .data_last (xfer_data_last),
        .data_ready(xfer_data_ready),
        .vram_we   (xfer_we),
        .vram_waddr(xfer_waddr),
        .vram_wdata(xfer_wdata),
        .vram_wbe  (xfer_wbe),
        .vram_wmask(xfer_wmask),
        .busy      (xfer_busy)
    );

    // ------------------------------------------------------------
    // vram_stub. Two readers: pcrtc (Phase 2) + TB (Phase 1/3).
    // ------------------------------------------------------------
    logic [31:0] vram_raddr;
    logic [31:0] vram_rdata;
    logic [31:0] vram_raddr2;
    logic [31:0] vram_rdata2;

    vram_stub #(.BYTES(16384)) u_vram (
        .clk(clk), .rst_n(rst_n),
        .write_en  (xfer_we),
        .write_addr(xfer_waddr),
        .write_data(xfer_wdata),
        .write_be  (xfer_wbe),
        .write_mask(xfer_wmask),
        .read_addr (vram_raddr),
        .read_data (vram_rdata),
        .read2_addr(vram_raddr2),
        .read2_data(vram_rdata2)
    );

    // ------------------------------------------------------------
    // gs_pcrtc_stub with the Ch132 PSMT8_SWIZZLE gate ON (and the
    // Ch120/Ch126 gates explicitly OFF). Drives vram_raddr.
    // ------------------------------------------------------------
    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'b0),
        .PSMT8_SWIZZLE  (1'b1)
    ) u_pcrtc (
        .clk(clk), .rst_n(rst_n),
        .pmode_q       (tb_pmode_q),
        .dispfb1_q     (tb_dispfb1_q),
        .display1_q    (tb_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)
    );

    // ------------------------------------------------------------
    // Counters + invariants observers.
    // ------------------------------------------------------------
    int  errors;
    int  xfer_writes_seen;
    int  trxdir_arms_seen;

    initial begin
        errors           = 0;
        xfer_writes_seen = 0;
        trxdir_arms_seen = 0;
    end

    always_ff @(posedge clk) begin
        if (rst_n && trxdir_wr_q)
            trxdir_arms_seen <= trxdir_arms_seen + 1;
        if (rst_n && xfer_we) begin
            xfer_writes_seen <= xfer_writes_seen + 1;
            if (xfer_wbe !== 4'b0001) begin
                $error("xfer write %0d: be=%b (expected 4'b0001 for PSMT8)",
                       xfer_writes_seen, xfer_wbe);
                errors <= errors + 1;
            end
            if (xfer_wmask !== 32'hFFFF_FFFF) begin
                $error("xfer write %0d: mask=0x%08x (expected 0xFFFFFFFF)",
                       xfer_writes_seen, xfer_wmask);
                errors <= errors + 1;
            end
        end
    end

    // ------------------------------------------------------------
    // Frame capture (Phase 2).
    // ------------------------------------------------------------
    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];
    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
        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 / TRX register 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

    function automatic logic [63:0] bitbltbuf_pack(
        input logic [13:0] dbp_v,
        input logic [5:0]  dbw_v,
        input logic [5:0]  dpsm_v);
        logic [63:0] v;
        v = 64'd0;
        v[45:32] = dbp_v;
        v[53:48] = dbw_v;
        v[61:56] = dpsm_v;
        return v;
    endfunction
    function automatic logic [63:0] trxpos_pack(
        input logic [10:0] dsax_v,
        input logic [10:0] dsay_v);
        logic [63:0] v;
        v = 64'd0;
        v[42:32] = dsax_v;
        v[58:48] = dsay_v;
        return v;
    endfunction
    function automatic logic [63:0] trxreg_pack(
        input logic [11:0] rrw_v,
        input logic [11:0] rrh_v);
        logic [63:0] v;
        v = 64'd0;
        v[11:0]  = rrw_v;
        v[43:32] = rrh_v;
        return v;
    endfunction
    function automatic logic [63:0] trxdir_pack(input logic [1:0] xdir_v);
        return {62'd0, xdir_v};
    endfunction

    task automatic push_image_qword(input logic [127:0] qword,
                                    input logic         last);
        @(negedge clk);
        wait (xfer_data_ready == 1'b1);
        @(negedge clk);
        xfer_data_valid = 1'b1;
        xfer_data_qword = qword;
        xfer_data_last  = last;
        @(posedge clk);
        @(negedge clk);
        xfer_data_valid = 1'b0;
        xfer_data_qword = 128'd0;
        xfer_data_last  = 1'b0;
    endtask

    // ------------------------------------------------------------
    // PSMT8 swizzle reference — block table + column table both
    // taken VERBATIM from PCSX2's pcsx2/GS/GSTables.cpp (master
    // HEAD 3000e113, file blob SHA 3581209b). Locked at byte level
    // by tb_gs_swizzle_psmt8 (Ch131): blockTable8 lines 53–59,
    // columnTable8 lines 111–145.
    // ------------------------------------------------------------
    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]})
            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;
            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;
            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;
            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;
            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;
            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;
            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;
            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;
            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;
            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;
            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;
            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;
            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;
            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;
            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;
            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]. PCRTC's PSMT8 grayscale fallback
    // 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

    // 16 PSMT8 pixels per IMAGE qword (16 × 8 bits = 128 bits).
    function automatic logic [127:0] qword_for_pixels(
        input int x_base, input int y);
        logic [127:0] q;
        q = 128'd0;
        for (int i = 0; i < 16; i++) begin
            q[i*8 +: 8] = idx_at(x_base + i, y);
        end
        return q;
    endfunction

    // Phase 3 distinct-pixel pattern. Different bit pattern from
    // Phase 1 so a Phase-3 byte landing at a Phase-1 byte (or
    // vice versa) surfaces as wrong RGB. p3_idx ∈ [0x80..0xFF],
    // strictly disjoint from Phase 1's [0x00..0x7F].
    function automatic logic [7:0] p3_idx_at(input int x_local, input int y_local);
        return 8'h80 | idx_at(x_local, y_local);
    endfunction

    function automatic logic [127:0] p3_qword_for_pixels(
        input int x_base_local, input int y_local);
        logic [127:0] q;
        q = 128'd0;
        for (int i = 0; i < 16; i++) begin
            q[i*8 +: 8] = p3_idx_at(x_base_local + i, y_local);
        end
        return q;
    endfunction

    // ------------------------------------------------------------
    // TB-side byte readback via the 2nd VRAM read port. PSMT8 = 1
    // byte/pixel, so each readback selects a byte from the
    // 32-bit word via addr[1:0].
    // ------------------------------------------------------------
    task automatic check_pixel_at_swizzled(input int x, input int y);
        logic [31:0] addr;
        logic [7:0]  expected;
        logic [7:0]  got;
        addr     = ref_addr8(0, DBW, x, y);
        expected = idx_at(x, y);
        vram_raddr2 = addr & ~32'd3;
        #1;
        case (addr[1:0])
            2'b00: got = vram_rdata2[7:0];
            2'b01: got = vram_rdata2[15:8];
            2'b10: got = vram_rdata2[23:16];
            default: got = vram_rdata2[31:24];
        endcase
        if (got !== expected) begin
            $error("[swizzled-byte] (%0d,%0d) @addr 0x%08x got 0x%02x expected 0x%02x",
                   x, y, addr, got, expected);
            errors = errors + 1;
        end
    endtask

    task automatic check_byte_zero(input logic [31:0] byte_addr,
                                   input string       tag);
        logic [31:0] got;
        vram_raddr2 = byte_addr & ~32'd3;
        #1;
        got = vram_rdata2;
        if (got !== 32'd0) begin
            $error("[%s] @byte 0x%08x got 0x%08x expected 0x00000000",
                   tag, byte_addr, got);
            errors = errors + 1;
        end
    endtask

    task automatic check_p3_pixel(input int x_local, input int y_local);
        logic [31:0] addr;
        logic [7:0]  expected;
        logic [7:0]  got;
        addr     = 32'(P3_DBP * 256)
                 + ref_addr8(0, DBW,
                             P3_DSAX + x_local,
                             P3_DSAY + y_local);
        expected = p3_idx_at(x_local, y_local);
        vram_raddr2 = addr & ~32'd3;
        #1;
        case (addr[1:0])
            2'b00: got = vram_rdata2[7:0];
            2'b01: got = vram_rdata2[15:8];
            2'b10: got = vram_rdata2[23:16];
            default: got = vram_rdata2[31:24];
        endcase
        if (got !== expected) begin
            $error("[p3-swizzled-byte] (%0d,%0d) @addr 0x%08x got 0x%02x expected 0x%02x",
                   x_local, y_local, addr, got, expected);
            errors = errors + 1;
        end
    endtask

    // ------------------------------------------------------------
    // Constants.
    // ------------------------------------------------------------
    localparam logic [7:0] R_BITBLTBUF = 8'h50;
    localparam logic [7:0] R_TRXPOS    = 8'h51;
    localparam logic [7:0] R_TRXREG    = 8'h52;
    localparam logic [7:0] R_TRXDIR    = 8'h53;

    // Phase 1/2 transfer parameters (origin: DBP=DSAX=DSAY=0).
    // PSMT8: 16 pixels per IMAGE qword. DBW must be even — minimum
    // is 2 (PCSX2 asserts (bw & 1) == 0 at GSLocalMemory.h:553).
    localparam int IMG_DBP = 0;
    localparam int DBW     = 2;
    localparam int RRW     = 16;
    localparam int RRH     = 8;
    localparam int NUM_PIXELS = RRW * RRH;     // 128
    localparam int NUM_QWORDS = NUM_PIXELS / 16; // 8

    // Phase 3 transfer parameters (non-origin: nonzero DBP +
    // nonzero DSAX + nonzero DSAY). For PSMT8, blocks are 16×16
    // pixels, so DSAX=12 + RRW=8 makes effective_x cross 16
    // (block_x=0→1) and DSAY=10 + RRH=8 makes effective_y cross
    // 16 (block_y=0→1).
    //
    //   DBP=8 → dest_base_q = 2048.
    //   DSAX=12, DSAY=10 → effective coords (12..19, 10..17).
    //   DBW=2, RRW=8, RRH=8 → 64 PSMT8 pixels = 4 IMAGE qwords.
    localparam int P3_DBP    = 8;
    localparam int P3_DSAX   = 12;
    localparam int P3_DSAY   = 10;
    localparam int P3_RRW    = 8;
    localparam int P3_RRH    = 8;
    localparam int P3_PIXELS = P3_RRW * P3_RRH; // 64
    // Note: P3 packs 16 PSMT8 pixels/qword by pairing two RRW=8
    // rows per qword, so P3_PIXELS/16 = 4 qwords are pushed (see
    // the row-pair loop below). No P3_QWORDS localparam — the
    // packing is intrinsic to the row-pair loop.

    initial begin
        rst_n            = 1'b0;
        gif_reg_wr_en    = 1'b0;
        gif_reg_num      = 8'd0;
        gif_reg_data     = 64'd0;
        xfer_data_valid  = 1'b0;
        xfer_data_qword  = 128'd0;
        xfer_data_last   = 1'b0;
        vram_raddr2      = 32'd0;
        tb_pmode_q       = 64'd0;
        tb_dispfb1_q     = 64'd0;
        tb_display1_q    = 64'd0;

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

        // ============================================================
        // Phase 1 — origin TRXDIR upload.
        // ============================================================
        drive_reg(R_BITBLTBUF, bitbltbuf_pack(14'(IMG_DBP), 6'(DBW), 6'h13)); // PSMT8
        drive_reg(R_TRXPOS,    trxpos_pack(11'd0, 11'd0));
        drive_reg(R_TRXREG,    trxreg_pack(12'(RRW), 12'(RRH)));
        drive_reg(R_TRXDIR,    trxdir_pack(2'd0));
        drive_idle();

        @(posedge clk);
        @(posedge clk);

        // Push 8 IMAGE qwords (16 PSMT8 pixels each, row-major).
        for (int yy = 0; yy < RRH; yy++) begin
            for (int qi = 0; qi < (RRW / 16); qi++) begin
                logic [127:0] q;
                bit           is_last;
                q       = qword_for_pixels(qi * 16, yy);
                is_last = (yy == RRH - 1) && (qi == (RRW / 16) - 1);
                push_image_qword(q, is_last);
            end
        end

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

        // Phase 1 count check.
        if (trxdir_arms_seen != 1) begin
            $error("after P1: trxdir_wr_q pulses=%0d (expected 1)",
                   trxdir_arms_seen);
            errors = errors + 1;
        end
        if (xfer_writes_seen != NUM_PIXELS) begin
            $error("after P1: xfer VRAM writes=%0d (expected %0d)",
                   xfer_writes_seen, NUM_PIXELS);
            errors = errors + 1;
        end

        // Phase 1 per-pixel byte readback at swizzled addresses.
        for (int yy = 0; yy < RRH; yy++)
            for (int xx = 0; xx < RRW; xx++)
                check_pixel_at_swizzled(xx, yy);

        // Strict separator: linear y=1 row starts at byte 128
        // (DBW=2 → row stride 128). Swizzled write set lives in
        // [0..127]. Byte 128 stays 0 → fall-through to linear
        // would have placed pixel(0, 1) there.
        check_byte_zero(32'd128, "linear-y1-byte-still-zero");
        // Linear y=2 row starts at byte 256, also outside swizzled
        // set.
        check_byte_zero(32'd256, "linear-y2-byte-still-zero");

        // ============================================================
        // Phase 3 — non-origin PSMT8 transfer. Re-arm with DBP=8,
        // DBW=2, DSAX=12, DSAY=10, RRW=8, RRH=8. Effective coords
        // (12..19, 10..17) cross block_x=0→1 at effective_x=16 AND
        // block_y=0→1 at effective_y=16, exercising both block-
        // table dimensions inside a single non-origin upload.
        // ============================================================
        drive_reg(R_BITBLTBUF, bitbltbuf_pack(14'(P3_DBP), 6'(DBW), 6'h13)); // PSMT8
        drive_reg(R_TRXPOS,    trxpos_pack(11'(P3_DSAX), 11'(P3_DSAY)));
        drive_reg(R_TRXREG,    trxreg_pack(12'(P3_RRW), 12'(P3_RRH)));
        drive_reg(R_TRXDIR,    trxdir_pack(2'd0));
        drive_idle();

        @(posedge clk);
        @(posedge clk);

        // Phase 3 has 8 pixels/row × 8 rows = 64 pixels. The engine
        // drains 16 lanes/qword; with RRW=8 only 8 lanes per row
        // are active, so 2 rows pack into one qword (16 lanes).
        // Generate qwords pairing rows: rows (0,1), (2,3), (4,5),
        // (6,7) — 4 qwords total.
        for (int row_pair = 0; row_pair < 4; row_pair++) begin
            logic [127:0] q;
            bit           is_last;
            q = 128'd0;
            // Lanes 0..7 carry row 2*row_pair, lanes 8..15 carry row 2*row_pair+1.
            for (int i = 0; i < 8; i++) begin
                q[i*8 +: 8]      = p3_idx_at(i, 2*row_pair);
                q[(i+8)*8 +: 8]  = p3_idx_at(i, 2*row_pair + 1);
            end
            is_last = (row_pair == 3);
            push_image_qword(q, is_last);
        end

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

        // Total counts: 2 arms, 128+64 = 192 PSMT8 writes.
        if (trxdir_arms_seen != 2) begin
            $error("after P3: trxdir_wr_q pulses=%0d (expected 2)",
                   trxdir_arms_seen);
            errors = errors + 1;
        end
        if (xfer_writes_seen != (NUM_PIXELS + P3_PIXELS)) begin
            $error("after P3: xfer VRAM writes=%0d (expected %0d)",
                   xfer_writes_seen, NUM_PIXELS + P3_PIXELS);
            errors = errors + 1;
        end

        // Per-pixel byte readback for Phase 3.
        for (int yy = 0; yy < P3_RRH; yy++)
            for (int xx = 0; xx < P3_RRW; xx++)
                check_p3_pixel(xx, yy);

        // P3 strict separator: linear formula for the Phase-3 PSMT8
        // upload would place effective coord (12, 10) at byte:
        //   dest_base + effective_y*row_stride + effective_x
        //   = 2048 + 10*128 + 12 = 3340
        // The SWIZZLED write set for P3 fills 4 corner blocks of
        // page (0,0) at DBP=8: blockTable8[0][0]=0 (bytes 2048..),
        // [0][1]=1 (2304..), [1][0]=2 (2560..), [1][1]=3 (2816..).
        // Total range [2048..3071]. Byte 3340 is strictly outside.
        check_byte_zero(32'd3340, "p3-linear-byte-still-zero");

        // ============================================================
        // Phase 2 — end-to-end agreement: enable PSMT8 swizzled
        // scanout (Ch132), capture the frame, assert each visible
        // pixel matches its uploaded byte (PCRTC PSMT8 grayscale
        // fallback gives R=G=B=idx).
        // ============================================================
        // DISPFB1: FBP=0, FBW=2, PSM=PSMT8 (=0x13).
        //   FBW [14:9]   = 2   → 0x0400
        //   PSM [19:15]  = 0x13 → 0x13<<15 = 0x9_8000
        tb_dispfb1_q = 64'h0000_0000_0009_8400;
        // DISPLAY1: DX=0, DY=0, DW=15, DH=7, MAGH=MAGV=0
        tb_display1_q = 64'h0070_000F_0000_0000;
        // PMODE.EN1=1
        tb_pmode_q = 64'h0000_0000_0000_0001;

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

        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("[scanout] (%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("[scanout] (%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

        $display("[tb_gs_image_xfer_swizzle_psmt8] arms=%0d writes=%0d errors=%0d",
                 trxdir_arms_seen, xfer_writes_seen, errors);
        if (errors == 0) $display("[tb_gs_image_xfer_swizzle_psmt8] PASS");
        else             $display("[tb_gs_image_xfer_swizzle_psmt8] FAIL");
        $finish;
    end

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

endmodule : tb_gs_image_xfer_swizzle_psmt8
