// ============================================================================
// tb_gs_lpddr_scanout — Ch320 Brick 1
//
// Preloads an AXI read-slave with a known 64x64 PSMCT16 frame (pixel n carries
// the value n), pulses frame_start, waits for the cache to fill, then presents
// PCRTC-style vram_read_addr values and checks the decoded r/g/b match the
// known frame (5->8 bit-replication) and that out-of-range reads return black.
// ============================================================================
`timescale 1ns/1ps

module tb_gs_lpddr_scanout;
    logic axi_clk = 0, video_clk = 0;
    always #2  axi_clk   = ~axi_clk;     // ~250 MHz
    always #10 video_clk = ~video_clk;   // 50 MHz

    logic        axi_rst_n;
    logic        enable, frame_start;
    logic [31:0] vram_read_addr;
    logic [7:0]  r, g, b;
    logic        cache_valid;
    logic [31:0] rd_beats, rd_errs;

    logic [29:0] araddr; logic [1:0] arburst; logic [6:0] arid;
    logic [7:0]  arlen;  logic [2:0] arsize;  logic arvalid, arready;
    logic [255:0] rdata; logic [1:0] rresp;   logic rlast, rvalid, rready;

    gs_lpddr_scanout #(.FB_BASE(30'd0), .CACHE_BEATS(256)) dut (
        .axi_clk(axi_clk), .axi_rst_n(axi_rst_n),
        .enable(enable), .frame_start(frame_start),
        .video_clk(video_clk), .vram_read_addr(vram_read_addr),
        .r(r), .g(g), .b(b),
        .cache_valid(cache_valid), .rd_beats(rd_beats), .rd_errs(rd_errs),
        .araddr(araddr), .arburst(arburst), .arid(arid), .arlen(arlen),
        .arsize(arsize), .arvalid(arvalid), .arready(arready),
        .rdata(rdata), .rresp(rresp), .rlast(rlast), .rvalid(rvalid), .rready(rready)
    );

    // ---- known frame: pixel n (16-bit) carries value n ----
    function automatic [255:0] beat_data(input [7:0] beat);
        integer i;
        for (i = 0; i < 16; i = i + 1)
            beat_data[16*i +: 16] = (beat * 16 + i);   // pixel index = value
    endfunction
    function automatic [15:0] pixel_val(input [31:0] byte_addr);
        pixel_val = byte_addr[16:1];   // pixel index = addr>>1
    endfunction

    // ---- AXI read-slave model ----
    localparam [1:0] S_IDLE=0, S_LAT=1, S_RESP=2;
    logic [1:0]  sst; logic [2:0] sdly; logic [7:0] sbeat;
    always_ff @(posedge axi_clk or negedge axi_rst_n) begin
        if (!axi_rst_n) begin
            sst<=S_IDLE; arready<=0; rvalid<=0; rlast<=0; rdata<='0; rresp<=2'b00; sdly<=0; sbeat<=0;
        end else begin
            arready <= 1'b0;
            case (sst)
                S_IDLE: if (arvalid) begin
                            arready <= 1'b1; sbeat <= araddr[12:5]; sdly <= 3'd3; sst <= S_LAT;
                        end
                S_LAT:  if (sdly==0) begin
                            rdata<=beat_data(sbeat); rresp<=2'b00; rlast<=1'b1; rvalid<=1'b1; sst<=S_RESP;
                        end else sdly <= sdly-1;
                S_RESP: if (rready) begin rvalid<=0; rlast<=0; sst<=S_IDLE; end
                default: sst <= S_IDLE;
            endcase
        end
    end

    // ---- checks ----
    integer errors = 0;
    task automatic check_pixel(input [31:0] addr);
        logic [15:0] px; logic [4:0] r5,g5,b5; logic [7:0] er,eg,eb;
        begin
            vram_read_addr = addr;
            @(posedge video_clk); @(posedge video_clk);   // registered (sync) read latency
            px = pixel_val(addr);
            r5 = px[4:0]; g5 = px[9:5]; b5 = px[14:10];
            er = {r5, r5[4:2]}; eg = {g5, g5[4:2]}; eb = {b5, b5[4:2]};
            if (r!==er || g!==eg || b!==eb) begin
                errors = errors + 1;
                $display("[tb] PIXEL MISMATCH @0x%0h: got(%0h,%0h,%0h) exp(%0h,%0h,%0h)",
                         addr, r, g, b, er, eg, eb);
            end
        end
    endtask

    task automatic check_black(input [31:0] addr);
        begin
            vram_read_addr = addr;
            @(posedge video_clk); @(posedge video_clk);
            if (r!==8'd0 || g!==8'd0 || b!==8'd0) begin
                errors = errors + 1;
                $display("[tb] EXPECTED BLACK @0x%0h: got(%0h,%0h,%0h)", addr, r, g, b);
            end
        end
    endtask

    // Ch352 (Codex) — register-contract witness: while disabled (the de25 top gates this with video_src=0),
    // the scanout reader must issue ZERO AXI AR traffic. Proves video_src=0 -> no LPDDR scanout reads.
    logic saw_ar_dis; initial saw_ar_dis=0;
    always_ff @(posedge axi_clk) if (!enable && arvalid) saw_ar_dis <= 1'b1;

    initial begin
        enable=0; frame_start=0; vram_read_addr=0;
        axi_rst_n=0; repeat(8) @(posedge axi_clk); axi_rst_n=1;
        repeat(4) @(posedge video_clk);

        // out-of-range before any fill -> black (cache_valid=0)
        check_black(32'h0000_0000);
        // pulse frame_start while DISABLED — must NOT trigger any AR (contract: video_src=0 => no reads)
        @(posedge video_clk); frame_start=1'b1; @(posedge video_clk); frame_start=1'b0;
        repeat(40) @(posedge axi_clk);
        if (saw_ar_dis) begin errors=errors+1; $display("[tb] AR issued while disabled (video_src=0 contract violated)"); end

        // trigger a frame fill
        enable = 1'b1;
        @(posedge video_clk); frame_start = 1'b1; @(posedge video_clk); frame_start = 1'b0;

        // wait for the cache to load
        begin integer to; to=0;
            while (!cache_valid && to<100000) begin @(posedge axi_clk); to=to+1; end
            if (!cache_valid) begin errors=errors+1; $display("[tb] cache_valid TIMEOUT"); end
        end

        if (rd_beats !== 32'd256) begin errors=errors+1; $display("[tb] rd_beats=%0d exp 256", rd_beats); end
        if (rd_errs  !== 32'd0)   begin errors=errors+1; $display("[tb] rd_errs=%0d exp 0", rd_errs); end

        // in-range pixels across beats + halfword lanes
        check_pixel(32'h0000_0000);   // pixel 0, beat 0 lane 0
        check_pixel(32'h0000_0002);   // pixel 1
        check_pixel(32'h0000_001E);   // pixel 15, beat 0 lane 15
        check_pixel(32'h0000_0020);   // pixel 16, beat 1 lane 0
        check_pixel(32'h0000_0080);   // pixel 64 (row 1, col 0)
        check_pixel(32'h0000_1FFE);   // pixel 4095, last in-range
        // out-of-range -> black
        check_black(32'h0000_2000);   // byte 8192 = first past the frame

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

    initial begin
        #500000; $display("[tb_gs_lpddr_scanout] TIMEOUT"); $display("[tb_gs_lpddr_scanout] FAIL"); $finish;
    end
endmodule
