// retroDE_ps2 — tb_gs_clut_load_csa_window (Ch102)
//
// Locks the contract for CLD=4 partial CSA-window load. The
// loader always fires on CLD=4 (gated by CSM=1 and
// CPSM ∈ {PSMCT32, PSMCT16}) but writes only 16 entries to
// clut_stub at base CSA*16, leaving the other 240 entries
// untouched. Used by real PS2 software when multiple small
// 16-entry palettes share one 256-entry staging area.
//
// Test plan (no scanout — TB-side `clut_we` snoop captures the
// last write per index, so we can compare before/after Phase 2):
//
//   Phase 1 — full PSMCT32 load (CLD=1) from CBP=4 with
//             pattern_a(idx). After this, captured_clut_wdata[i]
//             holds pattern_a(i) for all i in 0..255.
//
//   Phase 2 — partial CSA-window load (CLD=4) at CSA=2 → window
//             = idx 32..47. Source VRAM at CBP=8 holds 16 entries
//             of pattern_b(i) for i = 0..15. After this, the
//             snoop sees:
//               i ∈ 0..31    → pattern_a(i)        (untouched)
//               i ∈ 32..47   → pattern_b(i - 32)   (overwritten)
//               i ∈ 48..255  → pattern_a(i)        (untouched)
//
//   Phase 3 — Ch102-audit high-CSA wrap test. CLD=4 with CSA=16
//             (5-bit). load_csa_base = {16, 4'd0} = 9'h100 →
//             8-bit truncation = 0x00. So the window wraps to
//             idx 0..15. We stage pattern_c(i) at CBP=12 and
//             expect the captured array at i=0..15 to switch to
//             pattern_c(i) while phase-2's window (32..47) and
//             everything else stay where they were.
//
//   Phase 4 — Ch102-audit CT16 partial test. CLD=4 with
//             CPSM=PSMCT16 (=2) and CSA=10 → window = idx
//             160..175. Source VRAM at CBP=16 holds 16 RGB5A1
//             entries (2 bytes each) for pattern_d. After the
//             load, captured_clut_wdata[160..175] should match
//             the loader's 5→8 bit-replicate decode of pattern_d.
//             Other entries (0..15 from phase 3, 32..47 from
//             phase 2, the rest from phase 1) untouched.
//
//   All four phases also assert their per-phase write counts
//   (256 for phase 1, 16 each for phases 2/3/4).

`timescale 1ns/1ps

module tb_gs_clut_load_csa_window;

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

    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 [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_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 for staging both palettes.
    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;

    vram_stub #(.BYTES(8192)) u_vram (
        .clk(clk), .rst_n(rst_n),
        .write_en  (vram_we),
        .write_addr(vram_waddr),
        .write_data(vram_wdata),
        .write_be  (vram_wbe),
        .write_mask(32'hFFFF_FFFF),
        .read_addr (vram_raddr),
        .read_data (vram_rdata),
        .read2_addr(vram_raddr2),
        .read2_data(vram_rdata2)
    );
    assign vram_raddr = 32'd0;

    // CLUT — written by the loader; we don't actually read it
    // since the captured_clut_wdata snoop array IS the test.
    logic        clut_we;
    logic [7:0]  clut_widx;
    logic [31:0] clut_wdata;

    clut_stub u_clut (
        .clk(clk), .rst_n(rst_n),
        .write_en  (clut_we),
        .write_idx (clut_widx),
        .write_data(clut_wdata),
        .read_idx  (8'd0),
        .read_data ()
    );

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

    // Snoop every clut write so we can verify the partial-load
    // window honors the "preserve other entries" promise.
    logic [31:0] captured_clut_wdata [0:255];
    int          phase2_write_count;
    int          phase3_write_count;
    int          phase4_write_count;

    initial begin
        for (int i = 0; i < 256; i++) captured_clut_wdata[i] = 32'd0;
        phase2_write_count = 0;
        phase3_write_count = 0;
        phase4_write_count = 0;
    end

    // Per-phase active flags so the write counter only ticks for
    // the in-progress partial load.
    bit phase2_active, phase3_active, phase4_active;
    initial begin
        phase2_active = 1'b0;
        phase3_active = 1'b0;
        phase4_active = 1'b0;
    end

    always_ff @(posedge clk) begin
        if (rst_n && clut_we) begin
            captured_clut_wdata[clut_widx] <= clut_wdata;
            if (phase2_active) phase2_write_count <= phase2_write_count + 1;
            if (phase3_active) phase3_write_count <= phase3_write_count + 1;
            if (phase4_active) phase4_write_count <= phase4_write_count + 1;
        end
    end

    int errors;
    initial errors = 0;

    // Helper: pattern_a is the full-load (Phase 1) pattern.
    // pattern_b is the partial-load (Phase 2) pattern. They must
    // produce different values so we can tell which phase wrote
    // each entry.
    function automatic logic [31:0] pattern_a(input logic [7:0] i);
        // ABGR: A=0xFF, B=i+0x80, G=i+0x40, R=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

    // Pattern B uses i directly as a flat-ish "swapped" pattern
    // distinct from pattern_a so a stale pattern_a entry would
    // be visibly wrong:
    //   R = i ^ 8'hAA   (XOR with mask)
    //   G = i ^ 8'h55
    //   B = i ^ 8'hCC
    //   A = 0x80
    function automatic logic [31:0] pattern_b(input logic [7:0] i);
        logic [7:0] r8, g8, b8, a8;
        r8 = i ^ 8'hAA;
        g8 = i ^ 8'h55;
        b8 = i ^ 8'hCC;
        a8 = 8'h80;
        return {a8, b8, g8, r8};
    endfunction

    // Pattern C — distinct again, used for the high-CSA wrap
    // phase (CSA=16 → base wraps to 0).
    function automatic logic [31:0] pattern_c(input logic [7:0] i);
        logic [7:0] r8, g8, b8, a8;
        r8 = i + 8'h10;
        g8 = i + 8'h20;
        b8 = i + 8'h30;
        a8 = 8'h40;
        return {a8, b8, g8, r8};
    endfunction

    // Pattern D — RGB5A1 source for the CT16 partial phase. The
    // packed value goes into VRAM; the loader unpacks via 5→8
    // bit-replicate. r5/g5/b5 follow distinct per-index ramps so
    // the unpacked PSMCT32 entries are visibly different from
    // pattern_a/b/c.
    function automatic logic [4:0] pat_d_r5(input logic [7:0] i);
        logic [7:0] s; s = i + 8'd3;  return s[4:0];
    endfunction
    function automatic logic [4:0] pat_d_g5(input logic [7:0] i);
        logic [7:0] s; s = i + 8'd11; return s[4:0];
    endfunction
    function automatic logic [4:0] pat_d_b5(input logic [7:0] i);
        logic [7:0] s; s = i + 8'd19; return s[4:0];
    endfunction
    function automatic logic [15:0] pattern_d_pack(input logic [7:0] i);
        // {a1=1, b5, g5, r5}
        return {1'b1, pat_d_b5(i), pat_d_g5(i), pat_d_r5(i)};
    endfunction
    function automatic logic [31:0] pattern_d_decoded(input logic [7:0] i);
        logic [4:0] r5, g5, b5;
        logic [7:0] r8, g8, b8, a8;
        r5 = pat_d_r5(i);
        g5 = pat_d_g5(i);
        b5 = pat_d_b5(i);
        r8 = {r5, r5[4:2]};
        g8 = {g5, g5[4:2]};
        b8 = {b5, b5[4:2]};
        a8 = 8'hFF;
        return {a8, b8, g8, r8};
    endfunction

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

    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

    localparam logic [7:0]  GIF_TEX0_1 = 8'h06;

    localparam int          PHASE1_CBP = 4;      // CBP*256 = 1024
    localparam int          PHASE2_CBP = 8;      // CBP*256 = 2048
    localparam logic [4:0]  PHASE2_CSA = 5'd2;   // window base = 32, len 16
    localparam int          PHASE2_BASE = 32;
    localparam int          WINDOW_LEN  = 16;

    // Phase 3 — high-CSA wrap. CSA=16 means {16, 4'd0} = 9'h100,
    // 8-bit truncate = 0 → window base = 0.
    localparam int          PHASE3_CBP  = 12;    // CBP*256 = 3072
    localparam logic [4:0]  PHASE3_CSA  = 5'd16;
    localparam int          PHASE3_BASE = 0;     // 16*16 wraps mod 256

    // Phase 4 — CT16 partial. CSA=10 → window base = 160.
    localparam int          PHASE4_CBP  = 16;    // CBP*256 = 4096
    localparam logic [4:0]  PHASE4_CSA  = 5'd10;
    localparam int          PHASE4_BASE = 160;

    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;

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

        // ---- Phase 1: stage full-256-entry pattern_a at CBP=4, full load.
        for (int i = 0; i < 256; i++) begin
            logic [31:0] addr;
            logic [7:0]  ii;
            ii   = i[7:0];
            addr = 32'(PHASE1_CBP * 256) + 32'(i * 4);
            vram_write32(addr, pattern_a(ii), 4'b1111);
        end
        drive_reg(GIF_TEX0_1, tex0_pack(14'(PHASE1_CBP), 4'd0, 1'b1, 5'd0, 3'd1));
        drive_idle();
        wait (loader_busy == 1'b1);
        wait (loader_busy == 1'b0);
        repeat (4) @(posedge clk);

        // After Phase 1 every captured_clut_wdata[i] should
        // hold pattern_a(i). Spot-check two:
        if (captured_clut_wdata[8'h00] !== pattern_a(8'h00)) begin
            $error("phase1: CLUT[0] = 0x%08x (expected 0x%08x)",
                   captured_clut_wdata[8'h00], pattern_a(8'h00));
            errors = errors + 1;
        end
        if (captured_clut_wdata[8'hFE] !== pattern_a(8'hFE)) begin
            $error("phase1: CLUT[0xFE] = 0x%08x (expected 0x%08x)",
                   captured_clut_wdata[8'hFE], pattern_a(8'hFE));
            errors = errors + 1;
        end

        // ---- Phase 2: stage 16 pattern_b entries at CBP=8 (only
        // those 16 are needed, since CLD=4 reads 16). Then
        // trigger CLD=4 with CSA=2 → window = idx 32..47.
        for (int i = 0; i < WINDOW_LEN; i++) begin
            logic [31:0] addr;
            logic [7:0]  ii;
            ii   = i[7:0];
            addr = 32'(PHASE2_CBP * 256) + 32'(i * 4);
            vram_write32(addr, pattern_b(ii), 4'b1111);
        end

        phase2_active = 1'b1;
        drive_reg(GIF_TEX0_1, tex0_pack(14'(PHASE2_CBP), 4'd0, 1'b1, PHASE2_CSA, 3'd4));
        drive_idle();
        wait (loader_busy == 1'b1);
        wait (loader_busy == 1'b0);
        repeat (4) @(posedge clk);
        phase2_active = 1'b0;

        // Phase 2 must have written EXACTLY 16 CLUT entries.
        if (phase2_write_count != WINDOW_LEN) begin
            $error("phase2: write_count=%0d (expected %0d, partial CSA window)",
                   phase2_write_count, WINDOW_LEN);
            errors = errors + 1;
        end

        // Now verify the captured array reflects the partial
        // overlay: indices 0..31 + 48..255 still pattern_a;
        // indices 32..47 now pattern_b(idx - 32).
        for (int i = 0; i < 256; i++) begin
            logic [7:0]  idx_8;
            logic [31:0] got, expected;
            idx_8 = i[7:0];
            got   = captured_clut_wdata[idx_8];
            if (i >= PHASE2_BASE && i < PHASE2_BASE + WINDOW_LEN) begin
                expected = pattern_b(8'(i - PHASE2_BASE));
            end else begin
                expected = pattern_a(idx_8);
            end
            if (got !== expected) begin
                $error("phase2 CLUT[0x%02x] = 0x%08x (expected 0x%08x)",
                       idx_8, got, expected);
                errors = errors + 1;
            end
        end

        // ---- Phase 3: high-CSA wrap. Stage 16 pattern_c
        // entries at CBP=12, drive CLD=4 with CSA=16 (= 0x10),
        // expect window to wrap to base 0.
        for (int i = 0; i < WINDOW_LEN; i++) begin
            logic [31:0] addr;
            logic [7:0]  ii;
            ii   = i[7:0];
            addr = 32'(PHASE3_CBP * 256) + 32'(i * 4);
            vram_write32(addr, pattern_c(ii), 4'b1111);
        end

        phase3_active = 1'b1;
        drive_reg(GIF_TEX0_1, tex0_pack(14'(PHASE3_CBP), 4'd0, 1'b1, PHASE3_CSA, 3'd4));
        drive_idle();
        wait (loader_busy == 1'b1);
        wait (loader_busy == 1'b0);
        repeat (4) @(posedge clk);
        phase3_active = 1'b0;

        if (phase3_write_count != WINDOW_LEN) begin
            $error("phase3: write_count=%0d (expected %0d, CSA=16 wraps)",
                   phase3_write_count, WINDOW_LEN);
            errors = errors + 1;
        end

        // After phase 3, expected captured layout:
        //   0..15  → pattern_c(i)        (new — wrap from CSA=16)
        //   16..31 → pattern_a(i)        (still phase 1)
        //   32..47 → pattern_b(i - 32)   (still phase 2)
        //   48..255 → pattern_a(i)
        for (int i = 0; i < 256; i++) begin
            logic [7:0]  idx_8;
            logic [31:0] got, expected;
            idx_8 = i[7:0];
            got   = captured_clut_wdata[idx_8];
            if (i >= PHASE3_BASE && i < PHASE3_BASE + WINDOW_LEN) begin
                expected = pattern_c(8'(i - PHASE3_BASE));
            end else if (i >= PHASE2_BASE && i < PHASE2_BASE + WINDOW_LEN) begin
                expected = pattern_b(8'(i - PHASE2_BASE));
            end else begin
                expected = pattern_a(idx_8);
            end
            if (got !== expected) begin
                $error("phase3 CLUT[0x%02x] = 0x%08x (expected 0x%08x)",
                       idx_8, got, expected);
                errors = errors + 1;
            end
        end

        // ---- Phase 4: CT16 partial. Stage 16 RGB5A1 entries at
        // CBP=16 (16 × 2 = 32 bytes, two entries per 32-bit
        // write), drive CLD=4 with CPSM=2 + CSA=10. Loader must
        // unpack each 16-bit packed value via 5→8 bit-replicate
        // and write 16 PSMCT32 ABGR entries to clut_stub at base
        // 160.
        for (int i = 0; i < WINDOW_LEN; i = i + 2) begin
            logic [15:0] e0, e1;
            logic [31:0] addr;
            logic [31:0] data;
            logic [7:0]  ii0, ii1;
            int          i_plus_1;
            ii0 = i[7:0];
            i_plus_1 = i + 1;
            ii1 = i_plus_1[7:0];
            e0  = pattern_d_pack(ii0);
            e1  = pattern_d_pack(ii1);
            data = {e1, e0};
            addr = 32'(PHASE4_CBP * 256) + 32'(i * 2);
            vram_write32(addr, data, 4'b1111);
        end

        phase4_active = 1'b1;
        drive_reg(GIF_TEX0_1, tex0_pack(14'(PHASE4_CBP), 4'd2, 1'b1, PHASE4_CSA, 3'd4));
        drive_idle();
        wait (loader_busy == 1'b1);
        wait (loader_busy == 1'b0);
        repeat (4) @(posedge clk);
        phase4_active = 1'b0;

        if (phase4_write_count != WINDOW_LEN) begin
            $error("phase4: write_count=%0d (expected %0d, CT16 partial)",
                   phase4_write_count, WINDOW_LEN);
            errors = errors + 1;
        end

        // After phase 4, expected layout:
        //   0..15    → pattern_c (phase 3)
        //   16..31   → pattern_a (phase 1)
        //   32..47   → pattern_b (phase 2)
        //   48..159  → pattern_a (phase 1)
        //   160..175 → pattern_d_decoded (phase 4 — 5→8 unpack)
        //   176..255 → pattern_a (phase 1)
        for (int i = 0; i < 256; i++) begin
            logic [7:0]  idx_8;
            logic [31:0] got, expected;
            idx_8 = i[7:0];
            got   = captured_clut_wdata[idx_8];
            if      (i >= PHASE3_BASE && i < PHASE3_BASE + WINDOW_LEN)
                expected = pattern_c(8'(i - PHASE3_BASE));
            else if (i >= PHASE2_BASE && i < PHASE2_BASE + WINDOW_LEN)
                expected = pattern_b(8'(i - PHASE2_BASE));
            else if (i >= PHASE4_BASE && i < PHASE4_BASE + WINDOW_LEN)
                expected = pattern_d_decoded(8'(i - PHASE4_BASE));
            else
                expected = pattern_a(idx_8);
            if (got !== expected) begin
                $error("phase4 CLUT[0x%02x] = 0x%08x (expected 0x%08x)",
                       idx_8, got, expected);
                errors = errors + 1;
            end
        end

        $display("[tb_gs_clut_load_csa_window] phase2(CT32 CSA=2)=%0d phase3(CT32 CSA=16 wrap)=%0d phase4(CT16 CSA=10)=%0d writes; 208 entries preserved end-to-end",
                 phase2_write_count, phase3_write_count, phase4_write_count);

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

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

endmodule : tb_gs_clut_load_csa_window
