// retroDE_ps2 — tb_gs_texture_psmt4_clut (Ch297)
//
// Focused TB for PSMT4 indexed texture sampling: gs_texture_unit fetches a
// 4-bit INDEX from VRAM (PSMT4, 2 indices packed per byte, 8 per 32-bit
// word) and looks it up in clut_stub to produce a PSMCT32 color (DECAL).
// This is the PSMT4 rung built directly on top of the PSMT8 CLUT sampler
// (Ch296): the ONLY new bit is the nibble extraction (+ its SEL_DELAY
// alignment). The four architect-required proofs:
//
//   PROOF 1 — NIBBLE CONVENTION LOCKED: gs_texel_addr emits nibble_hi =
//             texel_offset[0], so EVEN texel -> LOW nibble, ODD texel ->
//             HIGH nibble. We assert specific (u,v) -> nibble -> index ->
//             color, and (crucially) that two horizontally-adjacent texels
//             sharing a byte read the LOW then the HIGH nibble.
//   PROOF 2 — CLUT lookup is correct for 4-bit indices (0..15): index ->
//             clut_stub[index] -> expected PSMCT32 color on tex_color.
//   PROOF 3 — palette indirection is REAL: re-load the CLUT with a
//             different palette over the SAME index texture; the output
//             colors change while the fetched indices are identical.
//   PROOF 4 — PSMT8 AND PSMCT32 paths STILL pass unchanged (bit-identical):
//             same DUT, same index texture re-read as PSMT8, and a PSMCT32
//             direct texture, both byte-identical to the Ch296 contract.
//
// SEL_DELAY=1 (TEX_RD_REGISTERED-style) is exercised in a second DUT so the
// nibble selector's pipe alignment is proven, not just the comb path.

`timescale 1ns/1ps

module tb_gs_texture_psmt4_clut;

    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;

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

    // sampler read port (byte addr in, 32-bit word back, combinational).
    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_byte(input logic [31:0] a, input logic [7:0] d);
        vram[a] = d;
    endtask
    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

    // ---- DUT inputs (shared by the SEL_DELAY=0 DUT under test) ----
    logic        in_valid;
    logic [10:0] u, v;
    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;

    gs_texture_unit #(.RD_LATENCY(1), .SEL_DELAY(0)) dut (
        .clk(clk), .rst_n(rst_n),
        .in_valid(in_valid),
        .u(u), .v(v),
        .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)
    );

    // ---- CLUT palette table (the reused on-chip CLUT) ----
    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

    // ---- texture geometry ----
    // 8x8 PSMT4 texture at byte base 0x0800. TBW=1 -> 64 texels/row stride.
    localparam int    TEXW  = 8;
    localparam int    TEXH  = 8;
    localparam [31:0] T4_BASE   = 32'h0000_0800;
    localparam [13:0] T4_TBW    = 14'd1;
    localparam [31:0] CT32_BASE = 32'h0000_2000;

    // Index pattern: use MANY of the 16 indices so the nibble + CLUT path
    // is well-exercised. idx = (x + 2*y) mod 16 -> ramps across all 16.
    function automatic logic [7:0] idx_pattern(input int x, input int y);
        return 8'((x + 2*y) % 16);
    endfunction

    // Palette A / B — distinct PSMCT32 ABGR for all 16 indices.
    function automatic logic [31:0] palA(input logic [7:0] i);
        return 32'hFF000000 | ({24'd0, i} * 32'h00111111) | 32'h00000010;
    endfunction
    function automatic logic [31:0] palB(input logic [7:0] i);
        return 32'hFF000000 | ((15 - {24'd0, i}) * 32'h00080402) | 32'h00000020;
    endfunction

    // ---- drive one sample, capture color (comb path: SEL_DELAY=0) ----
    task automatic sample(input int su, input int sv, output logic [31:0] col);
        @(negedge clk);
        in_valid = 1'b1;
        u = 11'(su);
        v = 11'(sv);
        #2;                       // settle the combinational sampler chain
        col = tex_color;
    endtask

    int errors;
    initial errors = 0;
    bit sel_delay_done;
    initial sel_delay_done = 1'b0;

    // observed indices for the palette-indirection proof.
    logic [7:0] idx_obs_A [0:TEXH-1][0:TEXW-1];
    logic [7:0] idx_obs_B [0:TEXH-1][0:TEXW-1];

    logic [31:0] got, exp;
    int x, y;

    // ===== helpers to upload textures =====
    task automatic upload_psmt4_texture;
        // 2 texels/byte: even x -> low nibble, odd x -> high nibble.
        // Pack both nibbles of each byte for the pair (2x, 2x+1).
        for (y = 0; y < TEXH; y++) begin
            for (x = 0; x < TEXW; x += 2) begin
                logic [3:0] lo, hi;
                logic [31:0] byte_addr;
                lo = idx_pattern(x,   y);          // 0..15, truncates to [3:0]
                hi = idx_pattern(x+1, y);          // odd -> high nibble
                // PSMT4 byte addr = base + (texel_offset>>1), texel_offset =
                // v*row_texels + x ; for the pair the two texels share one byte.
                byte_addr = T4_BASE + ((y * (T4_TBW * 64)) + x) / 2;
                poke_byte(byte_addr, {hi, lo});
            end
        end
    endtask

    initial begin
        rst_n    = 1'b0;
        in_valid = 1'b0;
        u = 0; v = 0;
        tbp0_base_bytes = T4_BASE;
        tbw = T4_TBW;
        psm = PSMT4;
        clut_we = 1'b0; clut_widx = 0; clut_wdata = 0;
        for (int i = 0; i < VRAM_BYTES; i++) vram[i] = 8'd0;

        upload_psmt4_texture();

        // A PSMCT32 texture for PROOF 4 (distinct word per texel).
        for (y = 0; y < TEXH; y++)
            for (x = 0; x < TEXW; x++)
                poke_word(CT32_BASE + ((y * (T4_TBW * 64)) + x) * 4,
                          32'hFF000000 | (x << 4) | (y << 12));

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

        // Load Palette A for all 16 indices.
        for (int i = 0; i < 16; i++) clut_write(8'(i), palA(8'(i)));
        psm = PSMT4;
        tbp0_base_bytes = T4_BASE;

        // ============================================================
        // PROOF 1 (nibble convention) + PROOF 2 (CLUT lookup, idx 0..15).
        // ============================================================
        for (y = 0; y < TEXH; y++) begin
            for (x = 0; x < TEXW; x++) begin
                logic [7:0] idx;
                logic       exp_nib_hi;
                idx        = idx_pattern(x, y);
                exp_nib_hi = x[0];        // even x -> low (0), odd x -> high (1)
                sample(x, y, got);
                // PROOF 1a: the sampler picked the right NIBBLE -> right index.
                if (clut_rd_idx !== {4'd0, idx[3:0]}) begin
                    $error("[P1 nibble] (%0d,%0d) nib_hi_exp=%0b sampler idx=0x%02x expected 0x%02x",
                           x, y, exp_nib_hi, clut_rd_idx, idx[3:0]);
                    errors++;
                end
                // PROOF 1b: the high 4 bits of the index port are ZERO
                //           (4-bit index zero-extended, not a stray byte).
                if (clut_rd_idx[7:4] !== 4'd0) begin
                    $error("[P1 zext] (%0d,%0d) clut_rd_idx upper nibble nonzero =0x%01x",
                           x, y, clut_rd_idx[7:4]);
                    errors++;
                end
                idx_obs_A[y][x] = clut_rd_idx;
                // PROOF 2: index -> palette A color.
                exp = palA(idx[3:0]);
                if (got !== exp) begin
                    $error("[P2 lkup A] (%0d,%0d) idx=0x%01x got=%08x exp=%08x",
                           x, y, idx[3:0], got, exp);
                    errors++;
                end
            end
        end

        // PROOF 1c — explicit adjacent-pair check: texels (0,0) and (1,0)
        // share byte 0; (0,0) MUST read the low nibble, (1,0) the high
        // nibble. idx_pattern(0,0)=0, idx_pattern(1,0)=1 -> distinct.
        sample(0, 0, got);
        if (clut_rd_idx !== 8'd0) begin
            $error("[P1c low-nibble] texel(0,0) idx=0x%02x exp 0x00", clut_rd_idx); errors++;
        end
        sample(1, 0, got);
        if (clut_rd_idx !== 8'd1) begin
            $error("[P1c high-nibble] texel(1,0) idx=0x%02x exp 0x01", clut_rd_idx); errors++;
        end

        // ============================================================
        // PROOF 3 — reload a DIFFERENT palette over the SAME texture.
        // ============================================================
        for (int i = 0; i < 16; i++) clut_write(8'(i), palB(8'(i)));
        for (y = 0; y < TEXH; y++) begin
            for (x = 0; x < TEXW; x++) begin
                logic [7:0] idx;
                idx = idx_pattern(x, y);
                sample(x, y, got);
                idx_obs_B[y][x] = clut_rd_idx;
                if (idx_obs_B[y][x] !== idx_obs_A[y][x]) begin
                    $error("[P3 idx-stable] (%0d,%0d) idxA=0x%02x idxB=0x%02x — indices changed!",
                           x, y, idx_obs_A[y][x], idx_obs_B[y][x]);
                    errors++;
                end
                exp = palB(idx[3:0]);
                if (got !== exp) begin
                    $error("[P3 lkup B] (%0d,%0d) idx=0x%01x got=%08x exp=%08x",
                           x, y, idx[3:0], got, exp);
                    errors++;
                end
                if (palA(idx[3:0]) !== palB(idx[3:0]) && got === palA(idx[3:0])) begin
                    $error("[P3 changed] (%0d,%0d) idx=0x%01x output still palette-A color %08x",
                           x, y, idx[3:0], got);
                    errors++;
                end
            end
        end

        // ============================================================
        // PROOF 4a — PSMT8 path UNCHANGED (bit-identical to Ch296). Read
        // the SAME VRAM region as PSMT8 (1 byte/texel): the sampler must
        // return the FULL BYTE as the index (NO nibble split). The ground
        // truth is the raw VRAM byte at the PSMT8 byte address the address
        // generator computes = T4_BASE + y*(TBW*64) + x. (PSMT8 stride is
        // 64 bytes/row vs PSMT4's 64 texels=32 bytes/row, so we predict
        // from the VRAM model directly rather than from idx_pattern.)
        // ============================================================
        psm = PSMT8;
        tbp0_base_bytes = T4_BASE;
        for (y = 0; y < TEXH; y++) begin
            for (x = 0; x < TEXW; x++) begin
                logic [7:0] exp_byte;
                logic [31:0] ba;
                ba = T4_BASE + (y * (T4_TBW * 64)) + x;   // PSMT8 byte addr
                exp_byte = vram[ba];
                sample(x, y, got);
                if (clut_rd_idx !== exp_byte) begin
                    $error("[P4 psmt8] (%0d,%0d) byte idx=0x%02x exp 0x%02x (full byte, no nibble split)",
                           x, y, clut_rd_idx, exp_byte);
                    errors++;
                end
            end
        end

        // PROOF 4b — PSMCT32 DIRECT path unchanged (no CLUT indirection).
        psm = PSMCT32;
        tbp0_base_bytes = CT32_BASE;
        for (y = 0; y < TEXH; y++) begin
            for (x = 0; x < TEXW; x++) begin
                sample(x, y, got);
                exp = 32'hFF000000 | (x << 4) | (y << 12);
                if (got !== exp) begin
                    $error("[P4 ct32] (%0d,%0d) got=%08x exp=%08x (direct word, no CLUT)",
                           x, y, got, exp);
                    errors++;
                end
            end
        end

        // wait for the concurrent SEL_DELAY=1 proof to fold its result in.
        wait (sel_delay_done == 1'b1);

        $display("[tb_gs_texture_psmt4_clut] PSMT4 samples=%0d palettes=2 PSMT8/PSMCT32 cross-checked errors=%0d",
                 TEXW*TEXH*2, errors);
        if (errors == 0)
            $display("[tb_gs_texture_psmt4_clut] PASS");
        else
            $display("[tb_gs_texture_psmt4_clut] FAIL");
        $finish;
    end

    // ================================================================
    // SEL_DELAY=1 micro-proof (registered-read alignment).
    //
    // Under TEX_RD_REGISTERED the texel ADDRESS advances every cycle while
    // the read is in flight, so both the byte selector (sel_lo) AND the new
    // nibble selector (nib_sel) must be delayed one cycle to re-pair with
    // the returned word. We model a 1-cycle REGISTERED read here (RD_LATENCY
    // =1, SEL_DELAY=1, data returns one cycle after the address) and drive a
    // contiguous (u,v) scan so consecutive odd/even texels are in flight.
    // The expected index for the data arriving THIS cycle is the one issued
    // LAST cycle — a misaligned nibble would smear odd<->even.
    // ================================================================
    logic        d2_in_valid;
    logic [10:0] d2_u, d2_v;
    logic [31:0] d2_rd_addr;
    logic [31:0] d2_rd_data_q;      // REGISTERED read: 1-cycle delayed word
    logic [7:0]  d2_clut_idx;
    logic [31:0] d2_clut_data;
    logic        d2_out_valid;
    logic [31:0] d2_tex_color;

    // registered read off the SAME vram model
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) d2_rd_data_q <= 32'd0;
        else d2_rd_data_q <= { vram[(d2_rd_addr & ~32'd3) + 3],
                               vram[(d2_rd_addr & ~32'd3) + 2],
                               vram[(d2_rd_addr & ~32'd3) + 1],
                               vram[(d2_rd_addr & ~32'd3) + 0] };
    end

    gs_texture_unit #(.RD_LATENCY(1), .SEL_DELAY(1)) dut2 (
        .clk(clk), .rst_n(rst_n),
        .in_valid(d2_in_valid),
        .u(d2_u), .v(d2_v),
        .tbp0_base_bytes(T4_BASE),
        .tbw(T4_TBW),
        .psm(PSMT4),
        .tex_rd_en(),
        .tex_rd_addr(d2_rd_addr),
        .tex_rd_data(d2_rd_data_q),
        .clut_rd_idx(d2_clut_idx),
        .clut_rd_data(d2_clut_data),
        .out_valid(d2_out_valid),
        .tex_color(d2_tex_color)
    );

    clut_stub u_clut2 (
        .clk(clk), .rst_n(rst_n),
        .write_en (clut_we),         // share palette-A writes from main proc
        .write_idx(clut_widx),
        .write_data(clut_wdata),
        .read_idx (8'd0), .read_data(),
        .tex_read_idx (d2_clut_idx),
        .tex_read_data(d2_clut_data)
    );

    int errors2;
    initial errors2 = 0;

    initial begin
        d2_in_valid = 1'b0; d2_u = 0; d2_v = 0;
        // wait for palette A to be loaded + the main scan to be underway.
        wait (rst_n == 1'b1);
        repeat (40) @(posedge clk);    // palette A is loaded by now

        // Drive a contiguous row scan; data for the address issued at cycle
        // N arrives at the DUT inputs at cycle N+1, and the DUT's SEL_DELAY=1
        // pipe presents sel_lo/nib_sel issued at N. So the index the DUT
        // resolves at cycle N+1 is idx_pattern(x issued at N).
        for (int xx = 0; xx < TEXW; xx++) begin
            @(negedge clk);
            d2_in_valid = 1'b1;
            d2_u = 11'(xx);
            d2_v = 11'(0);
        end
        @(negedge clk);              // let the LAST issued addr's data land
        // After the loop, the data on d2_rd_data_q corresponds to the last
        // issued address (xx=TEXW-1) and the pipe presents its sel/nib. Check.
        begin
            logic [7:0] exp_last;
            exp_last = idx_pattern(TEXW-1, 0);    // 0..15
            #2;
            if (d2_clut_idx !== {4'd0, exp_last}) begin
                $error("[SEL_DELAY=1] last-texel idx=0x%02x exp 0x%02x (nibble re-pair failed)",
                       d2_clut_idx, exp_last);
                errors2++;
            end
        end

        // Now sweep each texel with the address HELD for 2 cycles so the
        // registered word is stable when we sample -> deterministic check of
        // BOTH nibbles across the whole row (odd + even).
        for (int xx = 0; xx < TEXW; xx++) begin
            logic [7:0] expi;
            @(negedge clk);
            d2_in_valid = 1'b1;
            d2_u = 11'(xx);
            d2_v = 11'(0);
            @(posedge clk);          // address registered into read
            @(negedge clk);          // data + pipe now aligned
            #2;
            expi = idx_pattern(xx, 0);            // 0..15
            if (d2_clut_idx !== {4'd0, expi}) begin
                $error("[SEL_DELAY=1 held] x=%0d idx=0x%02x exp 0x%02x (nib_sel mis-pipe)",
                       xx, d2_clut_idx, expi);
                errors2++;
            end
        end
        d2_in_valid = 1'b0;

        if (errors2 == 0)
            $display("[tb_gs_texture_psmt4_clut] SEL_DELAY=1 nibble-align PASS");
        else
            $display("[tb_gs_texture_psmt4_clut] SEL_DELAY=1 nibble-align FAIL errors=%0d", errors2);
        // fold into the main error tally (main initial waits on sel_delay_done)
        errors = errors + errors2;
        sel_delay_done = 1'b1;
    end

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

endmodule : tb_gs_texture_psmt4_clut
