// retroDE_ps2 — tb_gs_raster_bram_psmct16 (Ch156)
//
// Focused CT16 raster integration through the new BRAM-friendly
// VRAM. Drives gs_stub#(PSMCT16_SWIZZLE=1) directly via gif_reg_wr_*
// (no DMAC / GIF parser / EE bootlet) and routes the registered
// raster emit signals through `vram_normalize_pkg::normalize_write`
// into `vram_bram_stub` — the same wrapper-site plumbing landed in
// `top_psmct32_raster_demo_bram.sv` (Ch156).
//
// Why this TB exists: Ch155 unit-test `tb_vram_normalize_write`
// proved the function in isolation; the Ch155 BRAM top wrapper
// proved CT32 passthrough end-to-end; this TB closes the gap by
// proving the wrapper-site plumbing works for the FIRST sub-word
// PSM (CT16 — halfword writes shifted to byte_addr[1]-keyed
// lane). PSMT8 has its own sibling (`tb_gs_raster_bram_psmt8`).
// PSMT4 needs the old-byte read pipeline (Ch157+).
//
// Phase 1 only — the write-side lock. The drive splits a 16×4
// PSMCT16 image into TWO 8×4 SPRITEs:
//   * Sprite A at (0,0)..(7,3) with halfword A=0x6155 — under
//     the PSMCT16 column table (yb=0..3, xb=0..7) these halfwords
//     all land in the LOW lane (byte_addr[1]=0) of their target
//     32-bit words.
//   * Sprite B at (8,0)..(15,3) with halfword B=0x9F8E — these
//     land in the HIGH lane (byte_addr[1]=1) of the SAME 32-bit
//     words that Sprite A wrote to (PSMCT16's column-0..7 vs
//     column-8..15 share words at the same yb).
//
// Verification then asserts every pixel's halfword landed in its
// PSMCT16-swizzled byte address AT THE RIGHT LANE — Sprite A in
// the low halfword, Sprite B in the high halfword. With two
// distinct halfwords sharing each 32-bit word, lane-stomp /
// wrong-lane / over-wide-be normalizer bugs all surface (Codex
// Ch156 audit Medium #2 — uniform payloads were the prior
// weakness).
//
// Adversarial sentinel preload: `u_vram.mem[]` is written to a
// non-zero sentinel BEFORE the SPRITE drives so any normalize
// write that lands at the wrong byte address (or that stomps an
// unrelated word) shows up against the sentinel rather than
// against zero. The linear-stride separator at byte 0x80 (PSMCT16
// y=1 row start at FBW=1 stride 128 — outside the swizzled set)
// is checked to remain at the sentinel value as a strict
// non-overwrite assertion.

`timescale 1ns/1ps

module tb_gs_raster_bram_psmct16;

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

    // ------------------------------------------------------------
    // gs_stub I/O — minimal subset (raster + register drive).
    // ------------------------------------------------------------
    logic        gif_reg_wr_en;
    logic [7:0]  gif_reg_num;
    logic [63:0] gif_reg_data;

    logic        raster_pixel_emit;
    logic [31:0] raster_pixel_emit_count;
    logic [63:0] raster_pixel_color_q;
    logic [31:0] raster_pixel_fb_addr_q;
    logic [3:0]  raster_pixel_be_q;
    logic [31:0] raster_pixel_mask_q;
    logic [5:0]  raster_pixel_psm_q;
    logic        raster_active;

    gs_stub #(
        .PSMCT32_SWIZZLE(1'b0),
        .PSMCT16_SWIZZLE(1'b1)
    ) u_gs (
        .clk(clk), .rst_n(rst_n),
        .reg_wr_en  (1'b0),
        .reg_wr_addr(16'd0),
        .reg_wr_data(64'd0),
        .gif_reg_wr_en(gif_reg_wr_en),
        .gif_reg_num  (gif_reg_num),
        .gif_reg_data (gif_reg_data),
        .bg_r(), .bg_g(), .bg_b(),
        .pmode_q(), .dispfb1_q(), .display1_q(),
        .prim_q(), .rgbaq_q(),
        .xyz2_q(), .xyzf2_q(),
        .frame_1_q(), .zbuf_1_q(),
        .tex0_1_q(), .tex0_1_cbp_q(), .tex0_1_cpsm_q(),
        .tex0_1_csm_q(), .tex0_1_csa_q(), .tex0_1_cld_q(),
        .tex0_1_wr_q(),
        .bitbltbuf_q(), .trxpos_q(), .trxreg_q(), .trxdir_q(),
        .trxdir_wr_q(),
        .prim_complete(), .prim_complete_count(),
        .prim_v0_q(), .prim_v1_q(), .prim_v2_q(),
        .prim_color_q(),
        .prim_color_v0_q(), .prim_color_v1_q(), .prim_color_v2_q(),
        .prim_v0_decoded_q(), .prim_v1_decoded_q(), .prim_v2_decoded_q(),
        .prim_v0_color_decoded_q(), .prim_v1_color_decoded_q(), .prim_v2_color_decoded_q(),
        .pixel_emit(), .pixel_emit_count(),
        .pixel_x_q(), .pixel_y_q(), .pixel_color_q(),
        .pixel_fbp_q(), .pixel_fbw_q(), .pixel_psm_q(), .pixel_fb_addr_q(),
        .raster_pixel_emit(raster_pixel_emit),
        .raster_pixel_emit_count(raster_pixel_emit_count),
        .raster_pixel_x_q(), .raster_pixel_y_q(),
        .raster_pixel_color_q(raster_pixel_color_q),
        .raster_pixel_fb_addr_q(raster_pixel_fb_addr_q),
        .raster_pixel_be_q(raster_pixel_be_q),
        .raster_pixel_mask_q(raster_pixel_mask_q),
        .raster_pixel_psm_q(raster_pixel_psm_q),
        .raster_active(raster_active),
        .raster_overflow(), .raster_degenerate(),
        .ev_valid(), .ev_subsys(), .ev_event(),
        .ev_arg0(), .ev_arg1(), .ev_arg2(), .ev_arg3(), .ev_flags()
    );

    // ------------------------------------------------------------
    // Wrapper-site Ch156 normalize_write plumbing — same shape as
    // top_psmct32_raster_demo_bram. Raster-only path here; xfer
    // engine isn't instantiated in this focused TB.
    // ------------------------------------------------------------
    vram_normalize_pkg::norm_out_t vram_norm;
    assign vram_norm = vram_normalize_pkg::normalize_write(
        raster_pixel_fb_addr_q,    // byte_addr
        raster_pixel_psm_q,        // psm
        raster_pixel_color_q[31:0],// payload
        1'b0,                      // nibble_hi (T4 only)
        8'd0                       // old_byte  (T4 only)
    );

    // ------------------------------------------------------------
    // vram_bram_stub. BYTES sized to fit the 16×4 PSMCT16 swizzled
    // image at page (0,0); 4 KiB is more than enough.
    //
    // CT16 emits never enter the Ch157 PSMT4 RMW pipe (`is_t4_emit`
    // is always 0 here), so this focused TB connects
    // `raster_pixel_emit` directly to `write_en` and lets
    // `vram_norm` drive the addr/data/be triple. The full RMW pipe
    // shape is exercised by `tb_top_psmct32_raster_demo_bram`
    // (CT32 path) and by `tb_gs_raster_bram_psmt4` (T4 path).
    // ------------------------------------------------------------
    localparam int VRAM_BYTES = 4096;
    localparam int VRAM_WORDS = VRAM_BYTES / 4;
    localparam logic [31:0] SENTINEL = 32'hDEAD_BEEF;

    vram_bram_stub #(.BYTES(VRAM_BYTES)) u_vram (
        .clk(clk), .rst_n(rst_n),
        .write_en  (raster_pixel_emit),
        .write_addr(vram_norm.write_addr),
        .write_data(vram_norm.write_data),
        .write_be  (vram_norm.write_be),
        .read_addr (32'd0),
        .read_data (),
        .read_valid(),
        .read2_addr(32'd0),
        .read2_data(),
        .read2_valid()
    );

    // ------------------------------------------------------------
    // GIF register drive helper.
    // ------------------------------------------------------------
    task automatic drive_reg(input logic [7:0] num, input logic [63:0] data);
        @(negedge clk);
        gif_reg_wr_en = 1'b1;
        gif_reg_num   = num;
        gif_reg_data  = data;
        @(posedge clk);
        @(negedge clk);
        gif_reg_wr_en = 1'b0;
        gif_reg_num   = 8'd0;
        gif_reg_data  = 64'd0;
    endtask

    function automatic logic [63:0] xyz2_data(input logic [11:0] x_int,
                                              input logic [11:0] y_int);
        return {32'd0, y_int, 4'd0, x_int, 4'd0};
    endfunction

    // ------------------------------------------------------------
    // PSMCT16 swizzle reference (verbatim from Ch128/Ch125).
    // ------------------------------------------------------------
    function automatic int ref_block_idx16(input int by, input int bx);
        case ({by[2:0], bx[1:0]})
            5'd0:  return 0;   5'd1:  return 2;   5'd2:  return 8;
            5'd3:  return 10;  5'd4:  return 1;   5'd5:  return 3;
            5'd6:  return 9;   5'd7:  return 11;  5'd8:  return 4;
            5'd9:  return 6;   5'd10: return 12;  5'd11: return 14;
            5'd12: return 5;   5'd13: return 7;   5'd14: return 13;
            5'd15: return 15;  5'd16: return 16;  5'd17: return 18;
            5'd18: return 24;  5'd19: return 26;  5'd20: return 17;
            5'd21: return 19;  5'd22: return 25;  5'd23: return 27;
            5'd24: return 20;  5'd25: return 22;  5'd26: return 28;
            5'd27: return 30;  5'd28: return 21;  5'd29: return 23;
            5'd30: return 29;  default: return 31;
        endcase
    endfunction

    function automatic int ref_col_idx16(input int yb, input int xb);
        case ({yb[2:0], xb[3:0]})
            7'd0:   return 0;   7'd1:   return 2;   7'd2:   return 8;   7'd3:   return 10;
            7'd4:   return 16;  7'd5:   return 18;  7'd6:   return 24;  7'd7:   return 26;
            7'd8:   return 1;   7'd9:   return 3;   7'd10:  return 9;   7'd11:  return 11;
            7'd12:  return 17;  7'd13:  return 19;  7'd14:  return 25;  7'd15:  return 27;
            7'd16:  return 4;   7'd17:  return 6;   7'd18:  return 12;  7'd19:  return 14;
            7'd20:  return 20;  7'd21:  return 22;  7'd22:  return 28;  7'd23:  return 30;
            7'd24:  return 5;   7'd25:  return 7;   7'd26:  return 13;  7'd27:  return 15;
            7'd28:  return 21;  7'd29:  return 23;  7'd30:  return 29;  7'd31:  return 31;
            7'd32:  return 32;  7'd33:  return 34;  7'd34:  return 40;  7'd35:  return 42;
            7'd36:  return 48;  7'd37:  return 50;  7'd38:  return 56;  7'd39:  return 58;
            7'd40:  return 33;  7'd41:  return 35;  7'd42:  return 41;  7'd43:  return 43;
            7'd44:  return 49;  7'd45:  return 51;  7'd46:  return 57;  7'd47:  return 59;
            7'd48:  return 36;  7'd49:  return 38;  7'd50:  return 44;  7'd51:  return 46;
            7'd52:  return 52;  7'd53:  return 54;  7'd54:  return 60;  7'd55:  return 62;
            7'd56:  return 37;  7'd57:  return 39;  7'd58:  return 45;  7'd59:  return 47;
            7'd60:  return 53;  7'd61:  return 55;  7'd62:  return 61;  7'd63:  return 63;
            default: return 0;
        endcase
    endfunction

    function automatic logic [31:0] ref_addr16(
        input int fbp_v, input int fbw_v, input int x_v, input int y_v);
        int page_x, page_y, page_idx, page_base;
        int by, bx, blk_idx, xb, yb, hw_idx;
        int addr_v;
        page_x   = x_v / 64;
        page_y   = y_v / 64;
        page_idx = page_y * fbw_v + page_x;
        page_base = fbp_v * 2048 + page_idx * 8192;
        by  = (y_v % 64) / 8;
        bx  = (x_v % 64) / 16;
        blk_idx = ref_block_idx16(by, bx);
        xb = x_v % 16;
        yb = y_v % 8;
        hw_idx = ref_col_idx16(yb, xb);
        addr_v = page_base + blk_idx * 256 + hw_idx * 2;
        return addr_v[31:0];
    endfunction

    // ------------------------------------------------------------
    // Test stimulus.
    //   Two distinct PSMCT16 SPRITEs in PSMCT16 at FBP=0/FBW=1:
    //     A:  (0,0)..(7,3),  RGBAQ_A → halfword 0x6155.
    //     B:  (8,0)..(15,3), RGBAQ_B → halfword 0x9F8E.
    //   Adjacent columns (xb=0..7 vs xb=8..15) share 32-bit
    //   words via the PSMCT16 column-table layout, so this
    //   double-color drive pins both halfword lanes per word.
    // ------------------------------------------------------------
    localparam logic [7:0]  R_PRIM     = 8'h00;
    localparam logic [7:0]  R_RGBAQ    = 8'h01;
    localparam logic [7:0]  R_XYZ2     = 8'h05;
    localparam logic [7:0]  R_FRAME_1  = 8'h4C;

    localparam logic [63:0] PRIM_SPRITE = 64'd6;
    localparam logic [63:0] FRAME_1_VAL = 64'h0000_0000_0201_0000;

    // RGBAQ_A: R=0xAA, G=0x50, B=0xC0, A=0x00 → halfword 0x6155.
    localparam logic [63:0] RGBAQ_A     = 64'h0000_0000_00C0_50AA;
    localparam logic [15:0] HW_A        = 16'h6155;

    // RGBAQ_B: R=0x70, G=0xE0, B=0x38, A=0xFF → halfword 0x9F8E.
    localparam logic [63:0] RGBAQ_B     = 64'h0000_0000_FF38_E070;
    localparam logic [15:0] HW_B        = 16'h9F8E;

    int errors;

    initial begin
        rst_n          = 1'b0;
        gif_reg_wr_en  = 1'b0;
        gif_reg_num    = 8'd0;
        gif_reg_data   = 64'd0;
        errors         = 0;

        // Sentinel preload — fires at time 0 alongside
        // vram_bram_stub's own translate_off zero-init, but the
        // explicit assignment here is sequential so its order is
        // deterministic on iverilog. We re-stamp the sentinel after
        // 1 ns to make the order explicit.
        for (int i = 0; i < VRAM_WORDS; i++) u_vram.mem[i] = SENTINEL;
        #1;
        for (int i = 0; i < VRAM_WORDS; i++) u_vram.mem[i] = SENTINEL;

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

        // ---------- Sprite A — left half, halfword 0x6155 -----------
        drive_reg(R_PRIM,    PRIM_SPRITE);
        drive_reg(R_FRAME_1, FRAME_1_VAL);
        drive_reg(R_RGBAQ,   RGBAQ_A);
        drive_reg(R_XYZ2,    xyz2_data(12'd0, 12'd0));
        drive_reg(R_XYZ2,    xyz2_data(12'd7, 12'd3));

        wait (raster_active == 1'b1);
        wait (raster_active == 1'b0);
        repeat (10) @(posedge clk);

        // ---------- Sprite B — right half, halfword 0x9F8E ----------
        drive_reg(R_PRIM,    PRIM_SPRITE);
        drive_reg(R_RGBAQ,   RGBAQ_B);
        drive_reg(R_XYZ2,    xyz2_data(12'd8,  12'd0));
        drive_reg(R_XYZ2,    xyz2_data(12'd15, 12'd3));

        wait (raster_active == 1'b1);
        wait (raster_active == 1'b0);
        repeat (10) @(posedge clk);

        if (raster_pixel_emit_count != 32'(8 * 4 + 8 * 4)) begin
            $error("expected %0d raster emits, got %0d",
                   8 * 4 + 8 * 4, raster_pixel_emit_count);
            errors = errors + 1;
        end

        // Per-pixel verification. Each pixel's halfword is at the
        // PSMCT16-swizzled byte address; lane within the word is
        // selected by addr[1].
        for (int yy = 0; yy < 4; yy++) begin
            for (int xx = 0; xx < 16; xx++) begin
                logic [31:0] addr;
                logic [31:0] word;
                logic [15:0] got, expected;
                addr = ref_addr16(0, 1, xx, yy);
                word = u_vram.mem[addr >> 2];
                got  = addr[1] ? word[31:16] : word[15:0];
                expected = (xx < 8) ? HW_A : HW_B;
                if (got !== expected) begin
                    $error("[bram-ct16] (%0d,%0d) @addr 0x%08x got 0x%04x expected 0x%04x (word=0x%08x)",
                           xx, yy, addr, got, expected, word);
                    errors = errors + 1;
                end
            end
        end

        // Linear-stride separator: linear byte 128 (y=1 row start
        // at FBW=1 PSMCT16 stride 128) is OUTSIDE the swizzled
        // image set. The whole 32-bit word at [0x80] must stay at
        // the sentinel. mem index = 0x80/4 = 32.
        if (u_vram.mem[32'd32] !== SENTINEL) begin
            $error("[linear-sep] word @byte 0x80 got 0x%08x expected sentinel 0x%08x",
                   u_vram.mem[32'd32], SENTINEL);
            errors = errors + 1;
        end
        // A second linear separator at byte 256 (y=2 row start).
        if (u_vram.mem[32'd64] !== SENTINEL) begin
            $error("[linear-sep] word @byte 0x100 got 0x%08x expected sentinel 0x%08x",
                   u_vram.mem[32'd64], SENTINEL);
            errors = errors + 1;
        end

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

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

endmodule : tb_gs_raster_bram_psmct16
