// retroDE_ps2 — tb_gs_psmt8_alpha_sprite (Ch347 pre-fit brick)
//
// Extends the Ch344 textured-alpha SPRITE path from PSMCT32 to PSMT8 (CLUT-indexed):
//   PRIM.TME=1 + TEX0(PSM=PSMT8) + UV + SPRITE + ABE source-over
//     -> gs_texture_unit fetches an 8-bit INDEX from VRAM, looks it up in clut_stub
//        -> PSMCT32 ABGR texel (Ch296 path), output on s1_tex_color
//     -> the Ch344 half-rate texel+dest+blend path consumes s1_tex_color exactly as for PSMCT32.
// Gated by gs_stub param SPRITE_TEX_ALPHA_CLUT=1 (default 0 keeps PSMCT32 byte-identical).
//
// Uses the BOARD read model: 1-cycle REGISTERED read2 (TEX/FB/Z_RD_REGISTERED=1). A combinational-read
// TB would be a FALSE GREEN — the registered-read byte-select alignment is exactly what could skew the
// CLUT-decoded texel vs the frozen-beat capture (the trap that bit Ch344's first cut).
//
// PROOFs (Codex's pre-fit set):
//   1 index fetch — distinct per-pixel index pattern; a byte-lane error diverges the color.
//   2 CLUT map    — index -> clut_stub[index] -> ABGR (programmed palette).
//   3 texel ALPHA drives blend — palette entries have DISTINCT alphas (incl. a=0 transparent -> dest
//                  shows through); checked against an independent source-over reference.
//   4 no read2 collision — tex_rd_en and fb_rd_en never co-fire (monitor asserts).
//   5 PSMCT32 stays green — a PSMCT32 control sprite in the SAME build (param ON) still blends correctly.

`timescale 1ns/1ps

module tb_gs_psmt8_alpha_sprite;

    localparam logic [5:0] PSMCT32 = 6'h00;
    localparam logic [5:0] PSMT8   = 6'h13;
    localparam logic [5:0] PSMT4   = 6'h14;

    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 [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_pixel_abe_q;
    logic [16:0]         raster_pixel_alpha_q;
    logic                raster_active;
    logic                raster_overflow;
    logic                raster_fifo_full;
    logic                raster_degenerate;
    logic                tex_rd_en;
    logic [31:0]         tex_rd_addr;
    logic [31:0]         tex_rd_data;
    logic                fb_rd_en;
    logic [31:0]         fb_rd_addr;
    logic [31:0]         fb_rd_data;
    logic [7:0]          clut_rd_idx;
    logic [31:0]         clut_rd_data;
    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 #(.SPRITE_TEX_ALPHA(1'b1), .SPRITE_TEX_ALPHA_CLUT(1'b1),
              .TEX_RD_REGISTERED(1'b1), .FB_RD_REGISTERED(1'b1), .Z_RD_REGISTERED(1'b1)) 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_pixel_abe_q(raster_pixel_abe_q),
        .raster_pixel_alpha_q(raster_pixel_alpha_q),
        .raster_active(raster_active),
        .raster_overflow(raster_overflow),
        .raster_fifo_full(raster_fifo_full),
        .raster_degenerate(raster_degenerate),
        .tex_rd_en(tex_rd_en),
        .tex_rd_addr(tex_rd_addr),
        .tex_rd_data(tex_rd_data),
        .fb_rd_en(fb_rd_en),
        .fb_rd_addr(fb_rd_addr),
        .fb_rd_data(fb_rd_data),
        .clut_rd_idx(clut_rd_idx),
        .clut_rd_data(clut_rd_data),
        .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)
    );

    // ---- CLUT (external, board wrapper instantiates it; here the TB programs it directly) ----
    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(),
        .tex_read_idx (clut_rd_idx), .tex_read_data(clut_rd_data)
    );
    task automatic clut_write(input logic [7:0] idx, input logic [31:0] data);
        @(negedge clk); clut_we = 1'b1; clut_widx = idx; clut_wdata = data;
        @(negedge clk); clut_we = 1'b0;
    endtask

    // ---- VRAM (port0 raster write + readback, read2 = shared texel/dest) ----
    logic [31:0] vram_read_addr, vram_read_data;
    localparam int VRAM_BYTES = 8192;
    vram_stub #(.BYTES(VRAM_BYTES)) u_vram (
        .clk(clk), .rst_n(rst_n),
        .write_en  (raster_pixel_emit),
        .write_addr(raster_pixel_fb_addr_q),
        .write_data(raster_pixel_color_q[31:0]),
        .write_be  (raster_pixel_be_q),
        .write_mask(raster_pixel_mask_q),
        .read_addr (vram_read_addr),
        .read_data (vram_read_data),
        .read2_addr(rd2_addr),
        .read2_data(rd2_data)
    );
    logic [31:0] rd2_addr, rd2_data;
    assign rd2_addr = fb_rd_en ? fb_rd_addr : tex_rd_addr;
    logic [31:0] rd2_data_reg;
    always_ff @(posedge clk) rd2_data_reg <= rd2_data;   // 1-cycle registered read2 (board model)
    assign tex_rd_data = rd2_data_reg;
    assign fb_rd_data  = rd2_data_reg;

    // ---- GIF register numbers ----
    localparam logic [7:0] R_PRIM=8'h00, R_RGBAQ=8'h01, R_UV=8'h03, R_XYZ2=8'h05,
                           R_TEX0_1=8'h06, R_ALPHA_1=8'h42, R_FRAME_1=8'h4C;
    localparam logic [63:0] PRIM_SPR_TEX_ABE = 64'd6 | (64'd1<<4) | (64'd1<<6);
    localparam logic [63:0] ALPHA_SRCOVER    = 64'h0000_0000_0000_0044;
    localparam logic [63:0] ALPHA_DARKEN     = 64'h0000_0000_0000_0046;
    localparam logic [63:0] FRAME_1_FB0      = 64'h0000_0000_0001_0000;   // FBP=0 FBW=1 PSMCT32

    localparam int SPRITE_W = 4, SPRITE_H = 4;
    localparam int FB0_STRIDE = 64*4;
    localparam logic [63:0] TINT_RGBAQ = 64'h0000_0000_10FF_8040;  // MODULATE: A*0x10 R*0x40 G*0x80 B*0xFF
    localparam logic [31:0] BG_PIXEL   = 32'hFF20_2020;

    // ---- Indexed textures plus PSMCT32 control ----
    localparam logic [63:0] TEX0_T8  = 64'd8  | (64'd1<<14) | (64'(PSMT8)<<20)
                                      | (64'd2<<26) | (64'd2<<30) | (64'd1<<34);   // TCC=1 -> texel(CLUT) alpha
    localparam logic [63:0] TEX0_T4  = 64'd24 | (64'd1<<14) | (64'(PSMT4)<<20)
                                      | (64'd2<<26) | (64'd2<<30) | (64'd1<<34);
    localparam logic [63:0] TEX0_C32 = 64'd16 | (64'd1<<14) | (64'd0<<20)
                                      | (64'd2<<26) | (64'd2<<30) | (64'd1<<34);
    localparam logic [31:0] T8_BASE  = 32'd8  * 32'd256;   // 2048
    localparam logic [31:0] C32_BASE = 32'd16 * 32'd256;   // 4096
    localparam logic [31:0] T4_BASE  = 32'd24 * 32'd256;   // 6144
    localparam int          ROW_TEXELS = 64;               // TBW*64

    // distinct index per pixel so a byte-lane error is caught; cycles all 4 palette entries incl. idx0.
    function automatic logic [7:0] idx_pattern(input int x, input int y);
        return 8'((x + y) & 3);
    endfunction
    // palette: DISTINCT alpha per index (0 transparent, 0x40, 0x80, 0x20) + distinct RGB. ABGR = {A,B,G,R}.
    function automatic logic [31:0] palette(input logic [7:0] i);
        case (i)
            8'd0:    return 32'h00FF_FFFF; // a=0   white  -> fully transparent (dest shows through)
            8'd1:    return 32'h4000_00FF; // a=40  red
            8'd2:    return 32'h8000_FF00; // a=80  green
            default: return 32'h20FF_0000; // a=20  blue
        endcase
    endfunction
    // PSMCT32 control texel(x,y): fixed alpha 0x40, distinct RGB.
    function automatic logic [31:0] c32_texel(input int x, input int y);
        return {8'h40, 8'hC0, 8'(8'h40 + y*16), 8'(8'h80 + x*16)};
    endfunction

    // ---- source-over reference (matches gs_stub ta_mod8 + gs_alpha_blend exactly) ----
    function automatic logic [7:0] sw_mod8(input logic [7:0] t, input logic [7:0] c);
        logic [15:0] p; begin p = ({8'd0,t} * {8'd0,c}) >> 7; sw_mod8 = (p > 16'd255) ? 8'hFF : p[7:0]; end
    endfunction
    function automatic logic [7:0] sw_blend(input logic [7:0] cs, input logic [7:0] cd, input logic [7:0] as);
        logic [7:0] ae; logic signed [17:0] d, pr, sh, su;
        begin
            ae = (as > 8'd128) ? 8'd128 : as;
            d  = $signed({2'b00, cs}) - $signed({2'b00, cd});
            pr = d * $signed({1'b0, ae});  sh = pr >>> 7;  su = sh + $signed({10'd0, cd});
            sw_blend = (su < 0) ? 8'd0 : (su > 18'sd255) ? 8'd255 : su[7:0];
        end
    endfunction
    // expected sprite pixel from an arbitrary ABGR texel (CLUT entry or PSMCT32 word).
    function automatic logic [31:0] expected_from_texel(input logic [31:0] t);
        logic [7:0] cs_r, cs_g, cs_b, cs_a;
        begin
            cs_r = sw_mod8(t[7:0],   TINT_RGBAQ[7:0]);
            cs_g = sw_mod8(t[15:8],  TINT_RGBAQ[15:8]);
            cs_b = sw_mod8(t[23:16], TINT_RGBAQ[23:16]);
            cs_a = sw_mod8(t[31:24], TINT_RGBAQ[31:24]);
            expected_from_texel = {cs_a,
                                   sw_blend(cs_b, BG_PIXEL[23:16], cs_a),
                                   sw_blend(cs_g, BG_PIXEL[15:8],  cs_a),
                                   sw_blend(cs_r, BG_PIXEL[7:0],   cs_a)};
        end
    endfunction

    // ---- drive helpers ----
    task automatic step_drive(input logic wr, input logic [7:0] num, input logic [63:0] data);
        @(negedge clk); gif_reg_wr_en = wr; 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] xyz2_data(input logic [11:0] xi, input logic [11:0] yi);
        return {32'd0, yi, 4'd0, xi, 4'd0};
    endfunction
    function automatic logic [63:0] uv_data(input int ui, input int vi);
        return {36'd0, 14'(vi) << 4, 14'(ui) << 4};
    endfunction
    task automatic vram_word(input logic [31:0] a, output logic [31:0] w);
        vram_read_addr = a; #1; w = vram_read_data;
    endtask
    task automatic force_word(input logic [31:0] a, input logic [31:0] d);
        u_vram.mem[a+0]=d[7:0]; u_vram.mem[a+1]=d[15:8]; u_vram.mem[a+2]=d[23:16]; u_vram.mem[a+3]=d[31:24];
    endtask
    task automatic force_byte(input logic [31:0] a, input logic [7:0] d); u_vram.mem[a] = d; endtask

    // ---- a textured-alpha SPRITE at FB (ox,oy), bound to tex0 ----
    task automatic draw_sprite(input logic [63:0] tex0, input logic [63:0] alpha,
                               input int ox, input int oy);
        drive_reg(R_PRIM,    PRIM_SPR_TEX_ABE);
        drive_reg(R_FRAME_1, FRAME_1_FB0);
        drive_reg(R_TEX0_1,  tex0);
        drive_reg(R_ALPHA_1, alpha);
        drive_reg(R_RGBAQ,   TINT_RGBAQ);
        drive_reg(R_UV,      uv_data(0, 0));
        drive_reg(R_XYZ2,    xyz2_data(12'(ox), 12'(oy)));
        drive_reg(R_RGBAQ,   TINT_RGBAQ);
        drive_reg(R_UV,      uv_data(SPRITE_W-1, SPRITE_H-1));
        drive_reg(R_XYZ2,    xyz2_data(12'(ox+SPRITE_W-1), 12'(oy+SPRITE_H-1)));
        drive_idle();
        wait (raster_active == 1'b1);
        wait (raster_active == 1'b0);
        repeat (10) @(posedge clk);
    endtask

    // ---- read2 no-overlap monitor (Ch344 core gate) ----
    int overlap_count; initial overlap_count = 0;
    always_ff @(posedge clk)
        if (rst_n && tex_rd_en && fb_rd_en) begin
            $error("[tb_gs_psmt8_alpha_sprite] READ2 OVERLAP @%0t tex=0x%08x fb=0x%08x", $time, tex_rd_addr, fb_rd_addr);
            overlap_count <= overlap_count + 1;
        end

    // The external LPDDR ROP must receive architectural ABE/ALPHA metadata even
    // when the legacy internal-BRAM sprite blender does not implement that mode.
    // x=24 is the authentic SH3 destination-darken control; the other sprites
    // retain the established source-over metadata.
    int lpddr_meta_count; initial lpddr_meta_count = 0;
    always_ff @(posedge clk)
        if (rst_n && raster_pixel_emit) begin
            logic [16:0] exp_alpha;
            exp_alpha = (raster_pixel_x_q >= 12'd24)
                      ? {1'b1, 2'd2, 2'd1, 2'd0, 2'd1, 8'd0} // ALPHA=0x46
                      : {1'b1, 2'd0, 2'd1, 2'd0, 2'd1, 8'd0}; // ALPHA=0x44
            if (!raster_pixel_abe_q || raster_pixel_alpha_q !== exp_alpha) begin
                $error("LPDDR alpha metadata x=%0d got abe=%0b alpha=%05x exp=%05x",
                       raster_pixel_x_q, raster_pixel_abe_q, raster_pixel_alpha_q, exp_alpha);
                errors++;
            end
            lpddr_meta_count++;
        end

    int errors; initial errors = 0;
    integer x, y; logic [31:0] got, exp;
    localparam int C32_OX = 8;   // PSMCT32 control sprite at FB x=8..11
    localparam int T4_OX  = 16;  // PSMT4 alpha sprite at FB x=16..19
    localparam int DARK_OX = 24; // PSMCT32 ALPHA=0x46 LPDDR-sideband control

    initial begin
        rst_n=1'b0; gif_reg_wr_en=1'b0; gif_reg_num=0; gif_reg_data=0; vram_read_addr=0;
        clut_we=1'b0; clut_widx=0; clut_wdata=0;

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

        // PSMT8 (byte indices), PSMT4 (packed nibble indices), PSMCT32
        // control texture, and destination BG under all three sprites.
        for (y=0; y<SPRITE_H; y++) for (x=0; x<SPRITE_W; x++) begin
            force_byte(T8_BASE + (y*ROW_TEXELS) + x, idx_pattern(x,y));
            force_word(C32_BASE + ((y*ROW_TEXELS)+x)*4, c32_texel(x,y));
            force_word((y*FB0_STRIDE) + (x*4),            BG_PIXEL);  // FB region for PSMT8 sprite
            force_word((y*FB0_STRIDE) + ((C32_OX+x)*4),   BG_PIXEL);  // FB region for PSMCT32 sprite
            force_word((y*FB0_STRIDE) + ((T4_OX+x)*4),    BG_PIXEL);  // FB region for PSMT4 sprite
            force_word((y*FB0_STRIDE) + ((DARK_OX+x)*4),  BG_PIXEL);  // FB region for darken control
        end
        for (y=0; y<SPRITE_H; y++) for (x=0; x<SPRITE_W; x+=2)
            force_byte(T4_BASE + ((y*ROW_TEXELS+x)>>1),
                       (idx_pattern(x+1,y)<<4) | idx_pattern(x,y));
        // Program the CLUT (board: clut_loader_stub on a CLD!=0 TEX0; here TB-direct, same write port).
        for (int i=0; i<4; i++) clut_write(8'(i), palette(8'(i)));

        // ===== PSMT8 CLUT alpha sprite =====
        draw_sprite(TEX0_T8, ALPHA_SRCOVER, 0, 0);
        // ===== PSMCT32 control alpha sprite (param ON -> must stay green) =====
        draw_sprite(TEX0_C32, ALPHA_SRCOVER, C32_OX, 0);
        // ===== PSMT4 CLUT alpha sprite (Ch408) =====
        draw_sprite(TEX0_T4, ALPHA_SRCOVER, T4_OX, 0);
        // The BRAM path deliberately does not blend this generic mode; this
        // control exists to prove the external ROP sideband is nevertheless live.
        draw_sprite(TEX0_C32, ALPHA_DARKEN, DARK_OX, 0);

        // PROOF 4 — read2 never overlapped.
        if (overlap_count != 0) begin
            $error("read2 OVERLAP cycles=%0d — invariant VIOLATED", overlap_count); errors++;
        end
        if (lpddr_meta_count != 4*SPRITE_W*SPRITE_H) begin
            $error("LPDDR metadata count got=%0d exp=%0d", lpddr_meta_count, 4*SPRITE_W*SPRITE_H); errors++;
        end

        // PROOF 1/2/3 — PSMT8 pixels == source-over(CLUT[idx]*tint, BG), As from the CLUT entry.
        for (y=0; y<SPRITE_H; y++) for (x=0; x<SPRITE_W; x++) begin
            logic [7:0] idx; logic [31:0] t8;
            idx = idx_pattern(x,y); t8 = palette(idx);
            vram_word((y*FB0_STRIDE)+(x*4), got);
            exp = expected_from_texel(t8);
            if (got !== exp) begin
                $error("[PSMT8] FB(%0d,%0d) got=%08x exp=%08x idx=0x%02x clut=%08x", x,y,got,exp,idx,t8); errors++;
            end
        end
        // PROOF 5 — PSMCT32 control sprite still correct in the same (param-ON) build.
        for (y=0; y<SPRITE_H; y++) for (x=0; x<SPRITE_W; x++) begin
            vram_word((y*FB0_STRIDE)+((C32_OX+x)*4), got);
            exp = expected_from_texel(c32_texel(x,y));
            if (got !== exp) begin
                $error("[PSMCT32 ctrl] FB(%0d,%0d) got=%08x exp=%08x", x,y,got,exp); errors++;
            end
        end

        // Ch408 — packed-nibble select, CLUT lookup, texel-alpha blend, and
        // registered read2 alignment must match the same independent oracle.
        for (y=0; y<SPRITE_H; y++) for (x=0; x<SPRITE_W; x++) begin
            logic [7:0] idx; logic [31:0] t4;
            idx = idx_pattern(x,y) & 8'h0F; t4 = palette(idx);
            vram_word((y*FB0_STRIDE)+((T4_OX+x)*4), got);
            exp = expected_from_texel(t4);
            if (got !== exp) begin
                $error("[PSMT4] FB(%0d,%0d) got=%08x exp=%08x idx=0x%01x clut=%08x", x,y,got,exp,idx[3:0],t4); errors++;
            end
        end

        $display("[tb_gs_psmt8_alpha_sprite] PSMT8+PSMT4+PSMCT32 sprites drawn, read2_overlap=%0d errors=%0d", overlap_count, errors);
        if (errors==0) begin $display("[tb_gs_psmt8_alpha_sprite] PASS"); $finish; end
        else $fatal(1, "[tb_gs_psmt8_alpha_sprite] FAIL (%0d errors)", errors);
    end

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

endmodule : tb_gs_psmt8_alpha_sprite
