// retroDE_ps2 — tb_top_psmct32_sh3_lpddr_fb  (Ch353 Brick 1c — LPDDR-only direct PSMCT32 framebuffer proof)
//
// Same SH3 draw as tb_top_psmct32_sh3_real_draw_demo, but the framebuffer is NOT mirrored into BRAM:
//   - FB_LPDDR_ONLY=1 suppresses the BRAM raster write (xfer/CLUT upload still lands in BRAM);
//   - the exposed PSMCT32 raster stream (flush_color32_o/flush_addr_o/flush_emit_o, gated by PSMCT32 psm)
//     drives gs_lpddr_axi_master#(.PIX_BYTES(4)) -> a behavioral, strobe-honouring LPDDR framebuffer;
//   - an end-of-scene `flush` pulse pushes the final partial beat.
// PROOF: every emitted raster pixel (captured into an "ideal" frame) must appear byte-exact in the LPDDR FB.
// This validates the LPDDR-only direct path itself (writer fidelity), independent of render correctness.
// LOCAL/gitignored fixtures (dump-derived); skip-guard if absent.
`timescale 1ns/1ps

module tb_top_psmct32_sh3_lpddr_fb;
`ifdef SH3_FULL_FRAME
`include "sh3_full_params.vh"          // 256x334 full-frame bounding box
`else
`include "sh3_real_params.vh"          // 256x120 cropped (default)
`endif
    localparam int FB_WORDS = FBPXW*FBH;        // cropped 256x120 here; full-frame swaps the fixture (FBH=334)
    localparam int FB_BYTES = FB_WORDS*4;

    logic clk; logic rst_n; initial clk=1'b0; always #5 clk=~clk;   // GS/design clock
    logic axi_clk2; initial axi_clk2=1'b0; always #2 axi_clk2=~axi_clk2;  // faster EMIF-like AXI clock (models the board's 310MHz drain; exercises the real async-FIFO CDC)
    logic        core_go; logic [7:0] r,g,b; logic hsync,vsync,de;
    logic        core_halt,dma_done_seen,frame_seen,raster_overflow,frame_toggle,dma_done_toggle;
    logic        feeder_go_tb, feeder_ready_tb;

    // texture-cache tap + cache<->behavioral texture LPDDR (cloned from the oracle)
    logic        gs_tex_rd_en_o; logic [31:0] gs_tex_rd_addr_o;
    logic [31:0] tex_cache_data; logic tex_cache_ready;
    logic [31:0] tex_cache_hits, tex_bram_hits;
    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;
    logic        fill_start, fill_done; logic [31:0] fill_beats, fill_bytes, tex_rd_errs, fill_crc_w;

    // exposed PSMCT32 raster stream (Ch323 ports) -> LPDDR FB writer
    logic        flush_emit_w; logic [31:0] flush_addr_w; logic [31:0] flush_color32_w; logic [5:0] flush_psm_w;

    top_psmct32_raster_demo_bram #(
        .H_ACTIVE(FBPXW), .V_ACTIVE(FBH),
        .VRAM_BYTES(VRAM_BYTES_P), .VRAM_ENABLE_READ2(1'b0), .PSMCT32_SWIZZLE(1'b0),
        .COMBINED_TAZ(1'b0), .TILE_LOCAL(1'b0), .TILE_COLS(1), .TILE_ROWS(1),
        .TILE_MULTIPRIM(1'b0), .TILE_PRIM_COUNT(1), .TILE_FIFO_DEPTH(8),
        .BIN_BUFFER_ENABLE(1'b0), .HEARTBEAT_SPLICE_ENABLE(1'b0),
        .FEEDER_ENABLE(1'b1), .FEEDER_STG_WORDS(STG_WORDS),
        .PERSPECTIVE_CORRECT(1'b1), .PERSP_RECIP_IDX_BITS(11),
        .GRAD_SEQ_DIVIDER(1'b1), .GRAD_DIV_CYCLES(4),
        .GS_LPDDR_TEX(1'b1), .TEX_VRAM_BASE(TEX_VRAM_BASE), .TEX_CACHE_BYTES(TEX_BYTES),
        .CLUT_CSM1_ENABLE(1'b1),
        .FB_LPDDR_ONLY(1'b1)                    // Ch353 — suppress BRAM raster mirror (CLUT/xfer still write)
    ) dut (
        .clk(clk), .rst_n(rst_n), .core_go(core_go),
        .r(r), .g(g), .b(b), .hsync(hsync), .vsync(vsync), .de(de),
        .core_halt(core_halt), .dma_done_seen(dma_done_seen), .frame_seen(frame_seen), .raster_overflow(raster_overflow),
        .frame_toggle(frame_toggle), .dma_done_toggle(dma_done_toggle), .joy_a_pressed_i(1'b0), .joy_b_pressed_i(1'b0),
        .feeder_stg_we_i(1'b0), .feeder_stg_waddr_i(12'd0), .feeder_stg_wdata_i(64'd0),
        .feeder_go_i(feeder_go_tb), .feeder_ready_o(feeder_ready_tb), .feeder_records_o(), .feeder_waits_o(),
        .flush_emit_o(flush_emit_w), .flush_addr_o(flush_addr_w),
        .flush_pix16_o(), .flush_color32_o(flush_color32_w), .flush_psm_o(flush_psm_w),
        .gs_tex_rd_en_o(gs_tex_rd_en_o), .gs_tex_rd_addr_o(gs_tex_rd_addr_o),
        .tex_cache_data_i(tex_cache_data), .tex_cache_ready_i(tex_cache_ready),
        .tex_cache_hits_o(tex_cache_hits), .tex_bram_hits_o(tex_bram_hits)
    );

    gs_texture_cache #(
        .LPDDR_TEX_BASE(LPDDR_TEX_BASE), .TEX_VRAM_BASE(TEX_VRAM_BASE),
        .TEX_BYTES(TEX_BYTES), .N_BEATS(N_BEATS)
    ) u_cache (
        .axi_clk(clk), .axi_rst_n(rst_n),
        .fill_start(fill_start), .fill_done(fill_done),
        .fill_beats(fill_beats), .fill_bytes(fill_bytes), .rd_errs(tex_rd_errs), .fill_crc(fill_crc_w),
        .araddr(araddr), .arburst(arburst), .arid(arid), .arlen(arlen),
        .arsize(arsize), .arvalid(arvalid), .arready(arready),
        .rdata(rdata), .rresp(rresp), .rlast(rlast), .rvalid(rvalid), .rready(rready),
        .sample_clk(clk), .tex_rd_en(gs_tex_rd_en_o),
        .tex_rd_addr(gs_tex_rd_addr_o), .tex_rd_data(tex_cache_data), .tex_ready(tex_cache_ready)
    );

    // behavioral texture LPDDR (cloned from the oracle)
    logic [31:0] lpddr_mem [0:(TEX_BYTES/4)-1];
    typedef enum logic [1:0] { S_IDLE, S_WAIT, S_DATA } sstate_t; sstate_t sst;
    logic [3:0] dly; int beat_idx;
    always_ff @(posedge clk) begin
        if (!rst_n) begin sst<=S_IDLE; arready<=0; rvalid<=0; rlast<=0; rresp<=2'b00; rdata<='0; dly<='0; end
        else begin
            arready<=0; rvalid<=0; rlast<=0;
            case (sst)
                S_IDLE: if (arvalid) begin arready<=1; beat_idx<=(araddr-LPDDR_TEX_BASE)>>5; dly<=4'd2; sst<=S_WAIT; end
                S_WAIT: if (dly==0) sst<=S_DATA; else dly<=dly-1'b1;
                S_DATA: if (rready) begin
                    for (int w=0; w<8; w++) rdata[w*32 +: 32] <= lpddr_mem[beat_idx*8 + w];
                    rresp<=2'b00; rvalid<=1; rlast<=1; sst<=S_IDLE;
                end
            endcase
        end
    end

    // ---- Ch353 LPDDR FRAMEBUFFER writer (PSMCT32) + behavioral strobe-honouring LPDDR FB ----
    logic fb_commit=0; always #13 fb_commit=~fb_commit;   // free-run ctrl_commit so arm latches (PIX_BYTES=4)
    logic        fb_flush=0;
    logic [255:0] fbw_wdata; logic [31:0] fbw_wstrb, fbw_awaddr; logic fbw_awvalid, fbw_wvalid;
    logic [31:0] fbw_beats, fbw_ovf; logic fbw_idle, fbw_drained;
    gs_lpddr_axi_master #(.FIFO_DEPTH(64), .PIX_BYTES(4)) u_fbwriter (
        .gs_clk(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(flush_emit_w && (flush_psm_w == 6'h00)),   // PSMCT32 raster emits only
        .px_addr(flush_addr_w), .px_pix32(flush_color32_w), .flush(fb_flush),
        .axi_clk(axi_clk2), .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(), .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: write only strobed bytes at the latched AW addr
    logic [7:0] fb_lpddr [0:FB_BYTES-1];
    logic [31:0] fb_aw_lat;
    always_ff @(posedge axi_clk2) begin       // writer's AXI domain
        if (fbw_awvalid) fb_aw_lat <= fbw_awaddr;
        if (fbw_wvalid) for (int i=0;i<32;i++) if (fbw_wstrb[i]) begin
            int a; a = fb_aw_lat + i; if (a>=0 && a<FB_BYTES) fb_lpddr[a] <= fbw_wdata[i*8 +: 8];
        end
    end
    // "ideal" frame: every emitted PSMCT32 pixel, captured straight off the raster stream
    logic [31:0] ideal [0:FB_WORDS-1]; logic ideal_set [0:FB_WORDS-1];
    always_ff @(posedge clk) begin
        if (rst_n && flush_emit_w && (flush_psm_w == 6'h00)) begin
            ideal    [flush_addr_w>>2] <= flush_color32_w;
            ideal_set[flush_addr_w>>2] <= 1'b1;
        end
    end

    // ---- Ch353 (Codex #1) FULL-FRAME bounded ORACLE: score the rendered frame vs the reference texel map.
    // Distinct from the transport proof above — this proves the UNCROPPED renderer is correct, not just that
    // bytes reached LPDDR. Same bounded <=1-texel acceptance as tb_top_psmct32_sh3_real_draw_demo.
    logic [31:0] idx_words [0:(512*512/4)-1];
    logic [31:0] pal       [0:255];
    logic [31:0] refmap    [0:FB_WORDS-1];
    function automatic logic [7:0] sh3_idx(input integer u, input integer v);
        integer lin; logic [31:0] w; lin=v*TW+u; w=idx_words[lin/4]; sh3_idx=w[(8*(lin%4)) +: 8];
    endfunction
    function automatic logic [23:0] exp_cell(input integer u, input integer v);
        exp_cell = pal[sh3_idx(u,v)][23:0]; endfunction

    int errors;
    initial begin
        errors=0; feeder_go_tb=1'b0;
        for (int i=0;i<FB_WORDS;i++) begin ideal_set[i]=1'b0; ideal[i]=32'd0; end  // model the precleared FB (Brick-3 preclear)
        lpddr_mem[0]='x;
        $readmemh(`FEEDER_SH3_REAL_FILE, dut.g_feeder.feeder_stg);
        $readmemh("../../data/top_psmct32_raster_demo/sh3_real_tex_lpddr.mem", lpddr_mem);
        $readmemh("../../data/top_psmct32_raster_demo/sh3_real_idx.mem", idx_words);
        $readmemh("../../data/top_psmct32_raster_demo/sh3_real_pal.mem", pal);
`ifdef SH3_FULL_FRAME
        $readmemh("../../data/top_psmct32_raster_demo/sh3_full_refmap.mem", refmap);
`else
        $readmemh("../../data/top_psmct32_raster_demo/sh3_real_refmap.mem", refmap);
`endif
        if (lpddr_mem[0] === 32'bx) begin
            $display("[tb_top_psmct32_sh3_lpddr_fb] SKIP — sh3_real_*.mem absent (run gs_make_sh3_real_draw_fixture.py)");
            $finish;
        end

        rst_n=1'b0; core_go=1'b0; fill_start=1'b0;
        repeat(4) @(posedge clk); rst_n=1'b1; repeat(8) @(posedge clk);

        // warm the texture cache
        @(posedge clk) fill_start<=1'b1;
        begin int gd=0; while (!fill_done && gd<200000) begin @(posedge clk); gd++; end end
        if (!fill_done)             begin $error("cache fill_done never asserted"); errors++; end
        if (fill_crc_w!==32'hfbdeaa32) begin $error("cache fill_crc=%08x exp fbdeaa32", fill_crc_w); errors++; end

        // boot the EE bootlet (uploads CLUT via BITBLT into BRAM) + render the scene
        @(negedge clk); core_go=1'b1; @(negedge clk); core_go=1'b0;
        wait (core_halt==1'b1); repeat(4) @(posedge clk);
        wait (dma_done_seen==1'b1); repeat(10) @(posedge clk);
        if (dut.xfer_busy==1'b1) wait (dut.xfer_busy==1'b0);
        if (!tex_cache_ready) begin $error("tex cache not ready before raster"); errors++; end
        wait (feeder_ready_tb==1'b1);            // whole scene rendered + drained
        repeat(50) @(posedge clk);

        // end-of-scene flush, then wait on the ORDERED drain ack. frame_drained (axi-domain) is set only when the
        // EOF marker pops — i.e. AFTER the last data beat's BRESP (in-order FIFO + per-beat B). It's a stable level
        // (not a transient like fbw_idle), so sampling it here is race-free. This is Codex's explicit drain ack.
        @(negedge clk) fb_flush<=1'b1; @(negedge clk) fb_flush<=1'b0;
        begin int d=0; while(!fbw_drained && d<400000) begin @(posedge clk); d++; end end
        if (!fbw_drained)       begin $error("[fbproof] frame_drained never asserted — writer not drained"); errors++; end
        if (fbw_ovf!==0)        begin $error("[fbproof] FIFO overflow=%0d", fbw_ovf); errors++; end

        // PROOF: every covered (emitted) pixel must be byte-exact in the LPDDR FB
        begin
            int nset, mism; nset=0; mism=0;
            for (int w=0; w<FB_WORDS; w++) if (ideal_set[w]) begin
                logic [31:0] fbw;
                nset++;
                fbw = {fb_lpddr[w*4+3], fb_lpddr[w*4+2], fb_lpddr[w*4+1], fb_lpddr[w*4]};
                if (fbw !== ideal[w]) begin
                    if (mism<10) $error("[fbproof] word %0d (byte %0d) LPDDR=%08x ideal=%08x", w, w*4, fbw, ideal[w]);
                    mism++;
                end
            end
            if (nset==0) begin $error("[fbproof] no PSMCT32 pixels captured — psm gate (0x00) wrong?"); errors++; end
            errors += mism;
            $display("[fbproof] covered=%0d words, mismatches=%0d, writer beats=%0d", nset, mism, fbw_beats);
        end

        // ===== Ch353 (Codex #1) FULL-FRAME bounded ORACLE: rendered frame vs reference texel map, <=1 texel =====
        begin
            int m_total, m_ok, int_total, int_ok, cb;
            m_total=0; m_ok=0; int_total=0; int_ok=0; cb=0;
            for (int py=0; py<FBH; py++) for (int px=0; px<FBPXW; px++) begin
                logic [31:0] rm; logic [23:0] fb; int tu, tv, D;
                rm = refmap[py*FBPXW+px];
                if (rm[31]) begin                          // covered by the draw
                    tu = (rm>>9)&9'h1FF; tv = rm&9'h1FF;
                    fb = ideal[py*FBPXW+px][23:0];          // rendered color (== LPDDR FB, proven equal above)
                    m_total++;
                    D=9;
                    for (int rad=0; rad<=1; rad++)
                      for (int du=-rad; du<=rad; du++) for (int dv=-rad; dv<=rad; dv++) begin
                        int ch; ch=(du<0?-du:du); if((dv<0?-dv:dv)>ch) ch=(dv<0?-dv:dv);
                        if (ch==rad && D==9 && (tu+du)>=0 && (tu+du)<TW && (tv+dv)>=0 && (tv+dv)<TH
                            && fb===exp_cell(tu+du, tv+dv)) D=rad;
                      end
                    if (D<=1) m_ok++;
                    if (rm[30]) begin int_total++; if (D<=1) int_ok++; end   // interior (seam-free band excluded)
                    begin bit f; f=1'b0; for (int i=0;i<256;i++) if (pal[i][23:0]===fb) f=1'b1; if (!f) cb++; end
                end
            end
            $display("[oracle] full-frame <=1texel ALL=%0d/%0d (%.1f%%) INT=%.1f%% clut_bad=%0d",
                     m_ok, m_total, (m_total>0)?100.0*m_ok/m_total:0.0,
                     (int_total>0)?100.0*int_ok/int_total:0.0, cb);
            if (cb !== 0)            begin $error("[oracle] %0d covered px NOT a CLUT entry (wrong palette)", cb); errors++; end
            if (int_total>0 && (100.0*int_ok/int_total) < 95.0)
                                    begin $error("[oracle] interior <=1texel %.1f%% < 95%% (render wrong)", 100.0*int_ok/int_total); errors++; end
        end

        $display("[tb_top_psmct32_sh3_lpddr_fb] errors=%0d", errors);
        if (errors==0) $display("[tb_top_psmct32_sh3_lpddr_fb] PASS");
        else           $display("[tb_top_psmct32_sh3_lpddr_fb] FAIL");
        $finish;
    end
    initial begin #20000000; $error("[tb_top_psmct32_sh3_lpddr_fb] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_lpddr_fb
