// retroDE_ps2 — clut_loader_stub (Ch99 + Ch100 + Ch101)
//
// VRAM→CLUT load engine triggered by GIF TEX0.CLD. Watches the
// 1-cycle `tex0_wr_pulse` from gs_stub and starts a 256-entry
// load when the just-written TEX0 satisfies all three:
//   - CSM == 1                (CSM2 linear; CSM1 swizzle deferred)
//   - CPSM ∈ {PSMCT32, PSMCT16}
//   - CLD permits a load under the change-detect policy:
//       0 = never, 1 = always,
//       2 = CBP changed since last load,
//       3 = CBP, CPSM, or CSA changed since last load,
//       4 = always, but write only the 16-entry CSA window
//           (Ch102) — destination indices CSA*16..CSA*16+15
//           wrap mod 256; the rest of clut_stub is preserved.
//       5..7 = reserved/edge cases at this scope (no-op).
// Per-CPSM stride: PSMCT32 reads 4 bytes/entry from
// VRAM[CBP*256 + i*4]; PSMCT16 reads 2 bytes/entry from
// VRAM[CBP*256 + i*2] and unpacks RGB5A1 → PSMCT32 ABGR with
// 5→8 bit-replicate. clut_stub always sees PSMCT32 entries.
//
// Scope (Ch99 + Ch100):
//   - CSM2 (linear addressing) only — entry i lives at byte
//     offset i*entry_stride from CBP*256, where entry_stride is
//     4 (PSMCT32) or 2 (PSMCT16). The loader explicitly gates
//     start on tex0_csm == 1'b1 (CSM2). A TEX0_1 write with
//     CSM=0 (CSM1, 16×16 grid swizzle) is silently ignored at
//     this scope rather than performing a wrong linear load.
//   - CPSM=PSMCT32 (=0) and CPSM=PSMCT16 (=2) accepted. PSMCT16
//     entries are unpacked from RGB5A1 to PSMCT32 ABGR via 5→8
//     bit-replicate ({c5, c5[4:2]}) so clut_stub always stores
//     PSMCT32 regardless of source format and pcrtc's existing
//     PSMT8+CLUT lookup path stays unchanged. Alpha is replicated
//     across 8 bits ({8{a1}}). Other CPSM codes (PSMCT24, PSMT8H,
//     etc.) are silently ignored.
//   - CLD modes (Ch101 + Ch102): full conditional policy
//     honored for CLD ∈ {0, 1, 2, 3, 4}.
//       0 = no load.
//       1 = always load — full 256 entries.
//       2 = load only when CBP changed since last load.
//       3 = load when CBP, CPSM, or CSA changed since last load.
//       4 = partial CSA-window load (Ch102) — always fires, but
//           writes only 16 entries at clut_stub[CSA*16 + i] for
//           i ∈ 0..15 (CSA*16 wraps mod 256). The other 240
//           entries are preserved; the VRAM source still starts
//           at CBP*256 and uses the same per-CPSM byte stride.
//     CLD ∈ {5, 6, 7} silently no-op at this scope (reserved /
//     edge cases). The change-detect compares against `prev_*`
//     regs latched on entry to S_LOAD; reset clears them to 0,
//     so a first CLD=2 with CBP==0 is silently skipped (matches
//     the "nothing changed" interpretation).
//   - Reference (kept for posterity): real PS2 CLD encodes:
//       1 = always
//       2 = CBP changed
//       3 = CBP, CPSM, or CSA changed
//       4 = CSA changed (partial 16-entry load at CSA)
//       5..7 = reserved/edge cases
//     Modeling those needs a full-CLUT register snapshot for
//     change detection — deferred.
//   - CSA is consumed two ways. (a) For CLD=3 it's a
//     change-detect input (any prev-vs-new CSA delta triggers
//     a full reload). (b) For CLD=4 it picks the destination
//     window: load_csa_base = {CSA, 4'd0} (8-bit, so CSA=16..31
//     wrap to base 0..240). Full-CLUT loads (CLD ∈ {1,2,3})
//     overwrite all 256 entries regardless of CSA.
//   - One in-flight load at a time. A new TEX0_1 write while
//     `load_busy=1` is silently ignored at this scope.
//
// Timing: full load = 256 clocks; partial (CLD=4) = 16 clocks.
// `load_busy` is high throughout. TBs typically `wait (load_busy == 0)` to
// gate scanout configuration on the load completing.

`timescale 1ns/1ps

module clut_loader_stub #(
    // Ch350 — CSM1 (16×16 CT32 grid) CLUT-load path. Default OFF so all existing CSM2-linear behaviour is
    // BYTE-IDENTICAL (a CSM=0 TEX0 is still silently ignored when this is 0, exactly as Ch99..Ch102). When 1,
    // a CSM=0 / CPSM=PSMCT32 TEX0 commit loads the palette in the real GS CSM1 grid order: palette entry i is
    // read at (x=i[3:0], y=i[7:4]) of a 16×16 PSMCT32 surface based at CBP, via the CT32 block+byte swizzle.
    // This is the order Ch349 proved SH3 uses (host gs_localmem 'grid'); CSM2-linear scatters those colours.
    parameter bit CLUT_CSM1_ENABLE = 1'b0
) (
    input  logic        clk,
    input  logic        rst_n,

    // From gs_stub: 1-cycle pulse on TEX0_1 commit + the
    // newly-decoded sub-fields.
    input  logic        tex0_wr_pulse,
    input  logic [13:0] tex0_cbp,
    input  logic [3:0]  tex0_cpsm,
    input  logic        tex0_csm,    // Ch99 audit: must be 1 (CSM2)
    input  logic [4:0]  tex0_csa,    // Ch101: change-detect for CLD=3
    input  logic [2:0]  tex0_cld,

    // VRAM second read port — combinational byte-addressed read.
    output logic [31:0] vram_read_addr,
    input  logic [31:0] vram_read_data,

    // CLUT staging-area write port.
    output logic        clut_write_en,
    output logic [7:0]  clut_write_idx,
    output logic [31:0] clut_write_data,

    // Status: high while a load is in flight.
    output logic        load_busy
);

    typedef enum logic [0:0] {
        S_IDLE,
        S_LOAD
    } state_e;

    state_e      state;
    logic [7:0]  load_idx;
    logic [13:0] load_cbp;
    logic        load_cpsm_is_ct16;   // Ch100: latched CPSM mode.

    // Ch101 — change-detect snapshots. Updated on every entry to
    // S_LOAD (i.e., every successful start). Used by CLD=2 (CBP
    // change) and CLD=3 (CBP/CPSM/CSA any-change).
    logic [13:0] prev_cbp;
    logic [3:0]  prev_cpsm;
    logic [4:0]  prev_csa;

    // Ch102 — partial CSA-window load mode (CLD=4). When set,
    // walks 16 entries instead of 256 and writes them to
    // clut_stub[load_csa_base + load_idx] (8-bit wrap).
    logic        load_partial;
    logic [7:0]  load_csa_base;
    logic [7:0]  load_terminal;
    assign load_terminal = load_partial ? 8'h0F : 8'hFF;
    logic        load_csm1;             // Ch350: latched CSM1-grid mode (PSMCT32 only).
    logic [31:0] cbp_bytes;
    logic [31:0] addr_offset_ct32;
    logic [31:0] addr_offset_ct16;
    logic [31:0] addr_offset_csm1;
    logic [31:0] addr_offset;

    assign load_busy = (state == S_LOAD);

    // CBP is in 256-byte units (matches PS2 GS docs for the CLUT
    // staging area: each CBP step covers one 256-byte block).
    assign cbp_bytes = {18'd0, load_cbp} << 8;

    // Per-PSM byte offset within the staging block.
    //   PSMCT32 entries are 4 bytes → byte offset = idx * 4.
    //   PSMCT16 entries are 2 bytes → byte offset = idx * 2.
    assign addr_offset_ct32 = {22'd0, load_idx, 2'd0};
    assign addr_offset_ct16 = {23'd0, load_idx, 1'd0};
    // Ch350 — CSM1 16×16 CT32 grid offset for entry load_idx (ix=load_idx[3:0], iy=load_idx[7:4]):
    //   block = {iy[3], ix[3]} (0..3) → block*256 ; byte_in_block = iy[2:0]*32 + ix[2:0]*4.
    // Matches gs_localmem.ct32_addr(cbp,dbw=1,ix,iy) (page_index=0 for a 16×16 region). PSMCT32 only.
    assign addr_offset_csm1 = ({30'd0, load_idx[7], load_idx[3]}        << 8)   // block * 256
                            + ({29'd0, load_idx[6:4]}                   << 5)   // iy[2:0] * 32
                            + ({29'd0, load_idx[2:0]}                   << 2);  // ix[2:0] * 4
    assign addr_offset      = load_csm1       ? addr_offset_csm1
                            : load_cpsm_is_ct16 ? addr_offset_ct16
                                                : addr_offset_ct32;
    assign vram_read_addr   = cbp_bytes + addr_offset;

    // Ch100 — PSMCT16 → PSMCT32 unpack. RGB5A1 packing in the
    // low 16 bits of vram_read_data: R[4:0] G[9:5] B[14:10] A[15].
    // 5→8 bit-replicate matches the same expansion pcrtc uses
    // for direct PSMCT16 framebuffer scanout (Ch94). Alpha is
    // replicated across 8 bits.
    logic [15:0] psm16_entry;
    logic [4:0]  psm16_r5, psm16_g5, psm16_b5;
    logic        psm16_a1;
    logic [7:0]  psm16_r8, psm16_g8, psm16_b8, psm16_a8;
    logic [31:0] write_data_ct16;

    assign psm16_entry      = vram_read_data[15:0];
    assign psm16_r5         = psm16_entry[4:0];
    assign psm16_g5         = psm16_entry[9:5];
    assign psm16_b5         = psm16_entry[14:10];
    assign psm16_a1         = psm16_entry[15];
    assign psm16_r8         = {psm16_r5, psm16_r5[4:2]};
    assign psm16_g8         = {psm16_g5, psm16_g5[4:2]};
    assign psm16_b8         = {psm16_b5, psm16_b5[4:2]};
    assign psm16_a8         = {8{psm16_a1}};
    assign write_data_ct16  = {psm16_a8, psm16_b8, psm16_g8, psm16_r8};

    // Combinational addr/data feed for vram_stub port 1 and
    // clut_stub write port. Idle when not loading. In partial
    // (CLD=4) mode the destination index is the CSA window base
    // + load_idx, with 8-bit wrap; in full mode it's just
    // load_idx (0..255).
    assign clut_write_en    = (state == S_LOAD);
    assign clut_write_idx   = load_partial ? (load_csa_base + load_idx)
                                           : load_idx;
    assign clut_write_data  = load_cpsm_is_ct16 ? write_data_ct16
                                                : vram_read_data;

    // Ch101 — CLD-mode trigger policy. cld_match says "the CLD
    // value alone permits a load (assuming CSM/CPSM also OK)."
    // The full start gate ANDs this with the existing CSM/CPSM
    // checks below.
    logic cld_match;
    always_comb begin
        unique case (tex0_cld)
            3'd0:    cld_match = 1'b0;                          // no load
            3'd1:    cld_match = 1'b1;                          // always (full)
            3'd2:    cld_match = (tex0_cbp  != prev_cbp);       // CBP changed
            3'd3:    cld_match = (tex0_cbp  != prev_cbp)
                              || (tex0_cpsm != prev_cpsm)
                              || (tex0_csa  != prev_csa);
            3'd4:    cld_match = 1'b1;                          // always (partial CSA window)
            default: cld_match = 1'b0;                          // CLD ∈ {5..7} reserved
        endcase
    end

    always_ff @(posedge clk) begin
        if (!rst_n) begin
            state             <= S_IDLE;
            load_idx          <= 8'd0;
            load_cbp          <= 14'd0;
            load_cpsm_is_ct16 <= 1'b0;
            load_csm1         <= 1'b0;
            load_partial      <= 1'b0;
            load_csa_base     <= 8'd0;
            prev_cbp          <= 14'd0;
            prev_cpsm         <= 4'd0;
            prev_csa          <= 5'd0;
        end else begin
            unique case (state)
                S_IDLE: begin
                    // Ch99 audit-medium: CSM=0 means CSM1 (16×16
                    // grid swizzle inside a CSPM block) which we
                    // do NOT model yet. Silently treating CSM=0
                    // as CSM2-linear would produce wrong palette
                    // bytes, so gate the start on CSM=1 as well.
                    // Ch100: CPSM == 0 (PSMCT32) and CPSM == 2
                    // (PSMCT16) are both honored. Other CPSMs
                    // are silently ignored.
                    // Ch101: CLD-mode policy decides whether the
                    // load fires AND records the new TEX0
                    // CBP/CPSM/CSA in prev_* for future change
                    // detection.
                    // Ch102: CLD=4 starts a 16-entry partial load
                    // at the CSA window; load_partial /
                    // load_csa_base latch the mode + destination
                    // base.
                    // Ch350 — start when CSM2 (csm=1, CPSM∈{CT32,CT16}, unchanged) OR, only when
                    // CLUT_CSM1_ENABLE, CSM1-grid (csm=0, CPSM=CT32). The CSM1 branch is the sole new
                    // trigger; with the param OFF this AND-term is constant-0 so a csm=0 TEX0 is ignored
                    // exactly as before (CSM2 path byte-identical).
                    if (tex0_wr_pulse
                        && cld_match
                        && ( ((tex0_csm == 1'b1) && ((tex0_cpsm == 4'd0) || (tex0_cpsm == 4'd2)))
                          || (CLUT_CSM1_ENABLE && (tex0_csm == 1'b0) && (tex0_cpsm == 4'd0)) )) begin
                        state             <= S_LOAD;
                        load_idx          <= 8'd0;
                        load_cbp          <= tex0_cbp;
                        load_cpsm_is_ct16 <= (tex0_cpsm == 4'd2);
                        load_csm1         <= (tex0_csm == 1'b0);   // CSM1-grid addressing (PSMCT32)
                        load_partial      <= (tex0_cld == 3'd4);
                        load_csa_base     <= {tex0_csa, 4'd0};
                        prev_cbp          <= tex0_cbp;
                        prev_cpsm         <= tex0_cpsm;
                        prev_csa          <= tex0_csa;
                    end
                end
                S_LOAD: begin
                    // Terminal index is 0xFF for full load, 0x0F
                    // for partial (CSA window). load_terminal
                    // mux below picks between them.
                    if (load_idx == load_terminal) begin
                        state <= S_IDLE;
                    end
                    load_idx <= load_idx + 8'd1;
                end
            endcase
        end
    end

endmodule : clut_loader_stub
