// retroDE_ps2 — tb_gs_lpddr_scanout_fb  (Ch353 Brick 2 — PSMCT32 LPDDR-FB line-buffer scanout proof)
//
// Closes the loop: the widened writer fills a behavioral LPDDR framebuffer, frame_drained asserts (EMIF domain),
// then gs_lpddr_scanout_lb reads that SAME FB back and serves r/g/b to a REAL video raster (independent video clock,
// with vertical back porch). Asserts every one of the 256x334 pixels — including the BLACK background — matches the
// FB, with underflow=0, rd_errs=0, and exactly 334*32=10688 read beats/frame.
// Codex Brick-2 gates: independent EMIF/video clocks + real cadence; enable gated by frame_drained; FB precleared to
// 0; compare ALL pixels incl. black; account for the 1-video-cycle registered pixel latency; variable AR/R latency.
`timescale 1ns/1ps

module tb_gs_lpddr_scanout_fb;
    localparam int W = 256, H = 334;            // frame
    localparam int STRIDE = W*4;                // 1024 B (PSMCT32)
    localparam int ROW_BEATS = STRIDE/32;       // 32
    localparam int FB_BYTES = STRIDE*H;         // 342016
    localparam int BEATS_PER_FRAME = ROW_BEATS*H; // 10688
    // active + blanking (real cadence, incl. horizontal + vertical back/front porch)
    localparam int H_ACT=256, V_ACT=334, H_BP=32, H_FP=8, V_BP=16, V_FP=8;
    localparam int H_TOT=H_BP+H_ACT+H_FP, V_TOT=V_BP+V_ACT+V_FP;

    logic gs_clk=0;    always #5 gs_clk=~gs_clk;     // writer GS/design clock
    logic emif_clk=0;  always #2 emif_clk=~emif_clk; // EMIF (fast, independent)
    logic video_clk=0; always #7 video_clk=~video_clk;// video (independent of both)
    logic rst_n;
    logic fb_commit=0; always #13 fb_commit=~fb_commit;

    int errors; initial errors=0;

    // ============ deterministic frame: left half (x<128) colored, right half BLACK (background) ============
    function automatic logic [31:0] exp_word(input int x, input int y);
        logic [7:0] r,g,b;
        if (x < 128) begin
            r = x[7:0]; g = y[7:0]; b = (x*3 + y*7);
            if (r==0 && g==0 && b==0) r = 8'h01;   // keep the covered region non-black
            exp_word = {8'h80, b, g, r};
        end else exp_word = 32'd0;                 // background stays precleared 0
    endfunction

    // ============ WRITER (PSMCT32) fills the behavioral LPDDR FB ============
    logic        wr_emit=0, wr_flush=0; logic [31:0] wr_addr=0, wr_data=0;
    logic [255:0] fbw_wdata; logic [31:0] fbw_wstrb, fbw_awaddr; logic fbw_awvalid, fbw_wvalid, fbw_bready;
    logic        fbw_drained, fbw_idle; logic [31:0] fbw_beats, fbw_ovf;
    gs_lpddr_axi_master #(.FIFO_DEPTH(64), .PIX_BYTES(4)) u_wr (
        .gs_clk(gs_clk), .gs_rst_n(rst_n), .enable(1'b1),
        .arm(1'b1), .canary(1'b0), .fb_base(32'h0), .ctrl_commit(fb_commit),
        .px_emit(wr_emit), .px_addr(wr_addr), .px_pix32(wr_data), .flush(wr_flush),
        .axi_clk(emif_clk), .axi_rst_n(rst_n),
        .awaddr(fbw_awaddr), .awlen(), .awsize(), .awburst(), .awid(),
        .awvalid(fbw_awvalid), .awready(1'b1),
        .wdata(fbw_wdata), .wstrb(fbw_wstrb), .wlast(), .wvalid(fbw_wvalid), .wready(1'b1),
        .bvalid(1'b1), .bready(fbw_bready), .bresp(2'b00),
        .beats_written(fbw_beats), .bursts_issued(), .bresp_err_count(),
        .fifo_overflow_count(fbw_ovf), .idle(fbw_idle), .frame_drained(fbw_drained)
    );
    // behavioral LPDDR FB — PRECLEARED to 0 (models the HPS preclear), then strobed writes land
    logic [7:0] fb [0:FB_BYTES-1];
    logic [31:0] fb_awlat;
    always_ff @(posedge emif_clk) begin
        if (fbw_awvalid) fb_awlat <= fbw_awaddr;
        if (fbw_wvalid) for (int i=0;i<32;i++) if (fbw_wstrb[i]) begin
            int a; a = fb_awlat + i; if (a>=0 && a<FB_BYTES) fb[a] <= fbw_wdata[i*8 +: 8];
        end
    end

    // ============ SCANOUT reads the SAME FB back ============
    logic [11:0] px, py; logic vsync, in_win;
    logic [7:0]  so_r, so_g, so_b;
    logic        so_underflow; logic [31:0] so_rd_errs; logic so_line_valid;
    logic [29:0] so_araddr; logic [1:0] so_arburst; logic [6:0] so_arid; logic [7:0] so_arlen;
    logic [2:0]  so_arsize; logic so_arvalid, so_arready;
    logic [255:0] so_rdata; logic [1:0] so_rresp; logic so_rlast, so_rvalid, so_rready;
    gs_lpddr_scanout_lb #(.FB_BASE(30'd0), .STRIDE_BYTES(STRIDE), .ROW_BEATS(ROW_BEATS),
                          .N_ROWS(H), .PSMCT32(1'b1)) u_scan (
        .axi_clk(emif_clk), .axi_rst_n(rst_n), .enable(fbw_drained),   // Codex — gate on frame_drained (EMIF domain)
        .video_clk(video_clk), .frame_start(vsync),
        .pixel_x(px), .pixel_y(py), .in_window(in_win),
        .r(so_r), .g(so_g), .b(so_b),
        .line_valid(so_line_valid), .underflow(so_underflow), .rd_errs(so_rd_errs),
        .araddr(so_araddr), .arburst(so_arburst), .arid(so_arid), .arlen(so_arlen), .arsize(so_arsize),
        .arvalid(so_arvalid), .arready(so_arready), .rdata(so_rdata), .rresp(so_rresp),
        .rlast(so_rlast), .rvalid(so_rvalid), .rready(so_rready)
    );
    // FB read model — single-beat, VARIABLE AR/R latency (Codex), counts read beats/frame
    logic [7:0] rlfsr=8'h3C; always_ff @(posedge emif_clk) rlfsr<={rlfsr[6:0], rlfsr[7]^rlfsr[5]^rlfsr[4]^rlfsr[3]};
    typedef enum logic [1:0] { R_IDLE, R_WAIT, R_DATA } rst_t; rst_t rst_state;
    logic [3:0] rdly; logic [29:0] rd_addr_l; int read_beats;
    // count read beats per EMIF-domain frame (between synced vsync edges) — the prefetch's own frame boundary,
    // so it's exactly 334 rows regardless of the video/emif CDC phase.
    logic [2:0] vs_e; wire vs_edge_e = vs_e[1] && !vs_e[2];   // RISING edge only (one reset per frame)
    int fb_reads, fb_reads_last;
    always_ff @(posedge emif_clk or negedge rst_n) begin
        if (!rst_n) begin rst_state<=R_IDLE; so_arready<=0; so_rvalid<=0; so_rlast<=0; so_rresp<=0; so_rdata<=0; rdly<=0;
                          read_beats<=0; vs_e<=0; fb_reads<=0; fb_reads_last<=0; end
        else begin
            so_arready<=0; so_rvalid<=0; so_rlast<=0;
            vs_e <= {vs_e[1:0], vsync};
            if (vs_edge_e) begin fb_reads_last <= fb_reads; fb_reads <= 0; end
            case (rst_state)
                R_IDLE: if (so_arvalid) begin so_arready<=1; rd_addr_l<=so_araddr; rdly<=rlfsr[2:0]; rst_state<=R_WAIT; end
                R_WAIT: if (rdly==0) rst_state<=R_DATA; else rdly<=rdly-1'b1;
                R_DATA: if (so_rready) begin
                    for (int w=0; w<8; w++) begin
                        int a; a = rd_addr_l + w*4;
                        so_rdata[w*32 +: 32] <= (a+3<FB_BYTES) ? {fb[a+3],fb[a+2],fb[a+1],fb[a]} : 32'd0;
                    end
                    so_rresp<=2'b00; so_rvalid<=1; so_rlast<=1; read_beats<=read_beats+1;
                    if (!vs_edge_e) fb_reads<=fb_reads+1;
                    rst_state<=R_IDLE;
                end
            endcase
        end
    end

    // ============ REAL video raster (independent video_clk, active + blanking incl. vertical back porch) ============
    logic vid_run=0; logic [11:0] rawx, rawy;
    always_ff @(posedge video_clk) begin
        if (!vid_run) begin rawx<=0; rawy<=0; end
        else if (rawx==H_TOT-1) begin rawx<=0; rawy<=(rawy==V_TOT-1)?12'd0:rawy+1'b1; end
        else rawx<=rawx+1'b1;
    end
    // active region is OFFSET by the back porch, giving the row prefetcher lead time before the display needs it.
    // The scanout takes ACTIVE-relative pixel_x/pixel_y (0-based) + in_window (as the de25 PCRTC feeds it).
    wire active = (rawx>=H_BP)&&(rawx<H_BP+H_ACT)&&(rawy>=V_BP)&&(rawy<V_BP+V_ACT);
    assign px     = active ? (rawx - H_BP) : 12'd0;
    assign py     = (rawy>=V_BP && rawy<V_BP+V_ACT) ? (rawy - V_BP) : 12'd0;
    assign in_win = active;
    assign vsync  = vid_run && (rawx==0) && (rawy==0);

    // pixel compare — account for the scanout's 1-video-cycle registered latency: r/g/b now correspond to the
    // pixel presented LAST cycle (px_q,py_q). Compare against the FB (incl. black background).
    logic [11:0] px_q, py_q; logic inwin_q, run_q;
    always_ff @(posedge video_clk) begin px_q<=px; py_q<=py; inwin_q<=in_win; run_q<=vid_run; end
    int checked; initial checked=0;
    logic scoring=0;
    always_ff @(posedge video_clk) if (scoring && run_q) begin
        logic [7:0] er,eg,eb; logic [31:0] w;
        if (inwin_q) begin
            int a; a = py_q*STRIDE + px_q*4;
            w  = {fb[a+3],fb[a+2],fb[a+1],fb[a]};
            er = w[7:0]; eg = w[15:8]; eb = w[23:16];
        end else begin er=0; eg=0; eb=0; end
        checked++;
        if (so_r!==er || so_g!==eg || so_b!==eb) begin
            if (errors<12) $error("[scan] px(%0d,%0d) in=%b got(%02x,%02x,%02x) exp(%02x,%02x,%02x)",
                                  px_q, py_q, inwin_q, so_r,so_g,so_b, er,eg,eb);
            errors++;
        end
    end

    initial begin
        rst_n=0;
        for (int i=0;i<FB_BYTES;i++) fb[i]=8'h00;   // Codex — model the HPS preclear (FB starts black)
        repeat(6) @(posedge emif_clk); rst_n=1; repeat(4) @(posedge emif_clk);

        // ---- writer fills the FB with the deterministic frame (raster order, left half only) ----
        for (int y=0;y<H;y++) for (int x=0;x<128;x++) begin
            @(negedge gs_clk); wr_emit<=1'b1; wr_addr<=32'(y*STRIDE + x*4); wr_data<=exp_word(x,y);
        end
        @(negedge gs_clk); wr_emit<=1'b0;
        @(negedge gs_clk); wr_flush<=1'b1; @(negedge gs_clk); wr_flush<=1'b0;
        begin int d=0; while(!fbw_drained && d<400000) begin @(posedge emif_clk); d++; end end
        if (!fbw_drained)  begin $error("[scan] writer frame_drained never asserted"); errors++; end
        if (fbw_ovf!==0)   begin $error("[scan] writer FIFO overflow=%0d", fbw_ovf); errors++; end

        // ---- now enable=frame_drained; run the video raster, let the pipeline prime one frame, then score one full frame ----
        vid_run=1;
        // prime one full frame (fill line buffers, warm the prefetch), landing on the 2nd frame start
        begin int d=0; while(!vsync && d<200000) begin @(posedge video_clk); d++; end end   // 1st vsync
        @(posedge video_clk); while(!vsync) @(posedge video_clk);                            // -> 2nd vsync
        // score EXACTLY one frame, measured vsync -> next vsync (prefetch reads all 334 rows once)
        begin scoring=1;
            @(posedge video_clk); while(!vsync) @(posedge video_clk);   // score exactly one full frame
            scoring=0;
            repeat(60) @(posedge emif_clk);        // let the emif-domain frame edge latch fb_reads_last
            // With rising-edge-only fs_edge_e in the scanout, the prefetcher reads EXACTLY 334 rows/frame.
            if (fb_reads_last !== BEATS_PER_FRAME) begin
                $error("[scan] read beats/frame=%0d exp %0d (%0dx%0d)", fb_reads_last, BEATS_PER_FRAME, H, ROW_BEATS);
                errors++;
            end
        end
        if (so_underflow!==0) begin $error("[scan] underflow asserted (row not ready before its pixel)"); errors++; end
        if (so_rd_errs !==0)  begin $error("[scan] rd_errs=%0d", so_rd_errs); errors++; end
        if (checked < H_ACT*V_ACT) begin $error("[scan] only %0d pixels checked (exp >= %0d) — final rows missed?", checked, H_ACT*V_ACT); errors++; end

        $display("[tb_gs_lpddr_scanout_fb] checked=%0d px, read_beats/frame=%0d (exp %0d), underflow=%0b rd_errs=%0d errors=%0d",
                 checked, fb_reads_last, BEATS_PER_FRAME, so_underflow, so_rd_errs, errors);
        if (errors==0) $display("[tb_gs_lpddr_scanout_fb] PASS");
        else           $display("[tb_gs_lpddr_scanout_fb] FAIL");
        $finish;
    end
    initial begin #60000000; $error("[tb_gs_lpddr_scanout_fb] TIMEOUT"); $finish; end
endmodule : tb_gs_lpddr_scanout_fb
