// retroDE_ps2 — tb_gs_scanout_psmt8_clut (Ch97)
//
// Locks the contract for PSMT8 + CLUT scanout. Pre-Ch97 the
// 8-bit index surfaced as grayscale (Ch96); now with `clut_enable`
// asserted, pcrtc looks up CLUT[index] for real RGB.
//
// Setup (no gs_stub — direct VRAM and direct CLUT writes for
// isolation):
//   - vram_stub holds a 4×4 PSMT8 sprite at indices 0x10..0x1F.
//     FBW=1 → 64 bytes/row stride; one 32-bit write packs 4
//     PSMT8 indices per row.
//   - clut_stub is programmed entry-by-entry: CLUT[i] has a
//     PSMCT32 ABGR value chosen so each channel is uniquely
//     identifiable. Concretely:
//       R = i
//       G = i + 0x40   (mod 256)
//       B = i + 0x80   (mod 256)
//       A = 0xFF
//     so CLUT[0x10] = ABGR (0xFF, 0x90, 0x50, 0x10), and so on.
//   - DISPFB1: FBP=0, FBW=1, PSM=PSMT8 (=0x13).
//   - DISPLAY1 covers full 16×8 active area; MAGH=MAGV=0.
//   - clut_enable = 1; clut_csa = 0 (no window shift).
//
// Per-pixel expectation:
//   if x ∈ [0, 4) AND y ∈ [0, 4):
//     idx = 0x10 + (y * 4 + x)
//     RGB = (idx, idx + 0x40, idx + 0x80)   (8-bit, mod 256)
//   else: RGB = (0, 0, 0)
//
// Verifying:
//   - sample r/g/b at every (x,y) in the active area
//   - assert sprite pixels match the CLUT-decoded RGB
//   - assert non-sprite pixels are 0
//   - structural assert: dispfb1_q[19:15] == 0x13 (PSMT8 latched)

`timescale 1ns/1ps

module tb_gs_scanout_psmt8_clut;

    localparam int PCRTC_H_ACTIVE = 16;
    localparam int PCRTC_V_ACTIVE = 8;

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

    // TB-driven privileged-register feeds (no gs_stub).
    logic [63:0] pmode_q;
    logic [63:0] dispfb1_q;
    logic [63:0] display1_q;

    // VRAM write side.
    logic        vram_we;
    logic [31:0] vram_waddr;
    logic [31:0] vram_wdata;
    logic [3:0]  vram_wbe;
    logic [31:0] vram_raddr;
    logic [31:0] vram_rdata;

    vram_stub #(.BYTES(4096)) u_vram (
        .clk(clk), .rst_n(rst_n),
        .write_en  (vram_we),
        .write_addr(vram_waddr),
        .write_data(vram_wdata),
        .write_be  (vram_wbe),
        .write_mask(32'hFFFF_FFFF),
        .read_addr (vram_raddr),
        .read_data (vram_rdata),
        .read2_addr(32'd0),
        .read2_data()
    );

    // CLUT write side + read interface.
    logic        clut_we;
    logic [7:0]  clut_widx;
    logic [31:0] clut_wdata;
    logic [7:0]  clut_ridx;
    logic [31:0] clut_rdata;

    clut_stub u_clut (
        .clk(clk), .rst_n(rst_n),
        .write_en  (clut_we),
        .write_idx (clut_widx),
        .write_data(clut_wdata),
        .read_idx  (clut_ridx),
        .read_data (clut_rdata)
    );

    logic       clut_enable;
    logic [4:0] clut_csa;
    logic       hsync_o, vsync_o, de_o;
    logic [7:0] r_o, g_o, b_o;
    logic                pcrtc_ev_valid;
    trace_pkg::subsys_e  pcrtc_ev_subsys;
    trace_pkg::event_e   pcrtc_ev_event;
    logic [63:0]         pcrtc_ev_arg0, pcrtc_ev_arg1;
    logic [63:0]         pcrtc_ev_arg2, pcrtc_ev_arg3;
    logic [31:0]         pcrtc_ev_flags;

    gs_pcrtc_stub #(
        .H_ACTIVE(PCRTC_H_ACTIVE), .H_FRONT(1), .H_SYNC(1), .H_BACK(1),
        .V_ACTIVE(PCRTC_V_ACTIVE), .V_FRONT(1), .V_SYNC(1), .V_BACK(1)
    ) u_pcrtc (
        .clk(clk), .rst_n(rst_n),
        .pmode_q       (pmode_q),
        .dispfb1_q     (dispfb1_q),
        .display1_q    (display1_q),
        .vram_read_addr(vram_raddr),
        .vram_read_data(vram_rdata),
        .clut_enable   (clut_enable),
        .clut_csa      (clut_csa),
        .clut_read_idx (clut_ridx),
        .clut_read_data(clut_rdata),
        .hsync(hsync_o), .vsync(vsync_o), .de(de_o),
        .r(r_o), .g(g_o), .b(b_o),
        .ev_valid(pcrtc_ev_valid),
        .ev_subsys(pcrtc_ev_subsys),
        .ev_event(pcrtc_ev_event),
        .ev_arg0(pcrtc_ev_arg0), .ev_arg1(pcrtc_ev_arg1),
        .ev_arg2(pcrtc_ev_arg2), .ev_arg3(pcrtc_ev_arg3),
        .ev_flags(pcrtc_ev_flags)
    );

    logic [7:0] cap_r [0:PCRTC_V_ACTIVE-1][0:PCRTC_H_ACTIVE-1];
    logic [7:0] cap_g [0:PCRTC_V_ACTIVE-1][0:PCRTC_H_ACTIVE-1];
    logic [7:0] cap_b [0:PCRTC_V_ACTIVE-1][0:PCRTC_H_ACTIVE-1];
    logic       cap_de[0:PCRTC_V_ACTIVE-1][0:PCRTC_H_ACTIVE-1];

    int errors;
    bit capture_armed;

    initial begin
        for (int y = 0; y < PCRTC_V_ACTIVE; y++)
            for (int x = 0; x < PCRTC_H_ACTIVE; x++) begin
                cap_r[y][x]  = 8'd0;
                cap_g[y][x]  = 8'd0;
                cap_b[y][x]  = 8'd0;
                cap_de[y][x] = 1'b0;
            end
        errors        = 0;
        capture_armed = 1'b0;
    end

    always_ff @(posedge clk) begin
        if (rst_n && capture_armed && de_o
            && (u_pcrtc.vcnt < PCRTC_V_ACTIVE)
            && (u_pcrtc.hcnt < PCRTC_H_ACTIVE)) begin
            cap_r [u_pcrtc.vcnt][u_pcrtc.hcnt] <= r_o;
            cap_g [u_pcrtc.vcnt][u_pcrtc.hcnt] <= g_o;
            cap_b [u_pcrtc.vcnt][u_pcrtc.hcnt] <= b_o;
            cap_de[u_pcrtc.vcnt][u_pcrtc.hcnt] <= 1'b1;
        end
    end

    task automatic vram_write32(input logic [31:0] addr,
                                input logic [31:0] data,
                                input logic [3:0]  be);
        @(negedge clk);
        vram_we    = 1'b1;
        vram_waddr = addr;
        vram_wdata = data;
        vram_wbe   = be;
        @(posedge clk);
        @(negedge clk);
        vram_we    = 1'b0;
        vram_waddr = 32'd0;
        vram_wdata = 32'd0;
        vram_wbe   = 4'b0000;
    endtask

    task automatic clut_write(input logic [7:0]  idx,
                              input logic [31:0] entry);
        @(negedge clk);
        clut_we    = 1'b1;
        clut_widx  = idx;
        clut_wdata = entry;
        @(posedge clk);
        @(negedge clk);
        clut_we    = 1'b0;
        clut_widx  = 8'd0;
        clut_wdata = 32'd0;
    endtask

    function automatic logic [31:0] clut_entry(input logic [7:0] i);
        // ABGR per Ch97 contract: A=0xFF, B=i+0x80, G=i+0x40, R=i.
        logic [7:0] r8, g8, b8, a8;
        r8 = i;
        g8 = i + 8'h40;
        b8 = i + 8'h80;
        a8 = 8'hFF;
        return {a8, b8, g8, r8};
    endfunction

    localparam int SPRITE_W   = 4;
    localparam int SPRITE_H   = 4;
    localparam logic [7:0] BASE_IDX = 8'h10;
    localparam int ROW_STRIDE = 64;

    // DISPFB1: FBP=0, FBW=1 (bit 9), PSM=PSMT8=0x13 (bits 19:15) → 0x9_8200.
    localparam logic [63:0] DISPFB1_VAL  = 64'h0000_0000_0009_8200;
    // DISPLAY1: full active area, MAGH=MAGV=0.
    localparam logic [63:0] DISPLAY1_VAL =
          (64'(PCRTC_H_ACTIVE - 1) << 32)
        | (64'(PCRTC_V_ACTIVE - 1) << 44);
    localparam logic [63:0] PMODE_EN1    = 64'h0000_0000_0000_0001;

    initial begin
        rst_n       = 1'b0;
        pmode_q     = 64'd0;
        dispfb1_q   = 64'd0;
        display1_q  = 64'd0;
        clut_enable = 1'b0;
        clut_csa    = 5'd0;
        vram_we     = 1'b0;
        vram_waddr  = 32'd0;
        vram_wdata  = 32'd0;
        vram_wbe    = 4'b0000;
        clut_we     = 1'b0;
        clut_widx   = 8'd0;
        clut_wdata  = 32'd0;

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

        // Program the CLUT — only the 16 entries the sprite uses
        // need real values; the rest stay zero (initial-block).
        for (int i = 0; i < SPRITE_W * SPRITE_H; i++) begin
            logic [7:0] idx;
            idx = BASE_IDX + 8'(i);
            clut_write(idx, clut_entry(idx));
        end

        // Populate VRAM with the 4×4 PSMT8 sprite.
        for (int y = 0; y < SPRITE_H; y++) begin
            logic [7:0] i0, i1, i2, i3;
            logic [31:0] data;
            int row_base;
            i0 = BASE_IDX + 8'(y * SPRITE_W + 0);
            i1 = BASE_IDX + 8'(y * SPRITE_W + 1);
            i2 = BASE_IDX + 8'(y * SPRITE_W + 2);
            i3 = BASE_IDX + 8'(y * SPRITE_W + 3);
            data     = {i3, i2, i1, i0};
            row_base = y * ROW_STRIDE;
            vram_write32(row_base, data, 4'b1111);
        end

        // Configure scanout: DISPFB1+DISPLAY1 first, then enable
        // CLUT, then PMODE.EN1 last.
        dispfb1_q  = DISPFB1_VAL;
        display1_q = DISPLAY1_VAL;
        @(posedge clk);

        if (dispfb1_q[19:15] !== 5'h13) begin
            $error("DISPFB1.PSM=0x%02x (expected 0x13)", dispfb1_q[19:15]);
            errors = errors + 1;
        end

        clut_enable = 1'b1;
        clut_csa    = 5'd0;
        @(posedge clk);

        pmode_q = PMODE_EN1;
        @(posedge clk);

        @(posedge u_pcrtc.end_of_frame);
        @(posedge clk);
        capture_armed = 1'b1;
        @(posedge u_pcrtc.end_of_frame);
        @(posedge clk);
        capture_armed = 1'b0;

        // Per-pixel verification.
        for (int y = 0; y < PCRTC_V_ACTIVE; y++) begin
            for (int x = 0; x < PCRTC_H_ACTIVE; x++) begin
                logic [7:0]  er, eg, eb;
                bit          in_sprite;
                logic [7:0]  idx;
                logic [31:0] entry;

                in_sprite = (x < SPRITE_W) && (y < SPRITE_H);
                if (in_sprite) begin
                    idx   = BASE_IDX + 8'(y * SPRITE_W + x);
                    entry = clut_entry(idx);
                    er    = entry[7:0];
                    eg    = entry[15:8];
                    eb    = entry[23:16];
                end else begin
                    er = 8'd0; eg = 8'd0; eb = 8'd0;
                end

                if (!cap_de[y][x]) begin
                    $error("(%0d,%0d) DE never asserted", x, y);
                    errors = errors + 1;
                end
                if (cap_r[y][x] !== er || cap_g[y][x] !== eg || cap_b[y][x] !== eb) begin
                    $error("(%0d,%0d) got (%02x,%02x,%02x) expected (%02x,%02x,%02x) in_sprite=%0d",
                           x, y, cap_r[y][x], cap_g[y][x], cap_b[y][x],
                           er, eg, eb, in_sprite);
                    errors = errors + 1;
                end
            end
        end

        $display("[tb_gs_scanout_psmt8_clut] phase 1 CSA=0 sprite=%0dx%0d done",
                 SPRITE_W, SPRITE_H);

        // ----------------------------------------------------------------
        // Phase 2 — CSA=1 base case. With idx=0x00 in VRAM and
        // CSA=1, the effective lookup is 0x00 + (1 << 4) = 0x10.
        // We programmed CLUT[0x10] in phase 1, so displayed (0,0)
        // must read that entry's RGB. The other sprite pixels
        // still hold indices 0x11..0x1F → effective 0x21..0x2F,
        // which are unprogrammed (0,0,0).
        // ----------------------------------------------------------------
        vram_write32(32'd0, 32'h13_12_11_00, 4'b0001);  // mem[0] = 0x00
        clut_csa = 5'd1;
        @(posedge u_pcrtc.end_of_frame);
        @(posedge clk);
        capture_armed = 1'b1;
        @(posedge u_pcrtc.end_of_frame);
        @(posedge clk);
        capture_armed = 1'b0;

        begin
            logic [31:0] entry_p2;
            entry_p2 = clut_entry(8'h10);
            if (cap_r[0][0] !== entry_p2[7:0]
             || cap_g[0][0] !== entry_p2[15:8]
             || cap_b[0][0] !== entry_p2[23:16]) begin
                $error("[phase2 CSA=1 base] (0,0) got (%02x,%02x,%02x) expected (%02x,%02x,%02x) = CLUT[0x10]",
                       cap_r[0][0], cap_g[0][0], cap_b[0][0],
                       entry_p2[7:0], entry_p2[15:8], entry_p2[23:16]);
                errors = errors + 1;
            end
        end

        // ----------------------------------------------------------------
        // Phase 3 — CSA=1 wrap. With idx=0xF8 and CSA=1, the
        // 8-bit add wraps: 0xF8 + 0x10 = 0x108 → 0x08 (mod 256).
        // Program CLUT[0x08] with a distinctive entry, write
        // idx=0xF8 at VRAM byte 0, and verify displayed (0,0)
        // reads CLUT[0x08].
        // ----------------------------------------------------------------
        clut_write(8'h08, clut_entry(8'h08));
        vram_write32(32'd0, 32'h13_12_11_F8, 4'b0001);  // mem[0] = 0xF8
        // CSA still = 1 from phase 2.
        @(posedge u_pcrtc.end_of_frame);
        @(posedge clk);
        capture_armed = 1'b1;
        @(posedge u_pcrtc.end_of_frame);
        @(posedge clk);
        capture_armed = 1'b0;

        begin
            logic [31:0] entry_p3;
            entry_p3 = clut_entry(8'h08);
            if (cap_r[0][0] !== entry_p3[7:0]
             || cap_g[0][0] !== entry_p3[15:8]
             || cap_b[0][0] !== entry_p3[23:16]) begin
                $error("[phase3 CSA=1 wrap] (0,0) got (%02x,%02x,%02x) expected (%02x,%02x,%02x) = CLUT[0x08] (after 0xF8+0x10 wrap)",
                       cap_r[0][0], cap_g[0][0], cap_b[0][0],
                       entry_p3[7:0], entry_p3[15:8], entry_p3[23:16]);
                errors = errors + 1;
            end
        end

        $display("[tb_gs_scanout_psmt8_clut] phase 2 CSA=1 (idx 0x00 → CLUT[0x10]) and phase 3 CSA=1 wrap (idx 0xF8 → CLUT[0x08]) done");

        if (errors == 0) $display("[tb_gs_scanout_psmt8_clut] PASS");
        else             $display("[tb_gs_scanout_psmt8_clut] FAIL");
        $finish;
    end

    initial begin
        #5000000;
        $error("[tb_gs_scanout_psmt8_clut] timeout");
        $finish;
    end

endmodule : tb_gs_scanout_psmt8_clut
