// retroDE_ps2 — tb_gs_psmt8_clut_triangle (Ch348 synthetic acceptance)
//
// Proves PSMT8 indexed-texture + CLUT on the textured-TRIANGLE path (DECAL) — the new Ch348 capability
// (CLUT was proven only on sprites; the triangle path was PSMCT32-DECAL-only). Composes Ch342 perspective
// ST/Q triangles + Ch347 PSMT8->CLUT sampling.
//
// Two triangles through gs_stub (PERSPECTIVE_CORRECT=1, registered read2 = board model):
//   1. AFFINE  : PRIM=TRI|TME, FST=1 per-vertex UV (UV==screen XY -> affine u=x,v=y). Uses the affine DDA.
//   2. PERSPECTIVE: PRIM=TRI|TME, FST=0 per-vertex ST + RGBAQ.Q (constant Q=1 -> ST==XY). Uses gs_persp_uv.
// Both sample a PSMT8 index texture through clut_stub.  The perspective case
// enables the serialized palette-bilinear path and independently checks that
// every covered sample is emitted exactly once; the affine case retains the
// exact CLUT[index(x,y)] color check.

`timescale 1ns/1ps

module tb_gs_psmt8_clut_triangle;
    localparam logic [5:0] PSMT8 = 6'h13;

    logic clk, 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,prim_q,rgbaq_q,xyz2_q,xyzf2_q,frame_1_q,zbuf_1_q,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,prim_color_q,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 [16:0] raster_pixel_alpha_q;
    logic        raster_active,raster_overflow,raster_fifo_full,raster_degenerate;
    logic        tex_rd_en; logic [31:0] tex_rd_addr,tex_rd_data;
    logic        fb_rd_en;  logic [31:0] fb_rd_addr,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 #(.PERSPECTIVE_CORRECT(1'b1),
              .BILINEAR_ENABLE(1'b1), .PALETTE_BILINEAR(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_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)
    );

    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

    logic [31:0] vram_read_addr, vram_read_data, rd2_addr, rd2_data, rd2_data_reg;
    vram_stub #(.BYTES(8192)) 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));
    assign rd2_addr = fb_rd_en ? fb_rd_addr : tex_rd_addr;
    always_ff @(posedge clk) rd2_data_reg <= rd2_data;
    assign tex_rd_data = rd2_data_reg; assign fb_rd_data = rd2_data_reg;

    localparam logic [7:0] R_PRIM=8'h00,R_RGBAQ=8'h01,R_ST=8'h02,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_TRI_TEX = 64'd3 | (64'd1<<4);          // TRIANGLE + TME, FST=0 (ST)
    localparam logic [63:0] PRIM_TRI_TEX_FST = PRIM_TRI_TEX | (64'd1<<8); // + FST=1 (UV affine)
    localparam logic [63:0] FRAME_1_FB0 = 64'h0000_0000_0001_0000;     // FBP=0 FBW=1 PSMCT32
    localparam logic [13:0] TBP0 = 14'd8; localparam logic [31:0] TEX_BASE = 32'd8*32'd256;
    localparam int ROW = 64;
    // TEX0: PSMT8, TBP0=8, TBW=1, TW=4 TH=4 (16x16), TFX=DECAL(1), TCC=1
    localparam logic [63:0] TEX0_T8 = {14'd0/*pad*/, TBP0} | (64'd1<<14) | (64'(PSMT8)<<20)
                                    | (64'd4<<26) | (64'd4<<30) | (64'd1<<34) | (64'd1<<35);
    localparam logic [63:0] TEX0_T8_MOD = TEX0_T8 & ~(64'd3<<35);

    function automatic logic [7:0] idx_pat(input int x, input int y); return 8'(((x*3) ^ (y*5) + x + y) & 8'hFF); endfunction
    function automatic logic [31:0] pal(input logic [7:0] i);          // distinct ABGR per index
        return {8'hFF, 8'(i ^ 8'h5A), 8'(i + 8'h20), 8'(i)};
    endfunction

    task automatic drive_reg(input logic [7:0] n,input logic [63:0] d);
        @(negedge clk); gif_reg_wr_en=1'b1; gif_reg_num=n; gif_reg_data=d; @(posedge clk); endtask
    task automatic drive_idle(); @(negedge clk); gif_reg_wr_en=1'b0; gif_reg_num=0; gif_reg_data=0; @(posedge clk); endtask
    function automatic logic [63:0] xyz2_data(input int x,input int y); return {32'd0,12'(y),4'd0,12'(x),4'd0}; endfunction
    function automatic logic [63:0] rgbaq(input int r,input int g,input int b);  // A=0xFF, Q=1.0
        return {32'h3F80_0000, 8'hFF, 8'(b), 8'(g), 8'(r)}; endfunction
    function automatic logic [63:0] rgbaqQ(input int r,input int g,input int b,input int qv); // A=0xFF, Q=f32(qv)
        return {f32(qv), 8'hFF, 8'(b), 8'(g), 8'(r)}; endfunction
    function automatic logic [63:0] uv_data(input int u,input int v); return {36'd0,14'(v)<<4,14'(u)<<4}; endfunction
    // int -> IEEE-754 single (small positive ints; iverilog 12 has no $shortrealtobits)
    function automatic logic [31:0] f32(input int n);
        int e, v; logic [22:0] mant;
        if (n <= 0) return 32'd0;
        v = n; e = 0; while (v > 1) begin v = v >> 1; e = e + 1; end
        mant = (32'(n) << (23 - e)) & 23'h7FFFFF;
        f32 = {1'b0, 8'(127 + e), mant};
    endfunction
    function automatic logic [63:0] st_data(input int s,input int t);  // S,T floats = (s,t); Q=1 -> S/Q==x
        return {f32(t), f32(s)}; endfunction
    function automatic void force_byte(input logic [31:0] a,input logic [7:0] d); u_vram.mem[a]=d; 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

    // independent triangle-inside reference (matches gs_stub top-left rule, mirrors tb_gs_textured_triangle)
    int vx0,vy0,vx1,vy1,vx2,vy2, sx1,sy1,sx2,sy2;
    function automatic int edge_f(input int px,input int py,input int ax,input int ay,input int bx,input int by);
        return (px-ax)*(by-ay)-(py-ay)*(bx-ax); endfunction
    function automatic bit tol(input int ax,input int ay,input int bx,input int by);
        int dx,dy; dx=bx-ax; dy=by-ay; return (dy>0)||((dy==0)&&(dx>0)); endfunction
    task automatic setup_ref();
        int sa; sa=(vx1-vx0)*(vy2-vy0)-(vy1-vy0)*(vx2-vx0);
        if (sa<0) begin sx1=vx2;sy1=vy2;sx2=vx1;sy2=vy1; end else begin sx1=vx1;sy1=vy1;sx2=vx2;sy2=vy2; end endtask
    function automatic bit ref_inside(input int px,input int py);
        int e0,e1,e2,b0,b1,b2;
        e0=edge_f(px,py,vx0,vy0,sx1,sy1); e1=edge_f(px,py,sx1,sy1,sx2,sy2); e2=edge_f(px,py,sx2,sy2,vx0,vy0);
        b0=tol(vx0,vy0,sx1,sy1)?0:1; b1=tol(sx1,sy1,sx2,sy2)?0:1; b2=tol(sx2,sy2,vx0,vy0)?0:1;
        return ((e0+b0)<=0)&&((e1+b1)<=0)&&((e2+b2)<=0); endfunction

    bit covered [0:15][0:15]; logic [31:0] cap_px [0:15][0:15]; bit cap_armed;
    always_ff @(posedge clk)
        if (rst_n && cap_armed && raster_pixel_emit && raster_pixel_x_q<16 && raster_pixel_y_q<16) begin
            covered[raster_pixel_y_q][raster_pixel_x_q] <= 1'b1;
            cap_px [raster_pixel_y_q][raster_pixel_x_q] <= raster_pixel_color_q[31:0];
        end

    // LPDDR ROP metadata is separate from the legacy internal-BRAM alpha path.
    // Capture a direct-color-style ABE triangle control so the production
    // perspective emit cannot silently degrade to an opaque copy.
    bit alpha_cap_armed; int alpha_emit_count, alpha_meta_bad;
    always_ff @(posedge clk)
        if (rst_n && alpha_cap_armed && raster_pixel_emit) begin
            alpha_emit_count <= alpha_emit_count + 1;
            if (raster_pixel_alpha_q !== {1'b1,2'd2,2'd1,2'd0,2'd1,8'h80})
                alpha_meta_bad <= alpha_meta_bad + 1;
        end
    bit mod_cap_armed; int mod_emit_count, mod_rgb_bad, mod_alpha_bad;
    always_ff @(posedge clk)
        if (rst_n && mod_cap_armed && raster_pixel_emit) begin
            mod_emit_count <= mod_emit_count + 1;
            if (raster_pixel_color_q[23:0] !== 24'd0)
                mod_rgb_bad <= mod_rgb_bad + 1;
            if (raster_pixel_color_q[31:24] !== 8'h50)
                mod_alpha_bad <= mod_alpha_bad + 1;
        end

    int errors, inside_ok; integer x,y;

    // draw one TME triangle (vertices in vx*/vy*); fst=1 -> UV, fst=0 -> ST. check covered px == CLUT[idx(x,y)].
    task automatic run_tri(input bit use_uv, input string tag);
        for (y=0;y<16;y++) for (x=0;x<16;x++) begin covered[y][x]=1'b0; cap_px[y][x]=0; end
        setup_ref();
        cap_armed=1'b1;
        drive_reg(R_PRIM,    use_uv ? PRIM_TRI_TEX_FST : PRIM_TRI_TEX);
        drive_reg(R_FRAME_1, FRAME_1_FB0);
        drive_reg(R_TEX0_1,  TEX0_T8);
        drive_reg(R_RGBAQ, rgbaq(8'hFF,0,0));
        if (use_uv) drive_reg(R_UV, uv_data(vx0,vy0)); else drive_reg(R_ST, st_data(vx0,vy0));
        drive_reg(R_XYZ2, xyz2_data(vx0,vy0));
        drive_reg(R_RGBAQ, rgbaq(0,8'hFF,0));
        if (use_uv) drive_reg(R_UV, uv_data(vx1,vy1)); else drive_reg(R_ST, st_data(vx1,vy1));
        drive_reg(R_XYZ2, xyz2_data(vx1,vy1));
        drive_reg(R_RGBAQ, rgbaq(0,0,8'hFF));
        if (use_uv) drive_reg(R_UV, uv_data(vx2,vy2)); else drive_reg(R_ST, st_data(vx2,vy2));
        drive_reg(R_XYZ2, xyz2_data(vx2,vy2));
        drive_idle();
        repeat (400) @(posedge clk); cap_armed=1'b0; @(posedge clk);
        for (y=0;y<16;y++) for (x=0;x<16;x++) begin
            bit want; logic [31:0] exp;
            want = ref_inside(x,y);
            if (want && covered[y][x]) begin
                exp = pal(idx_pat(x,y));
                if (cap_px[y][x][23:0] !== exp[23:0]) begin
                    if (errors<10) $error("[%s] (%0d,%0d) RGB got=%06x exp=%06x idx=%02x", tag,x,y,cap_px[y][x][23:0],exp[23:0],idx_pat(x,y));
                    errors++;
                end else inside_ok++;
            end else if (want && !covered[y][x]) begin
                if (errors<10) $error("[%s] inside (%0d,%0d) NOT drawn", tag,x,y); errors++;
            end
        end
        $display("[tb_gs_psmt8_clut_triangle] %s: inside_ok=%0d errors=%0d", tag, inside_ok, errors);
    endtask

    initial begin
        errors=0; inside_ok=0; cap_armed=1'b0; alpha_cap_armed=1'b0; alpha_emit_count=0; alpha_meta_bad=0;
        mod_cap_armed=1'b0; mod_emit_count=0; mod_rgb_bad=0; mod_alpha_bad=0;
        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);

        for (y=0;y<16;y++) for (x=0;x<16;x++) force_byte(TEX_BASE + (y*ROW) + x, idx_pat(x,y));
        for (int i=0;i<256;i++) clut_write(8'(i), pal(8'(i)));

        // vertices: UV/ST == screen XY at each vertex -> sampled idx == idx(x,y) at every interior pixel
        vx0=2; vy0=1; vx1=13; vy1=2; vx2=5; vy2=7;
        run_tri(1'b1, "AFFINE");      // FST=1 UV — exact per-pixel idx -> CLUT check

        // PERSPECTIVE PSMT8+CLUT: a real perspective triangle (Q varies -> texels foreshorten). Per-pixel
        // exact prediction needs the perspective interp; instead prove the PSMT8->CLUT chain on the persp
        // path = covered pixels show MANY distinct CLUT colors (varied indices) + every color is a real CLUT
        // entry. v0:(2,1)u0v0 Q1  v1:(13,2)u15v0 Q2  v2:(5,7)u0v15 Q1  -> u=S/Q sweeps 0..15, v 0..15.
        begin
            logic [31:0] seen[0:255]; int nseen; bit all_known;
            for (y=0;y<16;y++) for (x=0;x<16;x++) begin covered[y][x]=1'b0; cap_px[y][x]=0; end
            cap_armed=1'b1;
            drive_reg(R_PRIM, PRIM_TRI_TEX); drive_reg(R_FRAME_1, FRAME_1_FB0); drive_reg(R_TEX0_1, TEX0_T8);
            drive_reg(R_RGBAQ, rgbaqQ(0,0,0,1)); drive_reg(R_ST, st_data(0, 0));   drive_reg(R_XYZ2, xyz2_data(2,1));
            drive_reg(R_RGBAQ, rgbaqQ(0,0,0,2)); drive_reg(R_ST, st_data(30,0));   drive_reg(R_XYZ2, xyz2_data(13,2));
            drive_reg(R_RGBAQ, rgbaqQ(0,0,0,1)); drive_reg(R_ST, st_data(0, 15));  drive_reg(R_XYZ2, xyz2_data(5,7));
            drive_idle(); repeat (2000) @(posedge clk); cap_armed=1'b0; @(posedge clk);
            nseen=0; all_known=1'b1;
            begin
                int expected_covered, actual_covered, missing_covered;
                expected_covered=0; actual_covered=0; missing_covered=0;
                for (y=0;y<16;y++) for (x=0;x<16;x++) begin
                    if (ref_inside(x,y)) begin
                        expected_covered++;
                        if (!covered[y][x]) missing_covered++;
                    end
                    if (covered[y][x]) actual_covered++;
                end
                $display("[tb_gs_psmt8_clut_triangle] PERSP coverage: actual=%0d expected=%0d missing=%0d",
                         actual_covered, expected_covered, missing_covered);
                if (missing_covered != 0) begin
                    $error("PERSP: %0d/%0d covered samples missing (serialized sampler skipped pixels)",
                           missing_covered, expected_covered);
                    errors++;
                end
            end
            for (y=0;y<16;y++) for (x=0;x<16;x++) if (covered[y][x]) begin
                bit f;
                f=1'b0; for (int k=0;k<nseen;k++) if (seen[k]===cap_px[y][x]) f=1'b1;
                if (!f && nseen<256) begin seen[nseen]=cap_px[y][x]; nseen++; end
                if (^cap_px[y][x] === 1'bx) all_known=1'b0;
            end
            // The enabled palette-bilinear sampler legitimately blends CLUT
            // colors, so blended outputs need not equal a single palette entry.
            $display("[tb_gs_psmt8_clut_triangle] PERSP(chain): distinct colors=%0d all_known=%b",
                     nseen, all_known);
            if (nseen < 2) begin $error("PERSP: %0d distinct colors — persp path produced no CLUT variety", nseen); errors++; end
            if (!all_known) begin $error("PERSP: covered pixel contains unknown data"); errors++; end
        end


        // Authentic SH3 darken selectors: A=ZERO, B=Cd, C=As, D=Cd,
        // FIX=0x80.  The triangle is perspective because the board fixture
        // lowers each source sprite to ST/Q triangles.
        alpha_emit_count=0; alpha_meta_bad=0; alpha_cap_armed=1'b1;
        drive_reg(R_ALPHA_1, 64'h0000_0080_0000_0046);
        drive_reg(R_PRIM, PRIM_TRI_TEX | (64'd1<<6));
        drive_reg(R_FRAME_1, FRAME_1_FB0); drive_reg(R_TEX0_1, TEX0_T8);
        drive_reg(R_RGBAQ, rgbaqQ(0,0,0,1)); drive_reg(R_ST, st_data(0,0));   drive_reg(R_XYZ2, xyz2_data(2,1));
        drive_reg(R_RGBAQ, rgbaqQ(0,0,0,1)); drive_reg(R_ST, st_data(15,0));  drive_reg(R_XYZ2, xyz2_data(13,2));
        drive_reg(R_RGBAQ, rgbaqQ(0,0,0,1)); drive_reg(R_ST, st_data(0,15));  drive_reg(R_XYZ2, xyz2_data(5,7));
        drive_idle(); repeat (500) @(posedge clk); alpha_cap_armed=1'b0; @(posedge clk);
        $display("[tb_gs_psmt8_clut_triangle] TRI ABE metadata: emits=%0d bad=%0d", alpha_emit_count, alpha_meta_bad);
        if (alpha_emit_count==0 || alpha_meta_bad!=0) begin
            $error("TRI ABE metadata missing/corrupt: emits=%0d bad=%0d", alpha_emit_count, alpha_meta_bad); errors++;
        end

        // TFX=MODULATE with a black vertex tint must emit black RGB, not the
        // DECAL texel.  This is the other half of the SH3 darken source packet.
        // Give every sampled texel A=0x80 and use vertex A=0x50.  GS
        // MODULATE+TCC must therefore emit As=0x50.
        for (int i=0;i<256;i++) clut_write(8'(i), (pal(8'(i)) & 32'h00ff_ffff) | 32'h8000_0000);
        mod_emit_count=0; mod_rgb_bad=0; mod_alpha_bad=0; mod_cap_armed=1'b1;
        drive_reg(R_PRIM, PRIM_TRI_TEX); drive_reg(R_FRAME_1, FRAME_1_FB0); drive_reg(R_TEX0_1, TEX0_T8_MOD);
        drive_reg(R_RGBAQ, {f32(1),8'h50,24'd0}); drive_reg(R_ST, st_data(0,0));   drive_reg(R_XYZ2, xyz2_data(2,1));
        drive_reg(R_RGBAQ, {f32(1),8'h50,24'd0}); drive_reg(R_ST, st_data(15,0));  drive_reg(R_XYZ2, xyz2_data(13,2));
        drive_reg(R_RGBAQ, {f32(1),8'h50,24'd0}); drive_reg(R_ST, st_data(0,15));  drive_reg(R_XYZ2, xyz2_data(5,7));
        drive_idle(); repeat (500) @(posedge clk); mod_cap_armed=1'b0; @(posedge clk);
        $display("[tb_gs_psmt8_clut_triangle] TRI MODULATE black/A: emits=%0d bad_rgb=%0d bad_alpha=%0d", mod_emit_count, mod_rgb_bad, mod_alpha_bad);
        if (mod_emit_count==0 || mod_rgb_bad!=0 || mod_alpha_bad!=0) begin
            $error("TRI MODULATE ignored/misaligned: emits=%0d bad_rgb=%0d bad_alpha=%0d", mod_emit_count, mod_rgb_bad, mod_alpha_bad); errors++;
        end

        if (raster_overflow) begin $error("raster_overflow"); errors++; end
        if (errors==0) begin $display("[tb_gs_psmt8_clut_triangle] PASS"); $finish; end
        else $fatal(1, "[tb_gs_psmt8_clut_triangle] FAIL (%0d errors)", errors);
    end

    initial begin #5000000; $error("[tb_gs_psmt8_clut_triangle] timeout"); $finish; end
endmodule : tb_gs_psmt8_clut_triangle
