// retroDE_ps2 — gs_texture_unit
//
// Per-pixel texture sampler (brick 1, the texturing datapath core).
//
// Takes a per-pixel texture coordinate (u,v) + the TEX0 descriptor, fetches
// the texel from VRAM through a read port, and outputs the sampled color,
// pipelined to absorb the VRAM read latency.
//
//   (u,v,valid) --[gs_texel_addr]--> byte addr --> VRAM read port
//                                                     |  (RD_LATENCY cyc)
//                              sampled color <--[decode]-- tex_rd_data
//
// v1 scope (kept deliberately minimal so it's fully verifiable now):
//   - PSMCT32 only (32-bit ABGR texels, direct — no CLUT).
//   - DECAL texture function (texel replaces fragment color).
//
// Ch296 — PSMT8 indexed texturing (this chapter):
//   - When psm==PSMT8 (0x13) the fetched 32-bit word holds FOUR packed
//     8-bit indices. The byte for this texel is selected by the texel
//     byte address' low 2 bits (gs_texel_addr emits a 1-byte/texel
//     address for PSMT8). That index drives a CLUT lookup port; the
//     returned PSMCT32 entry is the texel color (DECAL).
//   - The lookup is COMBINATIONAL (clut_stub's read port is comb), so it
//     lands in the SAME cycle as the direct PSMCT32 path — the existing
//     single S1->S2 register in gs_stub aligns it with emit unchanged.
//   - PSMCT32 (psm==0x00) behavior is byte-identical to before.
// Next versions add: PSMCT16 unpack, PSMT4 (nibble) + CLUT, swizzle, and
// MODULATE/HIGHLIGHT tex functions.
//
// The VRAM read port here is generic (byte address out, 32-bit word in,
// fixed RD_LATENCY). Integration wires it to vram_stub's spare read port;
// vram_stub's exact address convention is reconciled at integration time.

`timescale 1ns/1ps

module gs_texture_unit #(
    // Ch298 — SWIZZLED PSMT4 texture sampling. When PSMT4_SWIZZLE=1 AND the
    // texture psm==PSMT4, the texel byte address + nibble_hi are computed by
    // gs_swizzle_psmt4_stub (the SAME proven module already on the framebuffer
    // WRITE / SCANOUT / UPLOAD paths) using the real PS2 PSMT4 block layout,
    // instead of the linear gs_texel_addr. LINEAR is the default (0) so every
    // existing linear PSMT4/PSMT8/PSMCT32 demo + TB is byte-identical. The
    // swizzled address feeds the SAME word-aligned read, byte-lane extract,
    // nibble select, and CLUT lookup — only the address GENERATION differs.
    // Because the swizzled address (its low 2 bits + nibble_hi) is also
    // address-derived, it flows through the SAME SEL_DELAY pipe as the linear
    // selectors, so registered-read (TEX_RD_REGISTERED=1) alignment is reused
    // verbatim. PSMT8/PSMCT32 always take the linear address (this rung is
    // PSMT4-only).
    parameter bit PSMT4_SWIZZLE = 1'b0,
    // Ch299 — SWIZZLED PSMT8 texture sampling. The sibling of PSMT4_SWIZZLE,
    // MINUS the nibble (PSMT8 is 1 byte/texel). When PSMT8_SWIZZLE=1 AND the
    // texture psm==PSMT8, the texel byte address is computed by
    // gs_swizzle_psmt8_stub (the SAME proven module already on the framebuffer
    // WRITE / SCANOUT / UPLOAD paths) using the real PS2 PSMT8 block layout,
    // instead of the linear gs_texel_addr. LINEAR is the default (0) so every
    // existing linear PSMT8/PSMT4/PSMCT32 demo + TB is byte-identical. The
    // swizzled address feeds the SAME word-aligned read, byte-lane extract, and
    // CLUT lookup — only the address GENERATION differs. Because the swizzled
    // address' low 2 bits (byte-lane selector) are also address-derived, they
    // flow through the SAME SEL_DELAY pipe as the linear selectors, so
    // registered-read (TEX_RD_REGISTERED=1) alignment is reused verbatim. NO
    // nibble pipe is needed — PSMT8 has no nibble. PSMT4/PSMCT32 always take
    // their own address (this rung is PSMT8-only).
    parameter bit PSMT8_SWIZZLE = 1'b0,
    // Ch300 — SWIZZLED PSMCT32 (direct-color) texture sampling. The closure
    // rung of the swizzle layout family. When PSMCT32_SWIZZLE=1 AND the texture
    // psm==PSMCT32, the texel byte address is computed by gs_swizzle_psmct32_stub
    // (the SAME proven module already on the framebuffer WRITE / SCANOUT / UPLOAD
    // paths — Ch119/Ch122) using the real PS2 PSMCT32 page/block layout, instead
    // of the linear gs_texel_addr. Unlike PSMT4/PSMT8 this needs NO CLUT and NO
    // byte-lane select: PSMCT32 is 4 bytes/texel, so the swizzled address is
    // already word-aligned and the fetched 32-bit word IS the color directly
    // (tex_color = tex_rd_data). LINEAR is the default (0) so every existing
    // linear PSMCT32 demo + TB (textured / tritex) is byte-identical. This is
    // the SAME single-param-per-format gate as PSMCT32_SWIZZLE on the FB side,
    // so a PSMCT32 texture and a PSMCT32 framebuffer swizzle together.
    parameter bit PSMCT32_SWIZZLE = 1'b0,
    // Ch294 — GS texture WRAP MODES (REPEAT + CLAMP). When TEX_WRAP_ENABLE=1
    // the per-pixel (u,v) are resolved against the texture's power-of-two
    // dimensions (width=2^TW, height=2^TH from TEX0) using the CLAMP_1 wrap
    // mode (WMS for u/S, WMT for v/T): 0=REPEAT (u & (width-1)), 1=CLAMP
    // (u>=width -> width-1). REGION_* (2/3) are NOT modelled and pass through.
    // The wrap is applied to u/v BEFORE address generation, so it covers the
    // linear path AND every swizzle path. With TEX_WRAP_ENABLE=0 (default)
    // u_eff===u and v_eff===v as a compile-time constant, so the wrap logic is
    // pruned and every existing consumer is BYTE-IDENTICAL.
    parameter bit TEX_WRAP_ENABLE = 1'b0,
    // Ch308 — BILINEAR (4-tap) texture filtering, PSMCT32-only this rung.
    // When BILINEAR_ENABLE=1 AND psm==PSMCT32 the sampler runs a 4-beat read
    // FSM: it fetches the 4 texels surrounding the fractional coord
    //   (u,v) (u+1,v) (u,v+1) (u+1,v+1)
    // — each independently wrapped/clamped through the SAME u_eff/v_eff
    // machinery (so edge taps repeat/clamp instead of reading outside the
    // texture) — then blends them per channel (R,G,B,A) by the 4-bit
    // fractional u_frac/v_frac (0..15, /16) using a >>4 fixed-point lerp.
    // For !BILINEAR_ENABLE (default) OR psm!=PSMCT32 the EXACT current
    // single-read NEAREST path is used and u_frac/v_frac are ignored, so the
    // synthesized logic and every existing consumer is BYTE-IDENTICAL (the
    // bilinear FSM, the per-beat coord select, and the blend datapath are all
    // pruned as compile-time-dead when BILINEAR_ENABLE=0). Bilinear is
    // PSMCT32-only by default; with PALETTE_BILINEAR=1 (Ch314) it also covers
    // PSMT8/PSMT4 via per-tap CLUT-before-interp. At PALETTE_BILINEAR=0 the
    // indexed textures still take the nearest path even with BILINEAR_ENABLE=1.
    //
    // ALPHA: the alpha channel is INTERPOLATED with the same 4-tap lerp as
    // R/G/B (not pass-through-nearest). For an opaque texture (all taps a=255)
    // this returns 255 exactly; for a texel-center sample (u_frac=v_frac=0) it
    // returns the (u,v) tap's alpha exactly.
    parameter bit BILINEAR_ENABLE = 1'b0,
    // Ch314 — BILINEAR for PALETTIZED (indexed) textures. When
    // PALETTE_BILINEAR=1 (and BILINEAR_ENABLE=1) the 4-tap path also runs for
    // PSMT8 (0x13) and PSMT4 (0x14). The CRITICAL rule is CLUT-BEFORE-INTERP:
    // each of the 4 taps fetches an INDEX, that index is CLUT'd to an RGBA
    // color (the existing combinational clut_rd_idx/clut_rd_data port), and the
    // 4 COLORS are then interpolated — NOT the indices. This falls out of
    // capturing `near_color` per tap (clut_rd_data for indexed, tex_rd_data for
    // PSMCT32) instead of the raw word. Swizzled addressing + wrap/clamp run in
    // the SAME per-tap addr-gen that already feeds the nearest path, so they
    // happen BEFORE the index/CLUT lookup. Default 0 → indexed textures stay
    // nearest even with BILINEAR_ENABLE=1, so every existing build is
    // byte-identical (the combined path only ever fed PSMCT32 textures anyway).
    parameter bit PALETTE_BILINEAR = 1'b0,
    parameter int RD_LATENCY = 1,      // VRAM read latency in clk cycles
    // Ch296 — PSMT8 byte-lane realignment. The byte selected from the
    // fetched word must use the LOW 2 bits of the address that was ISSUED
    // for the returned data. When the texel ADDRESS advances every cycle
    // while a read is in flight (gs_stub TEX_RD_REGISTERED=1: address
    // taken from the S0 walker, registered read returns 1 cycle later),
    // the current `addr` no longer matches the in-flight word, so the
    // selector must be delayed by SEL_DELAY cycles to re-pair them. When
    // the address is HELD stable across the read (combinational read port,
    // address from the stable S1 latch), SEL_DELAY=0 and the current addr
    // is correct. Driven from gs_stub as TEX_RD_REGISTERED?TEX_RD_LATENCY:0.
    parameter int SEL_DELAY  = 0
) (
    input  logic        clk,
    input  logic        rst_n,

    // per-pixel texture coordinate in
    input  logic        in_valid,
    input  logic [10:0] u,
    input  logic [10:0] v,

    // Ch308 — fractional texture coords for BILINEAR (4-bit, 0..15 => /16).
    // Unused at default (BILINEAR_ENABLE=0) and for non-PSMCT32 psm.
    input  logic [3:0]  u_frac,
    input  logic [3:0]  v_frac,

    // Ch310 — RUNTIME filter select (per-primitive TEX1_1.MMAG). When
    // BILINEAR_ENABLE=1 the 4-tap path runs ONLY when (is_ct32 && filter_lin);
    // with filter_lin=0 (TEX1.MMAG=0 NEAREST) the sampler falls back to the
    // exact nearest single-read path (busy stays 0). Unused at
    // BILINEAR_ENABLE=0 (g_nearest), so the default build is byte-identical.
    input  logic        filter_lin,

    // Ch294 — wrap-mode controls (CLAMP_1 WMS/WMT + TEX0 TW/TH). Unused at
    // default (TEX_WRAP_ENABLE=0) since u_eff/v_eff collapse to u/v.
    input  logic [1:0]  wms,
    input  logic [1:0]  wmt,
    input  logic [3:0]  tw,
    input  logic [3:0]  th,

    // TEX0 descriptor
    input  logic [31:0] tbp0_base_bytes, // texture base in VRAM (bytes)
    input  logic [13:0] tbw,             // TEX0.TBW (texels/row / 64)
    input  logic [5:0]  psm,             // pixel storage mode

    // VRAM texel read port
    output logic        tex_rd_en,
    output logic [31:0] tex_rd_addr,     // byte address
    input  logic [31:0] tex_rd_data,     // 32-bit word, valid RD_LATENCY later

    // Ch296 — CLUT lookup port (PSMT8 indexed texturing). The extracted
    // 8-bit index drives `clut_rd_idx`; the parent wires this to
    // clut_stub's second (combinational) read port and returns the
    // PSMCT32 entry on `clut_rd_data`. Unused for PSMCT32 textures.
    output logic [7:0]  clut_rd_idx,
    input  logic [31:0] clut_rd_data,    // PSMCT32 entry for clut_rd_idx

    // sampled color out (aligned with out_valid)
    output logic        out_valid,
    output logic [31:0] tex_color,       // ABGR8888

    // Ch308 — BILINEAR busy: high while the 4-beat read sequence is in flight
    // (the caller must not issue a new in_valid until it drops / out_valid
    // pulses). Always 0 on the nearest path (BILINEAR_ENABLE=0 or non-PSMCT32),
    // so a caller that ignores it sees byte-identical behavior.
    output logic        busy
);

    localparam logic [5:0] PSM_PSMCT32 = 6'h00;
    localparam logic [5:0] PSM_PSMT8   = 6'h13;
    localparam logic [5:0] PSM_PSMT4   = 6'h14;

    // --- Ch294: wrap-mode resolution (u/v -> u_eff/v_eff) ---
    // Applied BEFORE any address generation so it covers the linear path AND
    // every swizzle path. width=2^TW, height=2^TH (both powers of two), so
    // REPEAT is a mask and CLAMP is a >width-1 saturate. u/v are unsigned so
    // there is no negative/underflow case to handle. REGION_* (2/3) pass
    // through unchanged (not modelled this rung). At TEX_WRAP_ENABLE=0 this is
    // a constant pass-through (u_eff===u, v_eff===v) -> byte-identical.
    // Ch308 — the coord that FEEDS the wrap. On the nearest path (bilinear off
    // or non-PSMCT32) this is the port u/v UNCHANGED, so the wrap output
    // (u_eff/v_eff) and everything downstream is byte-identical. On the
    // bilinear path it is the current beat's neighbor coord (u+du[k],v+dv[k]),
    // so each of the 4 taps is independently wrapped/clamped. `bili_active` is
    // a compile-time constant 0 when BILINEAR_ENABLE=0, so u_in===u / v_in===v
    // collapses away at the default build.
    logic        bili_active;            // declared below; bilinear running for this psm
    logic [10:0] beat_u, beat_v;         // declared below; current beat neighbor coord
    logic [10:0] u_in, v_in;
    always_comb begin
        if (BILINEAR_ENABLE && bili_active) begin
            u_in = beat_u;  v_in = beat_v;
        end else begin
            u_in = u;       v_in = v;     // byte-identical nearest coord
        end
    end

    logic [10:0] u_eff, v_eff;
    logic [10:0] u_wmask, v_wmask;     // width-1 / height-1
    logic [10:0] u_wlimit, v_wlimit;
    always_comb begin
        u_wmask  = (11'd1 << tw) - 11'd1;   v_wmask  = (11'd1 << th) - 11'd1;
        u_wlimit = u_wmask;                 v_wlimit = v_wmask;   // width-1 / height-1
        if (!TEX_WRAP_ENABLE) begin
            u_eff = u_in;  v_eff = v_in;    // pass-through -> byte-identical at default
        end else begin
            // U
            unique case (wms)
                2'd0:    u_eff = u_in & u_wmask;                       // REPEAT
                2'd1:    u_eff = (u_in > u_wlimit) ? u_wlimit : u_in;  // CLAMP
                default: u_eff = u_in;                                 // REGION_* not modelled -> pass-through
            endcase
            // V
            unique case (wmt)
                2'd0:    v_eff = v_in & v_wmask;
                2'd1:    v_eff = (v_in > v_wlimit) ? v_wlimit : v_in;
                default: v_eff = v_in;
            endcase
        end
    end

    // --- linear address (combinational) ---
    logic [31:0] lin_addr;
    logic        lin_nibble_hi;   // PSMT4: this texel is the HIGH nibble of its byte
    gs_texel_addr #(.ADDR_W(32)) u_addr (
        .base_byte_addr (tbp0_base_bytes),
        .u              (u_eff),
        .v              (v_eff),
        .tbw            (tbw),
        .psm            (psm),
        .texel_byte_addr(lin_addr),
        .nibble_hi      (lin_nibble_hi)
    );

    // --- swizzled PSMT4 address (combinational) ---
    // EXACTLY mirrors the texture-UPLOAD path (gif_image_xfer_stub Ch139):
    // the swizzle module is fed FBP=0 so it emits only the WITHIN-TEXTURE
    // byte OFFSET, and the texture base (tbp0_base_bytes) is ADDED on top.
    // This makes the sampled address bit-identical to the uploaded one for
    // ANY 256-byte-aligned base (using the swizzle module's `fbp` input here
    // would discard the low 11 bits of a non-2048-aligned base). FBW=TBW (in
    // 64-texel units); PSMT4 swizzle needs FBW even (bw_pg = FBW>>1). The
    // texture's (u,v) ARE the swizzle (x,y). Output is byte-offset + nibble_hi
    // — the SAME shape gs_texel_addr emits for linear PSMT4, so downstream
    // (word-align, byte-lane, nibble select, CLUT) is untouched.
    logic [31:0] swz_off;
    logic [31:0] swz_addr;
    logic        swz_nibble_hi;
    generate
        if (PSMT4_SWIZZLE) begin : g_swizzle4
            gs_swizzle_psmt4_stub u_swizzle4 (
                .fbp       (9'd0),
                .fbw       (tbw[5:0]),
                .x         ({1'b0, u_eff}),
                .y         ({1'b0, v_eff}),
                .addr      (swz_off),
                .nibble_hi (swz_nibble_hi)
            );
            assign swz_addr = tbp0_base_bytes + swz_off;
        end else begin : g_no_swizzle4
            assign swz_off       = 32'd0;
            assign swz_addr      = 32'd0;
            assign swz_nibble_hi = 1'b0;
        end
    endgenerate

    // --- swizzled PSMT8 address (combinational) ---
    // Ch299 — EXACTLY mirrors the PSMT4-swizzle sampler arm above (and the
    // PSMT8 UPLOAD path in gif_image_xfer_stub Ch133), MINUS the nibble.
    // gs_swizzle_psmt8_stub is fed FBP=0 so it emits only the WITHIN-TEXTURE
    // byte OFFSET; the texture base (tbp0_base_bytes) is ADDED on top. This
    // makes the sampled address bit-identical to the uploaded one for ANY
    // 256-byte-aligned base. FBW=TBW (in 64-texel units); the PSMT8 swizzle
    // needs FBW even (bw_pg = FBW>>1). The texture's (u,v) ARE the swizzle
    // (x,y). Output is a byte address — the SAME shape gs_texel_addr emits for
    // linear PSMT8 — so downstream (word-align, byte-lane, CLUT) is untouched.
    // No nibble_hi: PSMT8 is one full byte per texel.
    logic [31:0] swz8_off;
    logic [31:0] swz8_addr;
    generate
        if (PSMT8_SWIZZLE) begin : g_swizzle8
            gs_swizzle_psmt8_stub u_swizzle8 (
                .fbp  (9'd0),
                .fbw  (tbw[5:0]),
                .x    ({1'b0, u_eff}),
                .y    ({1'b0, v_eff}),
                .addr (swz8_off)
            );
            assign swz8_addr = tbp0_base_bytes + swz8_off;
        end else begin : g_no_swizzle8
            assign swz8_off  = 32'd0;
            assign swz8_addr = 32'd0;
        end
    endgenerate

    // --- swizzled PSMCT32 address (combinational) ---
    // Ch300 — direct-color sibling of the PSMT4/PSMT8 swizzle arms above, using
    // the SAME proven gs_swizzle_psmct32_stub already on the FB WRITE / SCANOUT
    // / UPLOAD paths. Fed FBP=0 so it emits only the WITHIN-TEXTURE byte OFFSET;
    // the texture base (tbp0_base_bytes) is ADDED on top, making the sampled
    // address bit-identical to the uploaded one for ANY 2048-byte-aligned base.
    // FBW=TBW (in 64-pixel units — PSMCT32 page is 64 px wide, so TBW units
    // match the stub's fbw directly, NO >>1). The texture's (u,v) ARE the
    // swizzle (x,y). Output is a 4-byte-aligned byte address — gs_texel_addr's
    // PSMCT32 shape — so downstream is untouched. NO nibble, NO byte-lane, NO
    // CLUT: the fetched word is the color (tex_color = tex_rd_data).
    logic [31:0] swz32_off;
    logic [31:0] swz32_addr;
    generate
        if (PSMCT32_SWIZZLE) begin : g_swizzle32
            gs_swizzle_psmct32_stub u_swizzle32 (
                .fbp  (9'd0),
                .fbw  (tbw[5:0]),
                .x    ({1'b0, u_eff}),
                .y    ({1'b0, v_eff}),
                .addr (swz32_off)
            );
            assign swz32_addr = tbp0_base_bytes + swz32_off;
        end else begin : g_no_swizzle32
            assign swz32_off  = 32'd0;
            assign swz32_addr = 32'd0;
        end
    endgenerate

    // --- linear-vs-swizzled select ---
    // Swizzle applies to a PSMT4 texture when PSMT4_SWIZZLE is set, and to a
    // PSMT8 texture when PSMT8_SWIZZLE is set; every other psm always takes the
    // linear address, and the two swizzle gates are mutually exclusive by psm.
    // With both params 0 the selects are constant-false, so the synthesized
    // logic — and every linear TB/demo — is byte-identical.
    logic        use_swizzle4;
    logic        use_swizzle8;
    logic        use_swizzle32;
    logic [31:0] addr;
    logic        nibble_hi;
    assign use_swizzle4  = (PSMT4_SWIZZLE   != 1'b0) && (psm == PSM_PSMT4);
    assign use_swizzle8  = (PSMT8_SWIZZLE   != 1'b0) && (psm == PSM_PSMT8);
    assign use_swizzle32 = (PSMCT32_SWIZZLE != 1'b0) && (psm == PSM_PSMCT32);
    // Only the PSMT4 path carries a nibble; PSMT8/PSMCT32 swizzle and the
    // linear fallback have none.
    assign addr         = use_swizzle4  ? swz_addr      :
                          use_swizzle8  ? swz8_addr     :
                          use_swizzle32 ? swz32_addr    : lin_addr;
    assign nibble_hi    = use_swizzle4  ? swz_nibble_hi : lin_nibble_hi;

    // Nearest-path read enable / address. These are muxed at the module
    // outputs (tex_rd_en/tex_rd_addr) below: on the nearest path they ARE the
    // outputs (byte-identical); on the bilinear path the FSM drives the
    // outputs instead. The word-align mask is a no-op for PSMCT32.
    logic        near_rd_en;
    logic [31:0] near_rd_addr;
    assign near_rd_en   = in_valid;
    // The VRAM read port is 32-bit WORD-addressed (and vram_bram_stub's
    // read2 only returns data for word-aligned addresses). PSMCT32 texel
    // addresses are already word-aligned; PSMT8 byte addresses are not, so
    // present the word-aligned address and recover the right byte via the
    // low 2 bits (sel_lo) in the index extract below. Masking is a no-op
    // for PSMCT32, so that path stays byte-identical.
    assign near_rd_addr = addr & ~32'd3;

    // --- PSMT8 index extract ---
    // gs_texel_addr returns a 1-byte/texel address for PSMT8, so the
    // fetched 32-bit word (read at addr & ~3 by the word-addressed VRAM
    // port) packs 4 indices; the issued address' low 2 bits select which
    // byte is THIS texel.
    //
    // The byte selector uses the addr[1:0] from the issue cycle of the
    // returned word. SEL_DELAY (see the param comment) is 0 when the
    // address is held stable across the read (current addr is correct) and
    // >0 when the address advances while the read is in flight (delay the
    // selector to re-pair it with the in-flight word). `sel_lo` carries it.
    //
    // PSMT4 (Ch297) adds a NIBBLE selector on top of the byte selector.
    // gs_texel_addr emits a byte address (texel_offset>>1) plus `nibble_hi`
    // (= texel_offset[0]: even texel -> LOW nibble, odd -> HIGH nibble). The
    // selected byte (via sel_lo, exactly as PSMT8) holds TWO 4-bit indices;
    // nibble_hi picks which. Because nibble_hi is derived from the texel
    // ADDRESS — which advances every cycle while a read is in flight under
    // TEX_RD_REGISTERED=1 — it must be SEL_DELAY-aligned by the SAME pipe
    // depth as sel_lo so it re-pairs with the returned word. (Same class as
    // the PSMT8 byte-lane realignment; get it wrong and odd/even texels smear.)
    logic [1:0] sel_lo;
    logic       nib_sel;          // SEL_DELAY-aligned nibble_hi
    generate
        if (SEL_DELAY == 0) begin : g_sel_comb
            assign sel_lo  = addr[1:0];
            assign nib_sel = nibble_hi;
        end else begin : g_sel_reg
            logic [1:0] sel_pipe [0:SEL_DELAY-1];
            logic       nib_pipe [0:SEL_DELAY-1];
            always_ff @(posedge clk or negedge rst_n) begin
                if (!rst_n) begin
                    for (int i = 0; i < SEL_DELAY; i++) begin
                        sel_pipe[i] <= 2'd0;
                        nib_pipe[i] <= 1'b0;
                    end
                end else begin
                    sel_pipe[0] <= addr[1:0];
                    nib_pipe[0] <= nibble_hi;
                    for (int i = 1; i < SEL_DELAY; i++) begin
                        sel_pipe[i] <= sel_pipe[i-1];
                        nib_pipe[i] <= nib_pipe[i-1];
                    end
                end
            end
            assign sel_lo  = sel_pipe[SEL_DELAY-1];
            assign nib_sel = nib_pipe[SEL_DELAY-1];
        end
    endgenerate

    // Byte select (shared by PSMT8 and PSMT4): pick the texel's byte lane.
    logic [7:0] sel_byte;
    always_comb begin
        unique case (sel_lo)
            2'b00:   sel_byte = tex_rd_data[ 7: 0];
            2'b01:   sel_byte = tex_rd_data[15: 8];
            2'b10:   sel_byte = tex_rd_data[23:16];
            default: sel_byte = tex_rd_data[31:24];
        endcase
    end

    // Nibble select for PSMT4 (4-bit index, zero-extended to 8 bits so the
    // SAME clut_rd_idx port + clut_stub feed it; CLUT entries 0..15 used).
    // iverilog-12: no bit-select on a parenthesized expr, so split into a
    // named net first, then index it.
    logic [7:0] sel_byte_for_nib;
    assign sel_byte_for_nib = sel_byte;
    logic [3:0] psmt4_nibble;
    assign psmt4_nibble = nib_sel ? sel_byte_for_nib[7:4] : sel_byte_for_nib[3:0];

    // Index out: PSMT4 -> zero-extended nibble; PSMT8 -> full byte.
    assign clut_rd_idx = (psm == PSM_PSMT4) ? {4'd0, psmt4_nibble} : sel_byte;

    // --- valid pipeline matching the read latency ---
    // in_valid presented with the address this cycle; tex_rd_data for it
    // arrives RD_LATENCY cycles later. Delay valid by the same amount.
    logic [RD_LATENCY-1:0] valid_pipe;
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            valid_pipe <= '0;
        end else begin
            if (RD_LATENCY == 1)
                valid_pipe[0] <= in_valid;
            else begin
                valid_pipe[0] <= in_valid;
                for (int i = 1; i < RD_LATENCY; i++)
                    valid_pipe[i] <= valid_pipe[i-1];
            end
        end
    end

    logic        near_out_valid;
    assign near_out_valid = valid_pipe[RD_LATENCY-1];

    // --- decode (DECAL) ---
    //   PSMT4   : texel color = CLUT[nibble]     (indexed indirection)
    //   PSMT8   : texel color = CLUT[byte index] (indexed indirection)
    //   PSMCT32 : texel word IS the color directly (byte-identical to v1)
    logic [31:0] near_color;
    assign near_color = (psm == PSM_PSMT8 || psm == PSM_PSMT4)
                      ? clut_rd_data : tex_rd_data;

    // ========================================================================
    // Ch308 — BILINEAR (4-tap) PSMCT32 FILTER
    // ========================================================================
    // When BILINEAR_ENABLE=1 and psm==PSMCT32 we sample the 4 texels around the
    // fractional coord and blend them. The whole block is wrapped in a generate
    // that is empty when BILINEAR_ENABLE=0, so the default build is pruned to
    // exactly the nearest path and is BYTE-IDENTICAL.
    //
    // CYCLE SCHEDULE (RD_LATENCY-aware; example RD_LATENCY=L):
    //   T0           : caller asserts in_valid (with u,v,u_frac,v_frac). FSM in
    //                  IDLE latches u/v/frac, sets beat index k=0, drives
    //                  bili_active=1, busy=1, moves to ISSUE.
    //   T0+ (ISSUE)  : present neighbor[k] coord (beat_u/beat_v -> wrap ->
    //                  gs_texel_addr -> tex_rd_addr) and pulse tex_rd_en for 1
    //                  cycle; start an L-cycle wait; -> WAIT.
    //   ISSUE+1..+L  : WAIT counts L cycles; on the L-th cycle tex_rd_data holds
    //                  beat[k]'s 32-bit ABGR word -> capture into tap[k].
    //                  If k<3: k++ and -> ISSUE (next neighbor). If k==3: -> DONE.
    //   DONE         : combinationally lerp the 4 captured taps by u_frac/v_frac
    //                  per channel and capture the result in tex_color_hold.
    //   OUT          : assert out_valid for 1 cycle with the registered color;
    //                  -> IDLE.
    //   => total ~ 4*(1+L)+2 cycles per filtered sample. Throughput is NOT a
    //      goal here (a later texture-cache pass collapses the 4 reads).
    //
    // Neighbor table (k -> du,dv): 0->(0,0) 1->(1,0) 2->(0,1) 3->(1,1).
    // Each neighbor coord is fed through the SAME u_eff/v_eff wrap (via
    // u_in/v_in above) so edge taps repeat/clamp and never read outside the
    // texture (proven in the TB clamp/repeat cases).
    //
    // lerp(a,b,f) = a + (($signed({1'b0,b}) - $signed({1'b0,a})) * $signed({1'b0,f})) >>> 4
    //   with f the 4-bit frac (0..15 => /16). a,b are 8-bit channels. The
    //   bracketed product is computed in a SIGNED temp (no bit-select on a
    //   parenthesized expr — iverilog-12 rule), then arithmetic-shifted >>>4,
    //   then defensively clamped to 0..255.
    generate
    if (BILINEAR_ENABLE) begin : g_bilinear
        localparam logic [2:0] BS_IDLE  = 3'd0;
        localparam logic [2:0] BS_ISSUE = 3'd1;
        localparam logic [2:0] BS_WAIT  = 3'd2;
        localparam logic [2:0] BS_DONE  = 3'd3;
        localparam logic [2:0] BS_OUT   = 3'd4;

        logic [2:0]  state;
        logic [1:0]  beat;               // which neighbor 0..3
        logic [31:0] wait_cnt;           // counts RD_LATENCY
        logic [31:0] tap [0:3];          // captured ABGR per neighbor
        logic [10:0] lat_u, lat_v;       // latched coord for this sample
        logic [3:0]  lat_uf, lat_vf;     // latched fracs

        // is this a PSMCT32 sample? bilinear runs for PSMCT32 always, and (Ch314)
        // for PSMT8/PSMT4 when PALETTE_BILINEAR=1; any other psm falls back to the
        // nearest path even with BILINEAR_ENABLE=1.
        logic        is_ct32;
        logic        is_indexed;
        logic        bili_psm_ok;
        assign is_ct32     = (psm == PSM_PSMCT32);
        assign is_indexed  = (psm == PSM_PSMT8) || (psm == PSM_PSMT4);
        assign bili_psm_ok = is_ct32 || (PALETTE_BILINEAR && is_indexed);

        // Ch310 — RUNTIME filter gate. The 4-tap path runs ONLY for a PSMCT32
        // texture whose primitive selected LINEAR magnification (filter_lin=1,
        // i.e. TEX1.MMAG=1). With filter_lin=0 (NEAREST) we fall back to the
        // single-read nearest path even with BILINEAR_ENABLE=1, so an
        // MMAG=0 primitive stays nearest. `do_lin` is the single predicate that
        // selects the bilinear datapath everywhere below.
        //
        // NOTE on the `!== 1'b0` test: it makes filter_lin DEFAULT-ON when the
        // port is left UNCONNECTED (sim Z). The standalone tb_gs_texture_bilinear
        // exercises the 4-tap path directly without driving filter_lin, so an
        // unconnected input must keep bilinear running (Z !== 0 → true). A
        // driven 0 (gs_stub MMAG=0) gives nearest; a driven 1 gives bilinear.
        // In synthesis filter_lin is always driven by gs_stub, so this reduces
        // to a plain `is_ct32 && filter_lin`.
        logic        do_lin;
        assign do_lin = bili_psm_ok && (filter_lin !== 1'b0);

        // bili_active (read by the wrap mux above): high whenever a filtered
        // PSMCT32 sample is being processed by the FSM (ISSUE/WAIT/DONE) so the
        // wrap consumes the per-beat neighbor coord. When do_lin=0 it is low so
        // the wrap uses the port u/v (nearest), byte-identical to the
        // non-bilinear coord path.
        assign bili_active = do_lin;

        // neighbor delta for the current beat
        logic [10:0] du, dv;
        always_comb begin
            unique case (beat)
                2'd0: begin du = 11'd0; dv = 11'd0; end
                2'd1: begin du = 11'd1; dv = 11'd0; end
                2'd2: begin du = 11'd0; dv = 11'd1; end
                default: begin du = 11'd1; dv = 11'd1; end
            endcase
        end
        // beat coord feeds the wrap (u_in/v_in). In IDLE (before latching) use
        // the live ports so the first ISSUE sees neighbor 0 of the live coord;
        // once latched, use the latched coord.
        always_comb begin
            if (state == BS_IDLE) begin
                beat_u = u   + du;       // beat==0 here -> u+0
                beat_v = v   + dv;
            end else begin
                beat_u = lat_u + du;
                beat_v = lat_v + dv;
            end
        end

        // The bilinear read address reuses the SAME addr-gen (gs_texel_addr via
        // the u_eff/v_eff wrap fed by beat_u/beat_v). near_rd_addr already is
        // (addr & ~3) for the currently-selected coord; for PSMCT32 the linear
        // path is used and it is word-aligned. We pulse rd_en only on ISSUE.
        logic        bi_rd_en;
        assign bi_rd_en = (state == BS_ISSUE);

        always_ff @(posedge clk or negedge rst_n) begin
            if (!rst_n) begin
                state    <= BS_IDLE;
                beat     <= 2'd0;
                wait_cnt <= 32'd0;
                lat_u    <= 11'd0;  lat_v  <= 11'd0;
                lat_uf   <= 4'd0;   lat_vf <= 4'd0;
                for (int i = 0; i < 4; i++) tap[i] <= 32'd0;
            end else begin
                unique case (state)
                    BS_IDLE: begin
                        if (in_valid && do_lin) begin
                            lat_u  <= u;   lat_v  <= v;
                            lat_uf <= u_frac; lat_vf <= v_frac;
                            beat   <= 2'd0;
                            state  <= BS_ISSUE;
                        end
                    end
                    BS_ISSUE: begin
                        // address presented this cycle (combinationally via
                        // beat -> beat_u/beat_v -> wrap -> addr). Begin the
                        // RD_LATENCY wait.
                        wait_cnt <= 32'd1;
                        state    <= BS_WAIT;
                    end
                    BS_WAIT: begin
                        if (wait_cnt >= RD_LATENCY[31:0]) begin
                            // tex_rd_data now holds beat's word. Capture the
                            // resolved COLOR (`near_color`): for PSMCT32 that is
                            // the raw word (byte-identical to the original);
                            // for PSMT8/PSMT4 (Ch314) it is clut_rd_data — the
                            // index extracted from this beat's word (sel_byte /
                            // psmt4_nibble, stable across the held beat) then CLUT'd.
                            // Capturing the CLUT'd color per tap is what makes the
                            // downstream lerp interpolate COLORS, not indices.
                            tap[beat] <= near_color;
                            if (beat == 2'd3) begin
                                state <= BS_DONE;
                            end else begin
                                beat  <= beat + 2'd1;
                                state <= BS_ISSUE;
                            end
                        end else begin
                            wait_cnt <= wait_cnt + 32'd1;
                        end
                    end
                    BS_DONE: begin
                        // tex_color_hold captures the finished blend on this
                        // edge.  Keep a distinct OUT cycle so no consumer can
                        // see the tap->two-lerp combinational cone directly.
                        state <= BS_OUT;
                    end
                    default: begin // BS_OUT
                        state <= BS_IDLE;
                    end
                endcase
            end
        end

        // --- 4-tap blend (combinational, on the captured taps) ---
        // PSMCT32 word layout: [31:24]=A [23:16]=B [15:8]=G [7:0]=R (ABGR8888).
        // tap0=(u,v) tap1=(u+1,v) tap2=(u,v+1) tap3=(u+1,v+1).
        function automatic logic [7:0] lerp8(input logic [7:0] a,
                                             input logic [7:0] b,
                                             input logic [3:0] f);
            logic signed [16:0] diff;     // b-a, signed, range -255..255
            logic signed [21:0] prod;     // diff*f
            logic signed [21:0] shifted;  // prod >>> 4
            logic signed [21:0] res;      // a + shifted
            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;
                // For 8-bit taps a,b and f in 0..15, res = a + floor((b-a)*f/16) always lands
                // in [min(a,b),max(a,b)] subset [0,255] (worst cases a=255,b=0,f=15 -> 15;
                // a=1,b=0,f=15 -> 0), so the old 0/255 clamp NEVER fired. Its comparators formed
                // a 161-node combinational loop (Codex STA) that failed design-clock setup by 42 ps.
                // Return the computed byte directly: functionally identical, breaks the loop.
                lerp8 = res[7:0];
            end
        endfunction

        // per-channel taps
        logic [7:0] t0_r, t0_g, t0_b, t0_a;
        logic [7:0] t1_r, t1_g, t1_b, t1_a;
        logic [7:0] t2_r, t2_g, t2_b, t2_a;
        logic [7:0] t3_r, t3_g, t3_b, t3_a;
        assign t0_r = tap[0][ 7: 0]; assign t0_g = tap[0][15: 8];
        assign t0_b = tap[0][23:16]; assign t0_a = tap[0][31:24];
        assign t1_r = tap[1][ 7: 0]; assign t1_g = tap[1][15: 8];
        assign t1_b = tap[1][23:16]; assign t1_a = tap[1][31:24];
        assign t2_r = tap[2][ 7: 0]; assign t2_g = tap[2][15: 8];
        assign t2_b = tap[2][23:16]; assign t2_a = tap[2][31:24];
        assign t3_r = tap[3][ 7: 0]; assign t3_g = tap[3][15: 8];
        assign t3_b = tap[3][23:16]; assign t3_a = tap[3][31:24];

        // top = lerp(tap0,tap1,uf); bot = lerp(tap2,tap3,uf); out = lerp(top,bot,vf)
        logic [7:0] top_r, top_g, top_b, top_a;
        logic [7:0] bot_r, bot_g, bot_b, bot_a;
        logic [7:0] cv_r,  cv_g,  cv_b,  cv_a;
        always_comb begin
            top_r = lerp8(t0_r, t1_r, lat_uf);
            top_g = lerp8(t0_g, t1_g, lat_uf);
            top_b = lerp8(t0_b, t1_b, lat_uf);
            top_a = lerp8(t0_a, t1_a, lat_uf);
            bot_r = lerp8(t2_r, t3_r, lat_uf);
            bot_g = lerp8(t2_g, t3_g, lat_uf);
            bot_b = lerp8(t2_b, t3_b, lat_uf);
            bot_a = lerp8(t2_a, t3_a, lat_uf);
            cv_r  = lerp8(top_r, bot_r, lat_vf);
            cv_g  = lerp8(top_g, bot_g, lat_vf);
            cv_b  = lerp8(top_b, bot_b, lat_vf);
            cv_a  = lerp8(top_a, bot_a, lat_vf);
        end

        // Ch310/Ch422 — HOLD register for the filtered color. The combined-renderer
        // FSM (gs_stub CB_TWAIT) may latch the result a cycle or two AFTER the
        // out_valid pulse (it steps at half-rate on z_advance beats), so the
        // blended ABGR must stay STABLE from out_valid until the next sample.
        // The register captures the blend on the clk edge that LEAVES DONE.
        // Ch422 makes OUT a separate following cycle, so tex_color and
        // out_valid expose only this registered value.  That is the timing
        // boundary between the two-stage bilinear arithmetic and downstream
        // TEX0 modulation; callers already wait on out_valid or !busy.
        logic [31:0] tex_color_blend;
        assign tex_color_blend = {cv_a, cv_b, cv_g, cv_r};
        logic [31:0] tex_color_hold;
        always_ff @(posedge clk or negedge rst_n) begin
            if (!rst_n)
                tex_color_hold <= 32'd0;
            else if (state == BS_DONE)
                tex_color_hold <= tex_color_blend;   // capture the just-blended value
        end
        // Registered result, held until the next sample completes.
        logic [31:0] tex_color_lin;
        assign tex_color_lin = tex_color_hold;

        // --- output mux: bilinear FSM owns the outputs for a FILTERED PSMCT32
        // sample (do_lin). When do_lin=0 — non-PSMCT32 psm OR MMAG=0 NEAREST —
        // we transparently fall back to the nearest single-read path so
        // PSMT8/PSMT4/swizzle and nearest PSMCT32 still work with
        // BILINEAR_ENABLE=1, and busy stays 0 there.
        // tex_color: the HELD blended ABGR (stable from out_valid to next DONE).
        assign tex_rd_en   = do_lin ? bi_rd_en      : near_rd_en;
        // tex_rd_addr is the SAME addr-gen output for both paths (the wrap
        // selects beat_u/beat_v vs port u/v); the FSM just gates rd_en.
        assign tex_rd_addr = near_rd_addr;
        assign out_valid   = do_lin ? (state == BS_OUT) : near_out_valid;
        assign tex_color   = do_lin ? tex_color_lin : near_color;
        assign busy        = do_lin && (state != BS_IDLE);
    end else begin : g_nearest
        // BYTE-IDENTICAL nearest path: outputs are exactly the original assigns.
        assign bili_active = 1'b0;       // constant -> wrap uses port u/v
        assign beat_u      = 11'd0;      // unused (pruned)
        assign beat_v      = 11'd0;
        assign tex_rd_en   = near_rd_en;
        assign tex_rd_addr = near_rd_addr;
        assign out_valid   = near_out_valid;
        assign tex_color   = near_color;
        assign busy        = 1'b0;
    end
    endgenerate

endmodule : gs_texture_unit
