// retroDE_ps2 — tb_gs_demo_psmt4_e2e (Ch107)
//
// First end-to-end demo for the GS/PCRTC stack. The point: drive
// the pipeline only through the GS-side surfaces a real PS2
// program's traffic eventually arrives at — i.e. the **post-GIF
// GS register stream** (the per-register A+D writes after
// GIFtag/PACKED unpacking) plus privileged-block MMIO writes for
// PMODE/DISPFB1/DISPLAY1 — and observe a real RGB frame come out
// the back. NO TB-direct vram_stub writes for the framebuffer —
// every visible pixel of the indexed FB has to arrive via the
// raster channel.
//
// **Scope caveat — what this TB does NOT cover**: the GIF
// front-end is bypassed. We drive `gs_stub.gif_reg_*` directly
// rather than feeding GIFtag PACKED A+D quadwords through
// `gif_packed_stub`. Closing that last bit of sideband — same
// demo but through GIFtag/PACKED A+D — is a future chapter.
// Until then, "driver-shaped" here means GS-side only:
// post-GIF register cadence + privileged MMIO + scanout
// observation.
//
// PSMT4 was picked for this demo because it exercises the
// densest stack of GS machinery in one shot:
//   - primitive observer + queue + raster pipeline (Ch76..Ch88)
//   - PSMT4 raster emit with byte-lane RMW (Ch106)
//   - vram_stub per-bit merge commit (Ch106)
//   - CLUT loader policy + partial CSA-window load (Ch99..Ch102)
//   - PSMT4 scanout with CLUT lookup at idx + (CSA<<4) (Ch103)
//   - PMODE/DISPFB1/DISPLAY1 register-driven scanout (Ch91..Ch93)
//
// The "program" sequence (everything driver-visible):
//
//   1. Stage 16 PSMCT32 palette entries in VRAM at CBP*256. In a
//      real PS2 this would be a separate DMA upload; modelling
//      DMA→GS image transfer is a future chapter so here the TB
//      writes the palette source directly into VRAM (NOT into
//      the framebuffer — only the palette source region).
//
//   2. Drive a post-GIF register stream (PRIM/FRAME_1/RGBAQ/XYZ2
//      writes via gs_stub.gif_reg_*) that paints a 4-quadrant
//      8×4 image into the PSMT4 framebuffer at FBP=0:
//        SPRITE A: (0,0)..(3,1) with R=0x5 → 4×2 region of idx 5
//        SPRITE B: (4,0)..(7,1) with R=0x7 → 4×2 region of idx 7
//        SPRITE C: (0,2)..(3,3) with R=0xA → 4×2 region of idx A
//        SPRITE D: (4,2)..(7,3) with R=0xC → 4×2 region of idx C
//      Each SPRITE emits 8 pixels (Ch106 RMW: low + high nibble
//      pairs across 4 bytes per row, two rows per SPRITE = 4 bytes).
//      4 SPRITEs × 8 pixels = 32 PSMT4 emits total.
//
//   3. Drive TEX0_1 with CBP=palette CBP, CPSM=PSMCT32, CSM=CSM2,
//      CSA=0, CLD=4. Loader walks 16 entries and writes
//      clut_stub[0..15] from VRAM. pcrtc.clut_csa wires from
//      gs_stub.tex0_1_csa_q so the lookup window settles on 0
//      the moment TEX0_1 commits.
//
//   4. Configure PMODE / DISPFB1 / DISPLAY1 via privileged-block
//      writes. DISPFB1.PSM=PSMT4 / FBP=0 / FBW=1 matches the
//      raster destination. PMODE.EN1=1 last so the screen lights
//      up only after the FB is finished.
//
//   5. Capture one full frame; assert each captured pixel reads
//      back as the CLUT-decoded RGB for its location:
//        - inside the 8×4 image: CLUT[quadrant_idx]
//        - outside (PCRTC is 16×8): CLUT[0] (because the raster
//          left those bytes untouched and vram_stub zero-init
//          means nibble=0)
//
// PASS criteria:
//   - 32 raster_pixel_emit pulses on the PSMT4 emit channel.
//   - All emits are PSM=0x14, BE=0001, mask ∈ {0x0F, 0xF0}.
//   - Loader fires once (1 rising edge of load_busy).
//   - The captured 16×8 frame matches the expected CLUT-decoded
//     RGB at every pixel.
//   - DE asserts at every captured pixel.

`timescale 1ns/1ps

module tb_gs_demo_psmt4_e2e;

    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 IO
    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                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  (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),
        .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 with TB-direct write port (palette source ONLY — the
    // PSMT4 framebuffer is filled via raster_pixel_emit).
    logic        vram_we;
    logic [31:0] vram_waddr;
    logic [31:0] vram_wdata;
    logic [3:0]  vram_wbe;
    logic [31:0] vram_raddr;
    logic [31:0] vram_rdata;
    logic [31:0] vram_raddr2;
    logic [31:0] vram_rdata2;

    // Mux between TB palette-staging writes and gs_stub raster
    // emit. tb_palette_active=1 selects the TB-driven preload.
    logic        tb_palette_active;
    logic        write_en_mux;
    logic [31:0] write_addr_mux;
    logic [31:0] write_data_mux;
    logic [3:0]  write_be_mux;
    logic [31:0] write_mask_mux;

    assign write_en_mux   = tb_palette_active ? vram_we    : raster_pixel_emit;
    assign write_addr_mux = tb_palette_active ? vram_waddr : raster_pixel_fb_addr_q;
    assign write_data_mux = tb_palette_active ? vram_wdata : raster_pixel_color_q[31:0];
    assign write_be_mux   = tb_palette_active ? vram_wbe   : raster_pixel_be_q;
    assign write_mask_mux = tb_palette_active ? 32'hFFFF_FFFF : raster_pixel_mask_q;

    vram_stub #(.BYTES(8192)) u_vram (
        .clk(clk), .rst_n(rst_n),
        .write_en  (write_en_mux),
        .write_addr(write_addr_mux),
        .write_data(write_data_mux),
        .write_be  (write_be_mux),
        .write_mask(write_mask_mux),
        .read_addr (vram_raddr),
        .read_data (vram_rdata),
        .read2_addr(vram_raddr2),
        .read2_data(vram_rdata2)
    );

    logic        clut_we;
    logic [7:0]  clut_widx;
    logic [31:0] clut_wdata;
    logic [7:0]  clut_ridx;
    logic [31:0] clut_rdata;

    clut_stub u_clut (
        .clk(clk), .rst_n(rst_n),
        .write_en  (clut_we),
        .write_idx (clut_widx),
        .write_data(clut_wdata),
        .read_idx  (clut_ridx),
        .read_data (clut_rdata)
    );

    logic loader_busy;

    clut_loader_stub u_loader (
        .clk(clk), .rst_n(rst_n),
        .tex0_wr_pulse  (tex0_1_wr_q),
        .tex0_cbp       (tex0_1_cbp_q),
        .tex0_cpsm      (tex0_1_cpsm_q),
        .tex0_csm       (tex0_1_csm_q),
        .tex0_csa       (tex0_1_csa_q),
        .tex0_cld       (tex0_1_cld_q),
        .vram_read_addr (vram_raddr2),
        .vram_read_data (vram_rdata2),
        .clut_write_en  (clut_we),
        .clut_write_idx (clut_widx),
        .clut_write_data(clut_wdata),
        .load_busy      (loader_busy)
    );

    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)
    ) u_pcrtc (
        .clk(clk), .rst_n(rst_n),
        .pmode_q       (pmode_q),
        .dispfb1_q     (dispfb1_q),
        .display1_q    (display1_q),
        .vram_read_addr(vram_raddr),
        .vram_read_data(vram_rdata),
        .clut_enable   (1'b1),
        .clut_csa      (tex0_1_csa_q),
        .clut_read_idx (clut_ridx),
        .clut_read_data(clut_rdata),
        .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)
    );

    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;
    int  loader_starts;
    int  raster_psmt4_emits;
    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;
        loader_starts      = 0;
        raster_psmt4_emits = 0;
        capture_armed      = 1'b0;
    end

    // Frame capture.
    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

    // Loader-start observer (rising edge of load_busy).
    logic loader_busy_d;
    always_ff @(posedge clk) begin
        if (!rst_n) begin
            loader_busy_d <= 1'b0;
        end else begin
            loader_busy_d <= loader_busy;
            if (loader_busy && !loader_busy_d)
                loader_starts <= loader_starts + 1;
        end
    end

    // PSMT4 raster-emit observer: assert PSM/BE/MASK invariants
    // on every emit.
    always_ff @(posedge clk) begin
        if (rst_n && raster_pixel_emit) begin
            raster_psmt4_emits <= raster_psmt4_emits + 1;
            if (raster_pixel_psm_q !== 6'h14) begin
                $error("emit %0d: psm=0x%02x (expected 0x14)",
                       raster_psmt4_emits, raster_pixel_psm_q);
                errors <= errors + 1;
            end
            if (raster_pixel_be_q !== 4'b0001) begin
                $error("emit %0d: be=%b (expected 4'b0001)",
                       raster_psmt4_emits, raster_pixel_be_q);
                errors <= errors + 1;
            end
            if (raster_pixel_mask_q !== 32'h0000_000F &&
                raster_pixel_mask_q !== 32'h0000_00F0) begin
                $error("emit %0d: mask=0x%08x (expected 0x0F or 0xF0)",
                       raster_psmt4_emits, raster_pixel_mask_q);
                errors <= errors + 1;
            end
        end
    end

    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

    task automatic palette_write32(input logic [31:0] addr,
                                   input logic [31:0] data);
        @(negedge clk);
        tb_palette_active = 1'b1;
        vram_we    = 1'b1;
        vram_waddr = addr;
        vram_wdata = data;
        vram_wbe   = 4'b1111;
        @(posedge clk);
        @(negedge clk);
        vram_we    = 1'b0;
        vram_waddr = 32'd0;
        vram_wdata = 32'd0;
        vram_wbe   = 4'b0000;
        tb_palette_active = 1'b0;
    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

    function automatic logic [63:0] tex0_pack(
        input logic [13:0] cbp,
        input logic [3:0]  cpsm,
        input logic        csm,
        input logic [4:0]  csa,
        input logic [2:0]  cld);
        logic [63:0] v;
        v = 64'd0;
        v[50:37] = cbp;
        v[54:51] = cpsm;
        v[55]    = csm;
        v[60:56] = csa;
        v[63:61] = cld;
        return v;
    endfunction

    // CLUT pattern: ABGR(0xFF, i+0x80, i+0x40, i). Same formula as
    // Ch97/Ch99/Ch102/Ch104 — an entry's R = i, G = i+0x40,
    // B = i+0x80 (8-bit truncation), A = 0xFF.
    function automatic logic [31:0] pattern_a(input logic [7:0] i);
        logic [7:0] r8, g8, b8, a8;
        r8 = i; g8 = i + 8'h40; b8 = i + 8'h80; a8 = 8'hFF;
        return {a8, b8, g8, r8};
    endfunction

    // Push a flat-color SPRITE through the GIF reg port.
    task automatic gif_sprite(input logic [7:0]  rgbaq_r,
                              input logic [11:0] x0,
                              input logic [11:0] y0,
                              input logic [11:0] x1,
                              input logic [11:0] y1);
        logic [63:0] rgbaq_payload;
        rgbaq_payload = {32'd0, 8'hCC, 8'hBB, 8'hAA, rgbaq_r};
        drive_reg(R_PRIM,    PRIM_SPRITE);
        drive_reg(R_FRAME_1, FRAME_1_PSMT4);
        drive_reg(R_RGBAQ,   rgbaq_payload);
        drive_reg(R_XYZ2,    xyz2_data(x0, y0));
        drive_reg(R_XYZ2,    xyz2_data(x1, y1));
    endtask

    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 [7:0] GIF_TEX0_1 = 8'h06;

    localparam logic [63:0] PRIM_SPRITE   = 64'd6;
    // FRAME_1: FBP=0 / FBW=1 / PSM=PSMT4 (0x14).
    localparam logic [63:0] FRAME_1_PSMT4 = 64'h0000_0000_1401_0000;

    // Privileged-block offsets.
    localparam logic [15:0] PMODE_OFF    = 16'h0000;
    localparam logic [15:0] DISPFB1_OFF  = 16'h0070;
    localparam logic [15:0] DISPLAY1_OFF = 16'h0080;

    localparam int          PALETTE_CBP  = 4;          // VRAM offset = 4*256 = 1024
    localparam logic [4:0]  CSA          = 5'd0;

    // DISPFB1: FBP=0, FBW=1, PSM=PSMT4. PSM at bit 15.
    localparam logic [63:0] DISPFB1_VAL  = 64'h0000_0000_000A_0200;
    // DISPLAY1.DW=H_ACTIVE-1, DH=V_ACTIVE-1, DX/DY=0, MAGH/MAGV=0.
    localparam logic [63:0] DISPLAY1_VAL =
          (64'(PCRTC_H_ACTIVE - 1) << 32)
        | (64'(PCRTC_V_ACTIVE - 1) << 44);
    localparam logic [63:0] PMODE_EN1    = 64'h0000_0000_0000_0001;

    // Image quadrants. SPRITE coords use closed bounds (x0..x1
    // inclusive). 4 SPRITEs × 8 pixels = 32 emits.
    typedef struct packed {
        logic [7:0]  r;
        logic [11:0] x0; logic [11:0] y0;
        logic [11:0] x1; logic [11:0] y1;
    } quad_t;

    function automatic quad_t mk_quad(input logic [7:0] r,
                                      input int x0, input int y0,
                                      input int x1, input int y1);
        quad_t q;
        q.r  = r;
        q.x0 = 12'(x0); q.y0 = 12'(y0);
        q.x1 = 12'(x1); q.y1 = 12'(y1);
        return q;
    endfunction

    // Per-quadrant index value: read out R[3:0] (the PSMT4 idx).
    function automatic logic [3:0] quad_idx_at(input int x, input int y);
        // Quadrant layout:
        //   (0..3, 0..1) → 0x5
        //   (4..7, 0..1) → 0x7
        //   (0..3, 2..3) → 0xA
        //   (4..7, 2..3) → 0xC
        if (y <= 1 && x <= 3) return 4'h5;
        if (y <= 1 && x >= 4 && x <= 7) return 4'h7;
        if (y >= 2 && y <= 3 && x <= 3) return 4'hA;
        if (y >= 2 && y <= 3 && x >= 4 && x <= 7) return 4'hC;
        return 4'h0;  // outside the 8×4 image
    endfunction

    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_we           = 1'b0;
        vram_waddr        = 32'd0;
        vram_wdata        = 32'd0;
        vram_wbe          = 4'b0000;
        tb_palette_active = 1'b0;

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

        // ------------------------------------------------------------
        // Step 1 — stage palette source in VRAM at PALETTE_CBP*256.
        // (Real PS2: a DMA upload would do this. Modelling DMA→GS
        // image transfer is out of scope here.)
        // ------------------------------------------------------------
        for (int i = 0; i < 16; i++) begin
            logic [7:0]  ii;
            logic [31:0] addr;
            ii   = i[7:0];
            addr = 32'(PALETTE_CBP * 256) + 32'(i * 4);
            palette_write32(addr, pattern_a(ii));
        end

        // ------------------------------------------------------------
        // Step 2 — drive a post-GIF register stream (per-reg A+D
        // writes via gs_stub.gif_reg_*; gif_packed_stub /
        // GIFtag-PACKED is bypassed at this scope) that paints
        // the PSMT4 framebuffer at FBP=0 via the raster channel.
        // 4 SPRITEs of 4×2 = 32 pixels emitted via Ch106 RMW.
        // ------------------------------------------------------------
        gif_sprite(8'h05, 12'd0, 12'd0, 12'd3, 12'd1);  // quadrant TL
        gif_sprite(8'h07, 12'd4, 12'd0, 12'd7, 12'd1);  // quadrant TR
        gif_sprite(8'h0A, 12'd0, 12'd2, 12'd3, 12'd3);  // quadrant BL
        gif_sprite(8'h0C, 12'd4, 12'd2, 12'd7, 12'd3);  // quadrant BR
        drive_idle();

        // Wait for raster + draining pipeline to fully settle.
        wait (raster_active == 1'b1);
        wait (raster_active == 1'b0);
        repeat (10) @(posedge clk);

        // ------------------------------------------------------------
        // Step 3 — TEX0+CLD palette load into clut_stub[0..15].
        // ------------------------------------------------------------
        drive_reg(GIF_TEX0_1,
                  tex0_pack(14'(PALETTE_CBP), 4'd0, 1'b1, CSA, 3'd4));
        drive_idle();
        wait (loader_busy == 1'b1);
        wait (loader_busy == 1'b0);
        repeat (4) @(posedge clk);

        // ------------------------------------------------------------
        // Step 4 — bring up scanout. PMODE.EN1 last so any
        // partial-load state is gone before the screen lights up.
        // ------------------------------------------------------------
        drive_priv(DISPFB1_OFF,  DISPFB1_VAL);
        drive_priv(DISPLAY1_OFF, DISPLAY1_VAL);
        drive_priv(PMODE_OFF,    PMODE_EN1);

        @(posedge u_pcrtc.end_of_frame);
        @(posedge clk);
        capture_armed = 1'b1;
        @(posedge u_pcrtc.end_of_frame);
        @(posedge clk);
        capture_armed = 1'b0;

        // ------------------------------------------------------------
        // Step 5 — full-frame assertion.
        // ------------------------------------------------------------
        for (int y = 0; y < PCRTC_V_ACTIVE; y++) begin
            for (int x = 0; x < PCRTC_H_ACTIVE; x++) begin
                logic [3:0]  expected_idx;
                logic [7:0]  effective_idx;
                logic [31:0] entry;
                logic [7:0]  er, eg, eb;

                expected_idx  = quad_idx_at(x, y);
                effective_idx = {4'd0, expected_idx} + 8'(CSA) * 8'd16;
                entry         = pattern_a(effective_idx);
                er = entry[7:0];
                eg = entry[15:8];
                eb = entry[23:16];

                if (!cap_de[y][x]) begin
                    $error("(%0d,%0d) DE never asserted", x, y);
                    errors = errors + 1;
                end
                if (cap_r[y][x] !== er || cap_g[y][x] !== eg || cap_b[y][x] !== eb) begin
                    $error("(%0d,%0d) got (%02x,%02x,%02x) expected (%02x,%02x,%02x) idx=0x%01x",
                           x, y, cap_r[y][x], cap_g[y][x], cap_b[y][x],
                           er, eg, eb, expected_idx);
                    errors = errors + 1;
                end
            end
        end

        // ------------------------------------------------------------
        // Aggregate counts.
        // ------------------------------------------------------------
        if (raster_pixel_emit_count != 32'd32) begin
            $error("expected 32 PSMT4 emits (4 SPRITEs × 8 px), got %0d",
                   raster_pixel_emit_count);
            errors = errors + 1;
        end
        if (raster_psmt4_emits != 32) begin
            $error("PSMT4 emit observer count=%0d (expected 32)", raster_psmt4_emits);
            errors = errors + 1;
        end
        if (loader_starts != 1) begin
            $error("loader fired %0d times (expected 1)", loader_starts);
            errors = errors + 1;
        end
        if (raster_overflow) begin
            $error("raster_overflow asserted — primitive queue dropped");
            errors = errors + 1;
        end
        if (raster_degenerate) begin
            $error("raster_degenerate asserted — unexpected for SPRITE rect-fills");
            errors = errors + 1;
        end

        // Channel-isolation is locked architecturally by the
        // per-emit observer above (mask ∈ {0x0F, 0xF0} on every
        // emit — only the targeted nibble of R[3:0] is ever
        // committed; G/B/A bytes simply never reach vram_stub).
        // No post-hoc leak sweep needed.

        $display("[tb_gs_demo_psmt4_e2e] sprites=4 emits=%0d loader_starts=%0d frame=%0dx%0d",
                 raster_pixel_emit_count, loader_starts,
                 PCRTC_H_ACTIVE, PCRTC_V_ACTIVE);

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

    initial begin
        #20000000;
        $error("[tb_gs_demo_psmt4_e2e] timeout");
        $finish;
    end

endmodule : tb_gs_demo_psmt4_e2e
