// retroDE_ps2 — tb_gs_image_xfer_psmt4 (Ch118)
//
// Locks the contract for `gif_image_xfer_stub`'s PSMT4 path —
// the fourth and final supported PSM (Ch110 PSMCT32, Ch116
// PSMCT16, Ch117 PSMT8, Ch118 PSMT4). PSMT4 packs 0.5 bytes/pixel
// (a 4-bit CLUT index per pixel = 2 px/byte), so each 128-bit
// IMAGE qword carries 32 pixels in 16 bytes. Each emit is a
// SUB-BYTE write: write_be = 4'b0001 with a per-emit nibble
// mask (write_mask = 0x0000_000F for the LOW nibble, 0x0000_00F0
// for the HIGH nibble), keyed by `(DSAX + cur_x)[0]`. The 4-bit
// index sits at the matching nibble position in write_data[7:0];
// vram_stub's per-bit merge commits exactly that nibble — the
// OTHER nibble of the same byte is preserved.
//
// This TB is INTENTIONALLY adversarial about the nibble-RMW
// contract, mirroring the raster-channel Ch106 strategy:
//   - VRAM is preloaded with an 0xA5 byte pattern across every
//     byte the engine will write (and every neighbour byte). A
//     buggy full-byte commit would visibly stomp the un-touched
//     nibble; a correct RMW preserves it.
//   - The 8×4 destination rect (32 PSMT4 pixels = 1 IMAGE qword)
//     is small enough to walk pixel-by-pixel in the assertion
//     phase. Each lane writes one nibble; back-to-back lanes
//     within the same byte (lane 0+1, lane 2+3, ...) chain
//     through NBA semantics so both nibbles end up in the byte
//     without bypass logic.
//
// Setup:
//   No GS pipeline, no gif_packed_stub. Just gs_stub (for its
//   TRX-register latches + trxdir_wr_q pulse) + gif_image_xfer_stub
//   + vram_stub. TB drives BITBLTBUF/TRXPOS/TRXREG/TRXDIR via
//   gs_stub.gif_reg_wr_*, preloads vram_stub with 0xA5 at every
//   byte the engine will touch (plus a neighbour for boundary
//   preservation), then hand-pumps 1 IMAGE qword into xfer.data_*.
//
//   BITBLTBUF: DBP=4 (dest base = 4*256 = 1024 = 0x400 bytes),
//              DBW=1 (64 px/row → row stride = 1*64*0.5 = 32 bytes),
//              DPSM=PSMT4 (=0x14).
//   TRXPOS:    DSAX=DSAY=0.
//   TRXREG:    RRW=8, RRH=4 → 8×4 = 32 PSMT4 pixels = 1 qword.
//   TRXDIR:    XDIR=0 (host→local).
//
//   Pixel pattern: pixel(x, y) = 4'(y*8 + x) — 32 distinct
//   nibble values 0x0..0x1F (mod 16) across the 8×4 rect so a
//   wrong-lane commit shows up unambiguously.
//
// PASS criteria:
//   - 1 trxdir_wr_q pulse arms the engine.
//   - 32 vram writes from gif_image_xfer_stub.
//   - Every emit has be == 4'b0001, mask ∈ {0x0F, 0xF0}.
//   - Every pixel reads back at byte address
//       byte_addr(x, y) = DEST_BASE + y*ROW_STRIDE + (x >> 1)
//     with the right nibble at position (x & 1).
//   - The OTHER nibble of every touched byte is preserved at the
//     preload value (i.e., a touched byte has BOTH nibbles
//     correct: one from the upload, one stays at 0xA5 nibble's
//     half if the rect didn't cover it). For our 8-wide rect
//     all bytes within a row are FULLY covered by the rect, so
//     each touched byte ends up as `{nibble_high_pixel,
//     nibble_low_pixel}` with NO trace of the 0xA5 preload.
//   - Bytes immediately RIGHT of the rect on each row stay
//     0xA5 (no leak past RRW).
//   - Bytes BEFORE / AFTER the destination region stay 0xA5.

`timescale 1ns/1ps

module tb_gs_image_xfer_psmt4;

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

    // ------------------------------------------------------------
    // gs_stub — only used for TRX-register latches + trxdir_wr_q.
    // ------------------------------------------------------------
    logic        gif_reg_wr_en;
    logic [7:0]  gif_reg_num;
    logic [63:0] gif_reg_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 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
    // ------------------------------------------------------------
    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 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 with TB-side preload mux. The TB pre-fills VRAM
    // with 0xA5 at every byte the engine will write (plus
    // boundary bytes), then activates the engine path for the
    // upload.
    // ------------------------------------------------------------
    logic        tb_preload_active;
    logic        tb_preload_en;
    logic [31:0] tb_preload_addr;
    logic [31:0] tb_preload_data;
    logic [3:0]  tb_preload_be;
    logic [31:0] tb_preload_mask;

    logic        vram_we_mux;
    logic [31:0] vram_waddr_mux;
    logic [31:0] vram_wdata_mux;
    logic [3:0]  vram_wbe_mux;
    logic [31:0] vram_wmask_mux;

    assign vram_we_mux    = tb_preload_active ? tb_preload_en   : xfer_we;
    assign vram_waddr_mux = tb_preload_active ? tb_preload_addr : xfer_waddr;
    assign vram_wdata_mux = tb_preload_active ? tb_preload_data : xfer_wdata;
    assign vram_wbe_mux   = tb_preload_active ? tb_preload_be   : xfer_wbe;
    assign vram_wmask_mux = tb_preload_active ? tb_preload_mask : xfer_wmask;

    logic [31:0] vram_raddr;
    logic [31:0] vram_rdata;

    vram_stub #(.BYTES(4096)) u_vram (
        .clk(clk), .rst_n(rst_n),
        .write_en  (vram_we_mux),
        .write_addr(vram_waddr_mux),
        .write_data(vram_wdata_mux),
        .write_be  (vram_wbe_mux),
        .write_mask(vram_wmask_mux),
        .read_addr (vram_raddr),
        .read_data (vram_rdata),
        .read2_addr(32'd0),
        .read2_data()
    );

    // ------------------------------------------------------------
    // 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 PSMT4)",
                       xfer_writes_seen, xfer_wbe);
                errors <= errors + 1;
            end
            if (xfer_wmask !== 32'h0000_000F &&
                xfer_wmask !== 32'h0000_00F0) begin
                $error("xfer write %0d: mask=0x%08x (expected 0x0F or 0xF0)",
                       xfer_writes_seen, xfer_wmask);
                errors <= errors + 1;
            end
        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 preload_byte(input logic [31:0] byte_addr,
                                input logic [7:0]  val);
        @(negedge clk);
        tb_preload_active = 1'b1;
        tb_preload_en     = 1'b1;
        tb_preload_addr   = byte_addr;
        tb_preload_data   = {24'd0, val};
        tb_preload_be     = 4'b0001;
        tb_preload_mask   = 32'hFFFF_FFFF;
        @(posedge clk);
        @(negedge clk);
        tb_preload_en     = 1'b0;
        tb_preload_active = 1'b0;
    endtask

    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

    // PSMT4 pattern: 4-bit nibble per pixel.
    //   pixel(x, y) = 4'((y*8 + x) & 4'hF)
    // Distinct sequence 0,1,2,...,F,0,1,...F across the 32 pixels
    // of the 8×4 rect — easy to spot a wrong-lane commit.
    function automatic logic [3:0] pixel_at(input int x, input int y);
        return 4'((y * 8 + x) & 4'hF);
    endfunction

    // Build the IMAGE qword: 32 PSMT4 pixels packed low → high.
    // PSMT4 byte layout matches PCSX2: byte i holds pixels (2i, 2i+1)
    // with low nibble = pixel 2i, high nibble = pixel 2i+1.
    // Lanes within the qword are nibble-packed:
    //   qword[lane*4 +: 4] = pixel at lane index.
    function automatic logic [127:0] qword_for_pixels();
        // The 32 lanes traverse the 8×4 rect in row-major order:
        //   lane 0..7  → row 0, x=0..7
        //   lane 8..15 → row 1
        //   lane 16..23→ row 2
        //   lane 24..31→ row 3
        logic [127:0] q;
        q = 128'd0;
        for (int yy = 0; yy < 4; yy++) begin
            for (int xx = 0; xx < 8; xx++) begin
                int lane;
                lane = yy * 8 + xx;
                q[lane * 4 +: 4] = pixel_at(xx, yy);
            end
        end
        return q;
    endfunction

    // ------------------------------------------------------------
    // 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;

    localparam int          DBP    = 4;
    localparam int          DBW    = 1;
    localparam int          RRW    = 8;
    localparam int          RRH    = 4;
    // PSMT4: 0.5 byte/px, so DBW * 64 * 0.5 = DBW * 32.
    localparam int          ROW_STRIDE_BYTES = DBW * 32;        // = 32
    localparam int          DEST_BASE_BYTES  = DBP * 256;       // = 1024
    localparam int          NUM_PIXELS = RRW * RRH;             // 32
    localparam int          BYTES_PER_ROW    = RRW / 2;         // 4

    // ------------------------------------------------------------
    // Read helpers.
    // ------------------------------------------------------------
    task automatic read_byte(input logic [31:0] byte_addr,
                             output logic [7:0] byte_val);
        vram_raddr = byte_addr & ~32'd3;
        #1;
        unique case (byte_addr[1:0])
            2'd0: byte_val = vram_rdata[7:0];
            2'd1: byte_val = vram_rdata[15:8];
            2'd2: byte_val = vram_rdata[23:16];
            2'd3: byte_val = vram_rdata[31:24];
        endcase
    endtask

    task automatic check_byte(input logic [31:0] byte_addr,
                              input logic [7:0]  expected,
                              input string       tag);
        logic [7:0] got;
        read_byte(byte_addr, got);
        if (got !== expected) begin
            $error("[%s] @byte 0x%08x got 0x%02x expected 0x%02x",
                   tag, byte_addr, got, expected);
            errors = errors + 1;
        end
    endtask

    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_raddr        = 32'd0;
        tb_preload_active = 1'b0;
        tb_preload_en     = 1'b0;
        tb_preload_addr   = 32'd0;
        tb_preload_data   = 32'd0;
        tb_preload_be     = 4'd0;
        tb_preload_mask   = 32'hFFFF_FFFF;

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

        // ------------------------------------------------------------
        // Preload 0xA5 across every byte the engine will write
        // (4 bytes per row × 4 rows = 16 bytes), plus a few
        // boundary bytes (right of rect on each row + bytes
        // before/after the dest region).
        // ------------------------------------------------------------
        for (int yy = 0; yy < RRH; yy++) begin
            for (int b = 0; b < BYTES_PER_ROW + 1; b++) begin   // +1 for right-of-rect
                preload_byte(32'(DEST_BASE_BYTES + yy * ROW_STRIDE_BYTES + b),
                             8'hA5);
            end
        end
        // Bytes before / after the dest region (boundary preservation).
        preload_byte(32'(DEST_BASE_BYTES - 16), 8'hA5);
        preload_byte(32'(DEST_BASE_BYTES + RRH * ROW_STRIDE_BYTES + 16),
                     8'hA5);

        // ------------------------------------------------------------
        // TRX setup via GIF A+D writes. DPSM=PSMT4 (0x14).
        // ------------------------------------------------------------
        drive_reg(R_BITBLTBUF, bitbltbuf_pack(14'(DBP), 6'(DBW), 6'h14));
        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 1 IMAGE qword (32 PSMT4 pixels = entire 8×4 rect).
        // ------------------------------------------------------------
        push_image_qword(qword_for_pixels(), 1'b1);

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

        // ------------------------------------------------------------
        // Aggregate counts.
        // ------------------------------------------------------------
        if (trxdir_arms_seen != 1) begin
            $error("trxdir_wr_q pulses=%0d (expected 1)", trxdir_arms_seen);
            errors = errors + 1;
        end
        if (xfer_writes_seen != NUM_PIXELS) begin
            $error("xfer VRAM writes=%0d (expected %0d)",
                   xfer_writes_seen, NUM_PIXELS);
            errors = errors + 1;
        end

        // ------------------------------------------------------------
        // Per-byte readback. Each byte holds two pixels:
        //   byte_addr(byte_x, y) = DEST_BASE + y*ROW_STRIDE + byte_x
        //   low nibble  = pixel_at(2*byte_x,     y)
        //   high nibble = pixel_at(2*byte_x + 1, y)
        // ------------------------------------------------------------
        for (int yy = 0; yy < RRH; yy++) begin
            for (int bx = 0; bx < BYTES_PER_ROW; bx++) begin
                logic [31:0] addr;
                logic [3:0]  lo, hi;
                logic [7:0]  expected;
                addr     = 32'(DEST_BASE_BYTES + yy * ROW_STRIDE_BYTES + bx);
                lo       = pixel_at(2 * bx,     yy);
                hi       = pixel_at(2 * bx + 1, yy);
                expected = {hi, lo};
                check_byte(addr, expected, "psmt4-byte");
            end
        end

        // ------------------------------------------------------------
        // Boundary preservation: bytes immediately RIGHT of the
        // rect on each row should still read 0xA5 — proves no
        // leak past the rect (RRW pixels = BYTES_PER_ROW bytes).
        // ------------------------------------------------------------
        for (int yy = 0; yy < RRH; yy++) begin
            check_byte(32'(DEST_BASE_BYTES + yy * ROW_STRIDE_BYTES
                           + BYTES_PER_ROW),
                       8'hA5, "right-of-rect");
        end

        check_byte(32'(DEST_BASE_BYTES - 16), 8'hA5, "before-rect");
        check_byte(32'(DEST_BASE_BYTES + RRH * ROW_STRIDE_BYTES + 16),
                   8'hA5, "after-rect");

        $display("[tb_gs_image_xfer_psmt4] arms=%0d writes=%0d pixels=%0dx%0d",
                 trxdir_arms_seen, xfer_writes_seen, RRW, RRH);

        if (errors == 0) $display("[tb_gs_image_xfer_psmt4] PASS");
        else             $display("[tb_gs_image_xfer_psmt4] FAIL");
        $finish;
    end

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

endmodule : tb_gs_image_xfer_psmt4
