// ============================================================================
// tb_gs_lpddr_scanout_lb_psm32_256.sv  (Ch327a — line-buffer scanout for the PSMCT32 spill FB)
//
// Ch326 put the framebuffer in LPDDR with NO BRAM mirror, displayed via the whole-frame
// cache (gs_lpddr_scanout, 64 KiB BRAM @ 128x128). That cache does NOT scale: 256x256
// would be 256 KiB BRAM and undo the reclaim. The line-buffer (gs_lpddr_scanout_lb) holds
// only TWO scanlines, so it scales — but it was a Ch321 PSMCT16/FB-at-0 path. This TB
// proves the GENERALIZED PSMCT32 line-buffer for the Ch326 spill FB, and — the failure mode
// the frame-cache never had — that it meets the REAL-TIME per-scanline refill deadline
// (underflow=0) while sharing the EMIF read channel with the tile reload + HPS probe.
//
// Raster: H_TOTAL=160/H_ACTIVE=128, V_TOTAL=140/V_ACTIVE=128, 1 px/video_clk. The line-buffer
// fetches row L+1 (16 single-beat reads) during the ~160-cycle display of row L, on a 4x-faster
// axi_clk, while reload(s3)+probe(s1) hammer the shared rd_arb. Asserts every in-window pixel
// decodes to the known FB value AND underflow stays 0 AND the probe completes.
// ============================================================================
`timescale 1ns/1ps

module tb_gs_lpddr_scanout_lb_psm32_256;
    localparam int H_TOTAL=288, H_ACTIVE=256, V_TOTAL=280, V_ACTIVE=256;
    localparam [29:0] FB_BASE = 30'd0;
    localparam int STRIDE=1024, ROW_BEATS=32, NROWS=256;     // 128 px * 4 B PSMCT32

    logic axi_clk=0, video_clk=0, rst_n=0, en=0;
    always #2  axi_clk   = ~axi_clk;    // fast EMIF
    always #8  video_clk = ~video_clk;  // pixel clock (4x slower)

    // known FB: pixel p -> ABGR {0xFF, B=p^0xAA, G=p^0x55, R=p[7:0]}
    function automatic [31:0] pix32(input int p); pix32 = {8'hFF, 8'((p^'hAA)&'hFF), 8'((p^'h55)&'hFF), 8'(p&'hFF)}; endfunction
    logic [255:0] mem [0:16383];
    logic [29:0] a; int beatix, lane;
    initial begin
        for (int i=0;i<8192;i++) mem[i]='0;
        for (int gy=0; gy<NROWS; gy++) for (int gx=0; gx<H_ACTIVE; gx++) begin
            a = FB_BASE + 30'(gy*STRIDE + gx*4);
            beatix = a >> 5; lane = (a >> 2) & 32'd7;
            mem[beatix][lane*32 +: 32] = pix32(gy*H_ACTIVE+gx);
        end
        $display("[lb32] preload check: mem[0] lane0=%08h lane1=%08h lane7=%08h (exp %08h %08h %08h)",
                 mem[0][31:0], mem[0][63:32], mem[0][255:224], pix32(0), pix32(1), pix32(7));
    end

    // raster gen (video_clk) with back porches — vsync (frame_start) leads active row 0 by VBP
    // blank lines so the prefetcher can load row 0 before it displays (like a real PCRTC).
    localparam int HBP=16, VBP=8;
    logic [11:0] hcnt=0, vcnt=0; logic frame_start=0;
    always_ff @(posedge video_clk) begin
        frame_start <= 1'b0;
        if (!en) begin hcnt<=0; vcnt<=0; end
        else if (hcnt==H_TOTAL-1) begin
            hcnt<=0;
            if (vcnt==V_TOTAL-1) begin vcnt<=0; frame_start<=1'b1; end else vcnt<=vcnt+1'b1;
        end else hcnt<=hcnt+1'b1;
    end
    wire active = (hcnt>=HBP)&&(hcnt<HBP+H_ACTIVE)&&(vcnt>=VBP)&&(vcnt<VBP+V_ACTIVE);
    wire [11:0] px = active ? (hcnt-HBP[11:0]) : 12'd0;
    wire [11:0] py = active ? (vcnt-VBP[11:0]) : 12'd0;
    wire in_win = active;

    // DUT line-buffer (s0)
    wire [29:0] s0_aa; wire [1:0] s0_ab; wire [6:0] s0_id; wire [7:0] s0_al; wire [2:0] s0_as; wire s0_av,s0_ar;
    wire [255:0] s0_rd; wire [1:0] s0_rr; wire s0_rl,s0_rv,s0_rrdy;
    wire [7:0] lb_r,lb_g,lb_b; wire lb_lvalid, lb_uflow; wire [31:0] lb_rderrs;
    gs_lpddr_scanout_lb #(.FB_BASE(FB_BASE), .STRIDE_BYTES(STRIDE), .ROW_BEATS(ROW_BEATS),
                          .N_ROWS(NROWS), .PSMCT32(1'b1)) dut (
        .axi_clk(axi_clk), .axi_rst_n(rst_n), .enable(en),
        .video_clk(video_clk), .frame_start(frame_start), .pixel_x(px), .pixel_y(py),
        .in_window(in_win), .r(lb_r), .g(lb_g), .b(lb_b),
        .line_valid(lb_lvalid), .underflow(lb_uflow), .rd_errs(lb_rderrs),
        .araddr(s0_aa), .arburst(s0_ab), .arid(s0_id), .arlen(s0_al), .arsize(s0_as),
        .arvalid(s0_av), .arready(s0_ar), .rdata(s0_rd), .rresp(s0_rr), .rlast(s0_rl), .rvalid(s0_rv), .rready(s0_rrdy)
    );

    // reload (s3) hammer
    wire [29:0] s3_aa; wire [1:0] s3_ab; wire [6:0] s3_id; wire [7:0] s3_al; wire [2:0] s3_as; wire s3_av,s3_ar;
    wire [255:0] s3_rd; wire [1:0] s3_rr; wire s3_rl,s3_rv,s3_rrdy;
    logic reload_start; logic [7:0] rl_raddr;
    gs_tile_reload #(.COLOR_BASE(FB_BASE), .Z_BASE(30'h0010_0000), .TILE_W(16), .TILE_H(16),
                     .STRIDE_BYTES(STRIDE), .ROW_BEATS(2), .COLOR_W(32)) u_reload (
        .axi_clk(axi_clk), .axi_rst_n(rst_n), .reload_start(reload_start), .reload_base(30'd0), .reload_done(),
        .color_beats(), .z_beats(), .rd_errs(),
        .araddr(s3_aa), .arburst(s3_ab), .arid(s3_id), .arlen(s3_al), .arsize(s3_as), .arvalid(s3_av), .arready(s3_ar),
        .rdata(s3_rd), .rresp(s3_rr), .rlast(s3_rl), .rvalid(s3_rv), .rready(s3_rrdy),
        .serve_clk(video_clk), .raddr(rl_raddr), .color_o(), .z_o(), .reload_ready()
    );

    // probe (s1)
    wire [29:0] s1_aa; wire [1:0] s1_ab; wire [6:0] s1_id; wire [7:0] s1_al; wire [2:0] s1_as; wire s1_av,s1_ar;
    wire [255:0] s1_rd; wire [1:0] s1_rr; wire s1_rl,s1_rv,s1_rrdy;
    logic pr_pulse; logic [31:0] pr_addr; wire pr_done; wire [31:0] pr_data;
    gs_lpddr_rd_probe #(.ADDR_W(30)) u_probe (
        .axi_clk(axi_clk), .axi_rst_n(rst_n), .rd_pulse(pr_pulse), .rd_addr(pr_addr),
        .rd_done(pr_done), .rd_data(pr_data), .rd_busy(),
        .araddr(s1_aa), .arburst(s1_ab), .arid(s1_id), .arlen(s1_al), .arsize(s1_as), .arvalid(s1_av), .arready(s1_ar),
        .rdata(s1_rd), .rresp(s1_rr), .rlast(s1_rl), .rvalid(s1_rv), .rready(s1_rrdy)
    );

    // rd_arb + behavioral EMIF
    wire [29:0] m_aa; wire [1:0] m_ab; wire [6:0] m_id; wire [7:0] m_al; wire [2:0] m_as; wire m_av,m_ar;
    wire [255:0] m_rd; wire [1:0] m_rr; wire m_rl,m_rv,m_rrdy;
    gs_lpddr_rd_arb u_arb (
        .clk(axi_clk), .rst_n(rst_n),
        .s0_araddr(s0_aa),.s0_arburst(s0_ab),.s0_arid(s0_id),.s0_arlen(s0_al),.s0_arsize(s0_as),.s0_arvalid(s0_av),.s0_arready(s0_ar),
        .s0_rdata(s0_rd),.s0_rresp(s0_rr),.s0_rlast(s0_rl),.s0_rvalid(s0_rv),.s0_rready(s0_rrdy),
        .s1_araddr(s1_aa),.s1_arburst(s1_ab),.s1_arid(s1_id),.s1_arlen(s1_al),.s1_arsize(s1_as),.s1_arvalid(s1_av),.s1_arready(s1_ar),
        .s1_rdata(s1_rd),.s1_rresp(s1_rr),.s1_rlast(s1_rl),.s1_rvalid(s1_rv),.s1_rready(s1_rrdy),
        .s2_araddr(30'd0),.s2_arburst(2'b01),.s2_arid(7'd4),.s2_arlen(8'd0),.s2_arsize(3'b101),.s2_arvalid(1'b0),.s2_arready(),
        .s2_rdata(),.s2_rresp(),.s2_rlast(),.s2_rvalid(),.s2_rready(1'b1),
        .s3_araddr(s3_aa),.s3_arburst(s3_ab),.s3_arid(s3_id),.s3_arlen(s3_al),.s3_arsize(s3_as),.s3_arvalid(s3_av),.s3_arready(s3_ar),
        .s3_rdata(s3_rd),.s3_rresp(s3_rr),.s3_rlast(s3_rl),.s3_rvalid(s3_rv),.s3_rready(s3_rrdy),
        .m_araddr(m_aa),.m_arburst(m_ab),.m_arid(m_id),.m_arlen(m_al),.m_arsize(m_as),.m_arvalid(m_av),.m_arready(m_ar),
        .m_rdata(m_rd),.m_rresp(m_rr),.m_rlast(m_rl),.m_rvalid(m_rv),.m_rready(m_rrdy)
    );
    typedef enum logic [1:0] {RA,RL,RV} rs_t; rs_t rs; logic [29:0] rbeat; logic [3:0] rlat;
    assign m_ar=(rs==RA); assign m_rv=(rs==RV); assign m_rl=(rs==RV); assign m_rr=2'b00; assign m_rd=mem[rbeat[12:0]];
    always_ff @(posedge axi_clk or negedge rst_n) begin
        if (!rst_n) rs<=RA;
        else case (rs)
            RA: if (m_av) begin rbeat<=m_aa[29:5]; rlat<=4'd5; rs<=RL; end
            RL: if (rlat==0) rs<=RV; else rlat<=rlat-1'b1;
            RV: if (m_rrdy) rs<=RA;
        endcase
    end

    // reload + probe hammers
    initial begin reload_start=0; rl_raddr=0; wait(rst_n); repeat(20)@(posedge axi_clk);
        forever begin @(posedge video_clk) reload_start<=1'b1; @(posedge video_clk) reload_start<=1'b0; repeat(1500)@(posedge axi_clk); end
    end
    int probe_ok, probe_bad;
    initial begin pr_pulse=0; pr_addr=0; probe_ok=0; probe_bad=0; wait(rst_n); repeat(40)@(posedge axi_clk);
        forever begin
            pr_addr<=32'h0000_0040; @(posedge axi_clk) pr_pulse<=1'b1; @(posedge axi_clk) pr_pulse<=1'b0;
            begin int g=0; while(!pr_done && g<200000) begin @(posedge axi_clk); g++; end end
            if (!pr_done) probe_bad++; else if (pr_data!==pix32(16)) probe_bad++; else probe_ok++;
            repeat(800)@(posedge axi_clk);
        end
    end

    // scoreboard: sample r/g/b 1 video_clk after px/py (module registers word_q/lane_q/in_q)
    int errors, checked; logic [11:0] spx, spy; logic sin;
    initial begin errors=0; checked=0; end
    logic [31:0] c;
    always_ff @(posedge video_clk) begin
        spx<=px; spy<=py; sin<=in_win;     // delayed copy to align with the 1-cycle pixel pipe
        if (en && sin && (spx<H_ACTIVE) && (spy<NROWS)) begin
            c = pix32(spy*H_ACTIVE+spx);    // per-iteration (NOT a static decl-initializer)
            if (lb_r!==c[7:0] || lb_g!==c[15:8] || lb_b!==c[23:16]) begin
                if (errors<10) $display("[lb32] PIXEL (%0d,%0d): got(%02h,%02h,%02h) exp(%02h,%02h,%02h)",
                                        spx,spy,lb_r,lb_g,lb_b,c[7:0],c[15:8],c[23:16]);
                errors++;
            end
            checked++;
        end
    end

    bit uflow_seen;
    always_ff @(posedge video_clk) if (en && lb_uflow) uflow_seen<=1'b1;

    initial begin
        uflow_seen=0;
        repeat(8)@(posedge axi_clk); rst_n=1; repeat(8)@(posedge video_clk); en=1;
        // run ~3 full frames
        repeat(3*H_TOTAL*V_TOTAL + 400) @(posedge video_clk);
        $display("[lb32] checked=%0d px, errors=%0d, underflow=%0b, rd_errs=%0d, probe_ok=%0d probe_bad=%0d",
                 checked, errors, uflow_seen, lb_rderrs, probe_ok, probe_bad);
        if (errors==0 && !uflow_seen && lb_rderrs==0 && probe_bad==0 && checked>10000 && probe_ok>0)
            $display("[tb_gs_lpddr_scanout_lb_psm32_256] PASS");
        else
            $display("[tb_gs_lpddr_scanout_lb_psm32_256] FAIL");
        $finish;
    end
    initial begin #400000000; $display("[lb32] TIMEOUT"); $display("[tb_gs_lpddr_scanout_lb_psm32_256] FAIL"); $finish; end
endmodule
