// retroDE_ps2 — tb_gs_texture_bilinear (Ch308)
//
// Focused TB for BILINEAR (4-tap) PSMCT32 texture filtering at the sampler
// level. gs_texture_unit is instantiated with BILINEAR_ENABLE=1 +
// TEX_WRAP_ENABLE=1; for a fractional texel coord it fetches the 4 surrounding
// texels (u,v)(u+1,v)(u,v+1)(u+1,v+1), each independently wrapped/clamped, and
// blends them per channel by the 4-bit fractional u_frac/v_frac using the
// >>>4 fixed-point lerp:
//   lerp(a,b,f) = a + (($signed(b)-$signed(a)) * $signed(f)) >>> 4 , f in 0..15
// The expected values below mirror that lerp EXACTLY (same >>4, same rounding),
// so every check is an exact == match, not a tolerance.
//
// 4x4 PSMCT32 texture (TW=TH=2 => width=height=4), TBW=1, distinct ABGR per
// texel so 4-tap blends are individually checkable.
//
// PROOF CASES:
//   1. EXACT CENTER  (uf=0,vf=0)         : result == texel(u,v)  (== nearest).
//   2. HALFWAY       (uf=8,vf=8)         : result == 4-neighbor blend (lerp).
//   3. HALFWAY-U     (uf=8,vf=0)         : result == lerp(texel(u,v),texel(u+1,v)).
//   4. CLAMP edge    (wms=wmt=1,u=W-1)   : (u+1) clamps to W-1 => result ==
//                                          texel(W-1,v); issued addrs <= last col.
//   5. REPEAT edge   (wms=wmt=0,u=W-1)   : (u+1) wraps to 0 => result ==
//                                          lerp(texel(W-1,v),texel(0,v),uf).
//   6. NEAREST UNCHANGED: a 2nd instance with BILINEAR_ENABLE=0 returns the
//                         single nearest texel and ignores u_frac/v_frac.
//
// Prints [tb_gs_texture_bilinear] PASS / FAIL + $finish.

`timescale 1ns/1ps

module tb_gs_texture_bilinear;

    localparam logic [5:0] PSMCT32 = 6'h00;

    localparam logic [1:0] WM_REPEAT = 2'd0;
    localparam logic [1:0] WM_CLAMP  = 2'd1;

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

    // ---- tiny combinational VRAM model (word-addressed little-endian) ----
    localparam int VRAM_BYTES = 16384;
    logic [7:0] vram [0:VRAM_BYTES-1];

    logic [31:0] tex_rd_addr;
    logic [31:0] tex_rd_data;
    assign tex_rd_data = { vram[(tex_rd_addr & ~32'd3) + 3],
                           vram[(tex_rd_addr & ~32'd3) + 2],
                           vram[(tex_rd_addr & ~32'd3) + 1],
                           vram[(tex_rd_addr & ~32'd3) + 0] };

    task automatic poke_word(input logic [31:0] a, input logic [31:0] d);
        vram[a+0] = d[7:0];   vram[a+1] = d[15:8];
        vram[a+2] = d[23:16]; vram[a+3] = d[31:24];
    endtask

    // Ch314 — PSMT8: one index byte per texel at the linear byte address.
    task automatic poke_t8(input int x, input int y, input logic [7:0] idx);
        vram[T8_BASE + (y * (TBWv*64)) + x] = idx;   // 1 byte/texel
    endtask
    // Ch314 — PSMT4: pack 2 indices per byte; even texel-offset -> LOW nibble,
    // odd -> HIGH nibble (matches gs_texel_addr's texel_offset[0]==nibble_hi).
    task automatic poke_t4(input int x, input int y, input logic [3:0] idx);
        int off; int ba;
        begin
            off = (y * (TBWv*64)) + x;
            ba  = T4_BASE + (off >> 1);
            if (off[0]) vram[ba] = {idx, vram[ba][3:0]};   // high nibble
            else        vram[ba] = {vram[ba][7:4], idx};   // low nibble
        end
    endtask

    // ---- bilinear DUT ----
    logic        in_valid;
    logic [10:0] u, v;
    logic [3:0]  u_frac, v_frac;
    logic [1:0]  wms, wmt;
    logic [3:0]  tw, th;
    logic [31:0] tbp0_base_bytes;
    logic [13:0] tbw;
    logic [5:0]  psm;

    logic        tex_rd_en;
    logic [7:0]  clut_rd_idx;
    logic [31:0] clut_rd_data;
    logic        out_valid;
    logic [31:0] tex_color;
    logic        busy;

    // Ch314 — CLUT model (256 PSMCT32 entries) shared by both DUTs. The sampler
    // drives clut_rd_idx combinationally from the extracted index; we return the
    // palette color, exactly like clut_stub's 2nd (combinational) read port.
    logic [31:0] clut [0:255];
    assign clut_rd_data = clut[clut_rd_idx];

    gs_texture_unit #(.BILINEAR_ENABLE(1'b1), .PALETTE_BILINEAR(1'b1),
                      .TEX_WRAP_ENABLE(1'b1), .RD_LATENCY(1)) dut (
        .clk(clk), .rst_n(rst_n),
        .in_valid(in_valid),
        .u(u), .v(v),
        .u_frac(u_frac), .v_frac(v_frac),
        .wms(wms), .wmt(wmt), .tw(tw), .th(th),
        .tbp0_base_bytes(tbp0_base_bytes),
        .tbw(tbw),
        .psm(psm),
        .tex_rd_en(tex_rd_en),
        .tex_rd_addr(tex_rd_addr),
        .tex_rd_data(tex_rd_data),
        .clut_rd_idx(clut_rd_idx),
        .clut_rd_data(clut_rd_data),
        .out_valid(out_valid),
        .tex_color(tex_color),
        .busy(busy)
    );

    // ---- nearest reference DUT (BILINEAR_ENABLE=0) ----
    logic        n_in_valid;
    logic [10:0] n_u, n_v;
    logic [3:0]  n_uf, n_vf;
    logic [31:0] n_tex_rd_addr;
    logic [31:0] n_tex_rd_data;
    logic        n_out_valid;
    logic [31:0] n_tex_color;
    logic        n_busy;
    logic [7:0]  n_clut_rd_idx;   // Ch314 — nearest DUT's CLUT index (for PSMT8 nearest x-check)
    assign n_tex_rd_data = { vram[(n_tex_rd_addr & ~32'd3) + 3],
                             vram[(n_tex_rd_addr & ~32'd3) + 2],
                             vram[(n_tex_rd_addr & ~32'd3) + 1],
                             vram[(n_tex_rd_addr & ~32'd3) + 0] };
    gs_texture_unit #(.BILINEAR_ENABLE(1'b0), .TEX_WRAP_ENABLE(1'b1), .RD_LATENCY(1)) dut_near (
        .clk(clk), .rst_n(rst_n),
        .in_valid(n_in_valid),
        .u(n_u), .v(n_v),
        .u_frac(n_uf), .v_frac(n_vf),
        .wms(WM_REPEAT), .wmt(WM_REPEAT), .tw(tw), .th(th),
        .tbp0_base_bytes(tbp0_base_bytes),
        .tbw(tbw),
        .psm(psm),
        .tex_rd_en(),
        .tex_rd_addr(n_tex_rd_addr),
        .tex_rd_data(n_tex_rd_data),
        .clut_rd_idx(n_clut_rd_idx),
        .clut_rd_data(clut[n_clut_rd_idx]),
        .out_valid(n_out_valid),
        .tex_color(n_tex_color),
        .busy(n_busy)
    );

    // ---- texture geometry: 4x4 (TW=TH=2), TBW=1 ----
    localparam int    TEXW = 4;
    localparam int    TEXH = 4;
    localparam [3:0]  TWv  = 4'd2;     // log2(4)
    localparam [3:0]  THv  = 4'd2;
    localparam [13:0] TBWv = 14'd1;    // row stride = TBW*64 texels
    localparam [31:0] CT32_BASE = 32'h0000_2000;
    // Ch314 — indexed textures (same 4x4 geometry, TBW=1).
    localparam logic [5:0] PSMT8v = 6'h13;
    localparam logic [5:0] PSMT4v = 6'h14;
    localparam [31:0] T8_BASE = 32'h0000_1000;   // PSMT8: 1 byte/texel, row stride TBW*64=64 B
    localparam [31:0] T4_BASE = 32'h0000_1800;   // PSMT4: 1 nibble/texel, row stride TBW*64/2=32 B
    // Palette: distinct, well-separated colors so a COLOR interpolation is
    // visibly different from CLUT-of-an-interpolated-index. idx 0=red 1=blue
    // 2=green 3=white. The column-striped index layout idx(x,y)=x gives
    // y-independent taps so vf is irrelevant and the blend is a clean U-lerp.
    localparam [31:0] CLUT0 = 32'hFF_00_00_FF;   // red   (A,B,G,R)
    localparam [31:0] CLUT1 = 32'hFF_FF_00_00;   // blue
    localparam [31:0] CLUT2 = 32'hFF_00_FF_00;   // green
    localparam [31:0] CLUT3 = 32'hFF_FF_FF_FF;   // white

    // distinct PSMCT32 ABGR word per (x,y): pack varied per-channel values so
    // each tap differs in R,G,B AND A (so blends are individually checkable).
    function automatic logic [31:0] ct32_word(input int x, input int y);
        logic [7:0] r, g, b, a;
        begin
            r = 8'(16 + (y*TEXW + x) * 12);   // 16,28,40,... distinct
            g = 8'(8  + (x) * 30);            // varies with x
            b = 8'(4  + (y) * 40);            // varies with y
            a = 8'(255 - (y*TEXW + x) * 8);   // varies, stays high
            ct32_word = {a, b, g, r};
        end
    endfunction

    // reference lerp — mirrors the RTL EXACTLY (same >>>4, same clamp).
    function automatic logic [7:0] ref_lerp8(input logic [7:0] a,
                                             input logic [7:0] b,
                                             input logic [3:0] f);
        logic signed [16:0] diff;
        logic signed [21:0] prod;
        logic signed [21:0] shifted;
        logic signed [21:0] res;
        begin
            diff    = $signed({1'b0, b}) - $signed({1'b0, a});
            prod    = diff * $signed({1'b0, f});
            shifted = prod >>> 4;
            res     = $signed({14'd0, a}) + shifted;
            if (res < 0)             ref_lerp8 = 8'd0;
            else if (res > 22'sd255) ref_lerp8 = 8'd255;
            else                     ref_lerp8 = res[7:0];
        end
    endfunction

    // expected bilinear blend of four ABGR words by (uf,vf)
    function automatic logic [31:0] ref_blend(input logic [31:0] w00,
                                              input logic [31:0] w10,
                                              input logic [31:0] w01,
                                              input logic [31:0] w11,
                                              input logic [3:0]  uf,
                                              input logic [3:0]  vf);
        logic [7:0] tr, tg, tb, ta;   // top
        logic [7:0] br, bg, bb, ba;   // bottom
        logic [7:0] cr, cg, cb, ca;   // out
        begin
            tr = ref_lerp8(w00[ 7: 0], w10[ 7: 0], uf);
            tg = ref_lerp8(w00[15: 8], w10[15: 8], uf);
            tb = ref_lerp8(w00[23:16], w10[23:16], uf);
            ta = ref_lerp8(w00[31:24], w10[31:24], uf);
            br = ref_lerp8(w01[ 7: 0], w11[ 7: 0], uf);
            bg = ref_lerp8(w01[15: 8], w11[15: 8], uf);
            bb = ref_lerp8(w01[23:16], w11[23:16], uf);
            ba = ref_lerp8(w01[31:24], w11[31:24], uf);
            cr = ref_lerp8(tr, br, vf);
            cg = ref_lerp8(tg, bg, vf);
            cb = ref_lerp8(tb, bb, vf);
            ca = ref_lerp8(ta, ba, vf);
            ref_blend = {ca, cb, cg, cr};
        end
    endfunction

    int errors;
    initial errors = 0;

    // track the max issued texel column/row during a bilinear sample (for the
    // clamp proof: addresses must never read beyond the texture).
    logic        watch_addr;
    int          max_addr_seen;
    always @(posedge clk) begin
        if (watch_addr && tex_rd_en) begin
            if ($signed(tex_rd_addr) > max_addr_seen)
                max_addr_seen = tex_rd_addr;
        end
    end

    // drive one BILINEAR sample, wait for out_valid, return tex_color.
    task automatic bsample(input int su, input int sv,
                           input logic [3:0] suf, input logic [3:0] svf,
                           output logic [31:0] col);
        int guard;
        begin
            @(negedge clk);
            in_valid = 1'b1;
            u = 11'(su); v = 11'(sv); u_frac = suf; v_frac = svf;
            @(negedge clk);
            in_valid = 1'b0;            // single-cycle request; FSM is running
            guard = 0;
            while (out_valid !== 1'b1 && guard < 200) begin
                @(negedge clk);
                guard++;
            end
            col = tex_color;
            if (guard >= 200) begin
                $error("[bilinear] out_valid never asserted (u=%0d v=%0d)", su, sv);
                errors++;
            end
        end
    endtask

    // drive the NEAREST reference instance, settle combinationally.
    task automatic nsample(input int su, input int sv,
                           input logic [3:0] suf, input logic [3:0] svf,
                           output logic [31:0] col);
        begin
            @(negedge clk);
            n_in_valid = 1'b1;
            n_u = 11'(su); n_v = 11'(sv); n_uf = suf; n_vf = svf;
            @(posedge clk);             // RD_LATENCY=1 -> out_valid next cycle
            #2;
            col = n_tex_color;
            @(negedge clk);
            n_in_valid = 1'b0;
        end
    endtask

    logic [31:0] got, exp;
    logic [31:0] w00, w10, w01, w11;
    int x, y;

    initial begin
        rst_n = 1'b0; in_valid = 1'b0; n_in_valid = 1'b0;
        u = 0; v = 0; u_frac = 0; v_frac = 0;
        n_u = 0; n_v = 0; n_uf = 0; n_vf = 0;
        wms = WM_REPEAT; wmt = WM_REPEAT;
        tw = TWv; th = THv;
        tbp0_base_bytes = CT32_BASE; tbw = TBWv; psm = PSMCT32;
        watch_addr = 1'b0; max_addr_seen = 0;
        for (int i = 0; i < VRAM_BYTES; i++) vram[i] = 8'd0;

        // upload 4x4 PSMCT32 (row stride TBW*64 texels * 4 bytes)
        for (y = 0; y < TEXH; y++)
            for (x = 0; x < TEXW; x++)
                poke_word(CT32_BASE + ((y * (TBWv*64)) + x) * 4, ct32_word(x, y));

        // Ch314 — CLUT palette + column-striped indexed textures idx(x,y)=x.
        for (int i = 0; i < 256; i++) clut[i] = 32'hFF_80_80_80;  // neutral filler
        clut[0] = CLUT0; clut[1] = CLUT1; clut[2] = CLUT2; clut[3] = CLUT3;
        for (y = 0; y < TEXH; y++)
            for (x = 0; x < TEXW; x++) begin
                poke_t8(x, y, 8'(x));      // PSMT8 index = column
                poke_t4(x, y, 4'(x));      // PSMT4 index = column
            end

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

        // ============================================================
        // CASE 1 — EXACT TEXEL CENTER (uf=0,vf=0) == nearest texel(u,v)
        // ============================================================
        wms = WM_REPEAT; wmt = WM_REPEAT;
        bsample(1, 2, 4'd0, 4'd0, got);
        exp = ct32_word(1, 2);
        if (got !== exp) begin
            $error("[case1 center] u=1 v=2 uf=0 vf=0 got=%08x exp=%08x", got, exp);
            errors++;
        end else
            $display("[tb_gs_texture_bilinear] CASE1 center pass: got=%08x == texel(1,2)", got);

        // ============================================================
        // CASE 2 — HALFWAY (uf=8,vf=8) == blend of 4 neighbors
        // ============================================================
        bsample(1, 1, 4'd8, 4'd8, got);
        w00 = ct32_word(1,1); w10 = ct32_word(2,1);
        w01 = ct32_word(1,2); w11 = ct32_word(2,2);
        exp = ref_blend(w00, w10, w01, w11, 4'd8, 4'd8);
        if (got !== exp) begin
            $error("[case2 halfway] u=1 v=1 uf=8 vf=8 got=%08x exp=%08x", got, exp);
            errors++;
        end else
            $display("[tb_gs_texture_bilinear] CASE2 halfway pass: got=%08x == 4-tap blend", got);

        // ============================================================
        // CASE 3 — HALFWAY-U only (uf=8,vf=0) == lerp(texel(u,v),texel(u+1,v))
        // ============================================================
        bsample(0, 0, 4'd8, 4'd0, got);
        w00 = ct32_word(0,0); w10 = ct32_word(1,0);
        w01 = ct32_word(0,1); w11 = ct32_word(1,1);
        exp = ref_blend(w00, w10, w01, w11, 4'd8, 4'd0);   // vf=0 -> top only
        if (got !== exp) begin
            $error("[case3 halfway-u] u=0 v=0 uf=8 vf=0 got=%08x exp=%08x", got, exp);
            errors++;
        end else
            $display("[tb_gs_texture_bilinear] CASE3 halfway-u pass: got=%08x == lerp((0,0),(1,0))", got);

        // ============================================================
        // CASE 4 — CLAMP edge (wms=wmt=1, u=W-1, uf>0): (u+1) clamps to W-1
        //   => result == texel(W-1,v). Also assert the issued addresses never
        //      exceed the last texel's address (no read outside texture).
        // ============================================================
        wms = WM_CLAMP; wmt = WM_CLAMP;
        watch_addr = 1'b1; max_addr_seen = 0;
        bsample(TEXW-1, 1, 4'd8, 4'd0, got);   // u=3, u+1 clamps back to 3
        watch_addr = 1'b0;
        // all 4 taps collapse to (3,1) for u; vf=0 keeps top row (v and v+1
        // both clamp/stay -> here v=1, v+1=2 but vf=0 picks v=1). With u
        // clamped, w00==w10 and w01==w11, and vf=0 -> top only == texel(3,1).
        w00 = ct32_word(3,1); w10 = ct32_word(3,1);   // (u+1) clamped to 3
        w01 = ct32_word(3,2); w11 = ct32_word(3,2);
        exp = ref_blend(w00, w10, w01, w11, 4'd8, 4'd0);
        if (got !== ct32_word(3,1)) begin
            $error("[case4 clamp] u=3 v=1 uf=8 got=%08x exp=texel(3,1)=%08x", got, ct32_word(3,1));
            errors++;
        end else if (got !== exp) begin
            $error("[case4 clamp blend] got=%08x exp(blend)=%08x", got, exp);
            errors++;
        end else
            $display("[tb_gs_texture_bilinear] CASE4 clamp pass: got=%08x == texel(3,1), no OOB read", got);
        // last legal PSMCT32 texel addr = base + (3*64+3)*4 region; the issued
        // word addr for texel(3,3) is base + (3*64+3)*4 = base + 0x30C. Any
        // higher would mean reading past column 3.
        if (max_addr_seen > $signed(CT32_BASE + ((3*(TBWv*64)) + 3)*4)) begin
            $error("[case4 clamp addr] max issued addr=0x%08x exceeds last-texel addr 0x%08x",
                   max_addr_seen, CT32_BASE + ((3*(TBWv*64)) + 3)*4);
            errors++;
        end else
            $display("[tb_gs_texture_bilinear] CASE4 max issued addr=0x%08x <= last texel 0x%08x",
                     max_addr_seen, CT32_BASE + ((3*(TBWv*64)) + 3)*4);

        // ============================================================
        // CASE 5 — REPEAT edge (wms=wmt=0, u=W-1, uf>0): (u+1) wraps to 0
        //   => result == lerp(texel(W-1,v),texel(0,v),uf) (vf=0, top row).
        // ============================================================
        wms = WM_REPEAT; wmt = WM_REPEAT;
        bsample(TEXW-1, 1, 4'd8, 4'd0, got);   // u=3, u+1=4 wraps to 0
        w00 = ct32_word(3,1); w10 = ct32_word(0,1);   // (u+1) wraps to col 0
        w01 = ct32_word(3,2); w11 = ct32_word(0,2);
        exp = ref_blend(w00, w10, w01, w11, 4'd8, 4'd0);
        if (got !== exp) begin
            $error("[case5 repeat] u=3 v=1 uf=8 got=%08x exp=lerp(t(3,1),t(0,1))=%08x", got, exp);
            errors++;
        end else
            $display("[tb_gs_texture_bilinear] CASE5 repeat pass: got=%08x == lerp(t(3,1),t(0,1))", got);

        // ============================================================
        // CASE 6 — NEAREST UNCHANGED: BILINEAR_ENABLE=0 instance returns the
        //   single nearest texel and IGNORES u_frac/v_frac.
        // ============================================================
        nsample(1, 2, 4'd0, 4'd0, got);
        exp = ct32_word(1, 2);
        if (got !== exp) begin
            $error("[case6 nearest] u=1 v=2 got=%08x exp=%08x", got, exp);
            errors++;
        end
        // same (u,v) but NON-ZERO frac must give the SAME nearest texel.
        nsample(1, 2, 4'd15, 4'd15, got);
        if (got !== exp) begin
            $error("[case6 nearest frac-ignored] u=1 v=2 uf=15 vf=15 got=%08x exp=%08x (frac must be ignored)", got, exp);
            errors++;
        end else
            $display("[tb_gs_texture_bilinear] CASE6 nearest pass: got=%08x (frac ignored), busy=%0b", got, n_busy);
        if (n_busy !== 1'b0) begin
            $error("[case6 nearest busy] nearest instance busy=%0b (must be 0)", n_busy);
            errors++;
        end

        // ============================================================
        // CASE 7 — PSMT8 BILINEAR halfway-U: the GUARDRAIL. taps idx(0,*)=0(red)
        //   and idx(1,*)=1(blue) -> CLUT to red/blue/red/blue, lerp blends the
        //   COLORS (purple). Result must equal the color-blend AND be NEITHER
        //   endpoint color (which is what interpolating INDICES then CLUT'ing
        //   once would give). idx(x,y)=x so vf is irrelevant.
        // ============================================================
        psm = PSMT8v; tbp0_base_bytes = T8_BASE; wms = WM_REPEAT; wmt = WM_REPEAT;
        bsample(0, 0, 4'd8, 4'd0, got);
        exp = ref_blend(CLUT0, CLUT1, CLUT0, CLUT1, 4'd8, 4'd0);
        if (got !== exp) begin
            $error("[case7 t8 color-interp] got=%08x exp(blend red,blue)=%08x", got, exp);
            errors++;
        end else if (got === CLUT0 || got === CLUT1) begin
            $error("[case7 t8 GUARDRAIL] got=%08x is an endpoint color -> indices were interpolated, not colors", got);
            errors++;
        end else
            $display("[tb_gs_texture_bilinear] CASE7 PSMT8 CLUT-before-interp pass: got=%08x (blended red<->blue, neither endpoint)", got);

        // ============================================================
        // CASE 8 — PSMT8 halfway BOTH (uf=vf=8) blue<->green at (1,1)..(2,2).
        // ============================================================
        bsample(1, 1, 4'd8, 4'd8, got);
        exp = ref_blend(CLUT1, CLUT2, CLUT1, CLUT2, 4'd8, 4'd8);
        if (got !== exp) begin
            $error("[case8 t8 halfway-both] got=%08x exp=%08x", got, exp);
            errors++;
        end else if (got === CLUT1 || got === CLUT2) begin
            $error("[case8 t8 GUARDRAIL] got=%08x endpoint color", got); errors++;
        end else
            $display("[tb_gs_texture_bilinear] CASE8 PSMT8 halfway-both pass: got=%08x (blue<->green)", got);

        // ============================================================
        // CASE 9 — PSMT8 EXACT texel center == CLUT[idx(2,2)]=green. Also the
        //   NEAREST instance (BILINEAR_ENABLE=0) must return the same CLUT color.
        // ============================================================
        bsample(2, 2, 4'd0, 4'd0, got);
        if (got !== CLUT2) begin
            $error("[case9 t8 center] got=%08x exp=CLUT[2]=%08x", got, CLUT2); errors++;
        end else
            $display("[tb_gs_texture_bilinear] CASE9 PSMT8 center pass: got=%08x == CLUT[2]", got);
        nsample(2, 2, 4'd0, 4'd0, got);
        if (got !== CLUT2) begin
            $error("[case9 t8 nearest] nearest got=%08x exp=CLUT[2]=%08x", got, CLUT2); errors++;
        end else
            $display("[tb_gs_texture_bilinear] CASE9 PSMT8 nearest x-check pass: got=%08x == CLUT[2]", got);

        // ============================================================
        // CASE 10 — PSMT4 halfway-U ACROSS A BYTE BOUNDARY. At y=0 the nibbles
        //   are: off0=low(b0)=idx0, off1=high(b0)=idx1, off2=low(b1)=idx2,
        //   off3=high(b1)=idx3. Sampling u=1 (idx1=blue, HIGH nibble of byte0)
        //   with u+1=2 (idx2=green, LOW nibble of byte1) proves nibble extraction
        //   for ALL FOUR taps + the byte-boundary crossing + CLUT-before-interp.
        // ============================================================
        psm = PSMT4v; tbp0_base_bytes = T4_BASE;
        bsample(1, 0, 4'd8, 4'd0, got);
        exp = ref_blend(CLUT1, CLUT2, CLUT1, CLUT2, 4'd8, 4'd0);
        if (got !== exp) begin
            $error("[case10 t4 nibble blend] got=%08x exp(blue,green)=%08x", got, exp);
            errors++;
        end else if (got === CLUT1 || got === CLUT2) begin
            $error("[case10 t4 GUARDRAIL] got=%08x endpoint color", got); errors++;
        end else
            $display("[tb_gs_texture_bilinear] CASE10 PSMT4 nibble + CLUT-before-interp pass: got=%08x", got);

        // ============================================================
        // CASE 11 — PSMT8 REPEAT edge: u=3,u+1 wraps to col0. idx(3,*)=3(white),
        //   idx(0,*)=0(red) -> blend white<->red.
        // ============================================================
        psm = PSMT8v; tbp0_base_bytes = T8_BASE; wms = WM_REPEAT; wmt = WM_REPEAT;
        bsample(TEXW-1, 1, 4'd8, 4'd0, got);
        exp = ref_blend(CLUT3, CLUT0, CLUT3, CLUT0, 4'd8, 4'd0);
        if (got !== exp) begin
            $error("[case11 t8 repeat] got=%08x exp(white,red)=%08x", got, exp); errors++;
        end else
            $display("[tb_gs_texture_bilinear] CASE11 PSMT8 repeat-edge pass: got=%08x", got);

        // ============================================================
        // CASE 12 — PSMT8 CLAMP edge: u=3,u+1 clamps to col3 -> all u-taps white;
        //   vf=0 -> result == CLUT[idx(3,1)]=white, and NO address reads past the
        //   last texel column.
        // ============================================================
        wms = WM_CLAMP; wmt = WM_CLAMP;
        watch_addr = 1'b1; max_addr_seen = 0;
        bsample(TEXW-1, 1, 4'd8, 4'd0, got);
        watch_addr = 1'b0;
        if (got !== CLUT3) begin
            $error("[case12 t8 clamp] got=%08x exp=CLUT[3]=white=%08x", got, CLUT3); errors++;
        end else
            $display("[tb_gs_texture_bilinear] CASE12 PSMT8 clamp-edge pass: got=%08x == white", got);
        if (max_addr_seen > $signed(T8_BASE + ((3*(TBWv*64)) + 3))) begin
            $error("[case12 t8 clamp addr] max issued=0x%08x exceeds last texel 0x%08x",
                   max_addr_seen, T8_BASE + ((3*(TBWv*64)) + 3));
            errors++;
        end else
            $display("[tb_gs_texture_bilinear] CASE12 max issued addr=0x%08x <= last texel 0x%08x",
                     max_addr_seen, T8_BASE + ((3*(TBWv*64)) + 3));

        $display("[tb_gs_texture_bilinear] errors=%0d", errors);
        if (errors == 0)
            $display("[tb_gs_texture_bilinear] PASS");
        else
            $display("[tb_gs_texture_bilinear] FAIL");
        $finish;
    end

    initial begin
        #2000000;
        $error("[tb_gs_texture_bilinear] TIMEOUT");
        $finish;
    end

endmodule : tb_gs_texture_bilinear
