// retroDE_ps2 — tb_top_psmct32_sh3_real_draw_demo  (Ch350 full-res LPDDR integration)
//
// The EXACT Ch349 SH3 draw (f1 idx89761 lamppost/bench) on the newly-closed paths, end-to-end:
//   - reconstructed 512x512 PSMT8 texture lives in a behavioral LPDDR model, warmed into gs_texture_cache
//     (GS_LPDDR_TEX=1) BEFORE raster — the texel-fetch is served from LPDDR, not BRAM;
//   - the real CSM1 CLUT (16x16 CT32 grid bytes) is backdoored into BRAM; the feeder's TEX0 (CSM=0/CSM1,
//     CLD=1) fires the Ch350 CSM1-grid clut_loader path (CLUT_CSM1_ENABLE=1);
//   - the actual draw geometry (68 tris, perspective ST/Q) runs through the feeder S1 path.
// No crop / downscale / repack (Codex). Pixel-diff: read the linear PSMCT32 FB from BRAM and compare against
// the Ch349 host reference (per-pixel texel map) with the Ch348 bounded <=1-texel acceptance.
//
// Fixtures (LOCAL/gitignored): tools/gs_make_sh3_real_draw_fixture.py. NOT in the regression (dump-derived);
// skip-guard if absent. Run gs_extract_sh3_clut.py is NOT needed (this tool reconstructs from the dump).
`timescale 1ns/1ps

module tb_top_psmct32_sh3_real_draw_demo;
`include "sh3_real_params.vh"

    logic clk; logic rst_n; initial clk=1'b0; always #5 clk=~clk;
    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;

    // bram-top <-> texture-cache tap (Ch322)
    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;
    // cache AXI <-> behavioral LPDDR
    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;

    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),   // Ch351 — widened far-W reciprocal
        .GRAD_SEQ_DIVIDER(1'b1),                                 // Ch352 — match the board fit: sequential divider (must stay bit-exact)
        .GRAD_DIV_CYCLES(4),                                     // (ignored when GRAD_SEQ_DIVIDER=1)
        .GS_LPDDR_TEX(1'b1), .TEX_VRAM_BASE(TEX_VRAM_BASE), .TEX_CACHE_BYTES(TEX_BYTES),
        .CLUT_CSM1_ENABLE(1'b1)
    ) 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(),
        .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 LPDDR model: 65536 words = the SWIZZLED PSMT8 texture bytes ----
    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

    // ---- reference data: de-swizzled 512x512 indices (4/word), de-gridded palette, per-pixel texel map ----
    logic [31:0] idx_words [0:(512*512/4)-1];
    logic [31:0] pal       [0:255];
    logic [31:0] clut_grid [0:255];       // raw CT32-grid CLUT bytes -> backdoored into BRAM
    logic [31:0] refmap    [0:FBPXW*FBH-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, match_total, match_ok, clut_bad, int_total, n_drift; real sum_du, sum_dv;
    int hist[0:4], int_hist[0:4], deep_hist[0:4], distinct_seen, deep_total, seam_bad;

    initial begin
        errors=0; match_total=0; match_ok=0; clut_bad=0; int_total=0; n_drift=0; sum_du=0.0; sum_dv=0.0;
        distinct_seen=0; feeder_go_tb=1'b0;
        deep_total=0; seam_bad=0; for (int k=0;k<5;k++) begin hist[k]=0; int_hist[k]=0; deep_hist[k]=0; end
        idx_words[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);
        $readmemh("../../data/top_psmct32_raster_demo/sh3_real_clut.mem",       clut_grid);
        $readmemh("../../data/top_psmct32_raster_demo/sh3_real_refmap.mem",     refmap);
        if (idx_words[0] === 32'bx) begin
            $display("[tb_top_psmct32_sh3_real_draw_demo] 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 LPDDR texture cache FIRST (edge-triggered fill_start; hold high — do not pulse).
        @(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_beats!==N_BEATS)   begin $error("fill_beats=%0d exp %0d", fill_beats, N_BEATS); errors++; end
        if (tex_rd_errs!==0)        begin $error("tex_rd_errs=%0d exp 0", tex_rd_errs); errors++; end
        // Ch352 — cache-fill CRC PROVES tex_mem integrity: sum32 of the words written into tex_mem must equal the
        // texture file's sum32 (the same value the uploader prints / the board reg must report).
        if (fill_crc_w!==32'hfbdeaa32) begin $error("cache fill_crc=%08x exp fbdeaa32 — tex_mem corrupt", fill_crc_w); errors++; end
        else $display("[tb] cache fill_crc=%08x OK (tex_mem integrity proven)", fill_crc_w);

        // Ch352 — NO CLUT backdoor: the BOOTLET (payload_sh3_real) uploads the CSM1 grid CLUT to CBP via a
        // 256x1 BITBLT (the board path). This makes the SIM prove the board CLUT load end-to-end, not a backdoor.
        // (clut_grid is still loaded only for the verify-vs-pal sanity, not written into VRAM.)

        @(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);            // C_READY = the whole scene rendered + drained
        repeat(50) @(posedge clk);

        // DEBUG: dump the rendered FB (linear PSMCT32) for visual triage vs the host reference.
        begin integer fd; fd=$fopen("../../data/top_psmct32_raster_demo/sh3_real_fb_out.mem","w");
            for (int yy=0; yy<FBH; yy++) for (int xx=0; xx<FBPXW; xx++)
                $fwrite(fd, "%08x\n", dut.u_vram.mem[yy*FBPXW+xx]);
            $fclose(fd);
        end

        // ---- pixel-diff: read the linear PSMCT32 FB straight from BRAM ----
        for (int py=0; py<FBH; py++) for (int px=0; px<FBPXW; px++) begin
            logic [31:0] rm, fbw; logic [23:0] fb;
            int tu, tv, interior, deepint;
            rm = refmap[py*FBPXW+px];
            if (rm[31]) begin                                       // covered by the draw
            interior = rm[30]; deepint = rm[29];
            tu = (rm>>9)&9'h1FF; tv = rm&9'h1FF;
            fbw = dut.u_vram.mem[py*FBPXW+px];                      // FBP=0, linear PSMCT32
            fb  = fbw[23:0];                                        // {B,G,R}
            match_total++;
            // min Chebyshev texel-distance D at which fb matches a reference texel (search +/-3) + signed delta.
            begin
                int D, mdu, mdv;
                D=99; mdu=0; mdv=0;
                for (int rad=0; rad<=3; rad++)
                  for (int du=-rad; du<=rad; du++) for (int dv=-rad; dv<=rad; dv++) begin
                    int chev, ttu, ttv; chev=(du<0?-du:du); if ((dv<0?-dv:dv)>chev) chev=(dv<0?-dv:dv);
                    ttu=tu+du; ttv=tv+dv;
                    if (chev==rad && D==99 && ttu>=0 && ttu<TW && ttv>=0 && ttv<TH && fb===exp_cell(ttu,ttv))
                        begin D=rad; mdu=du; mdv=dv; end
                  end
                hist[(D<=3)?D:4]++;
                if (D<=1) match_ok++;
                if (D<=3) begin sum_du=sum_du+mdu; sum_dv=sum_dv+mdv; n_drift++; end
                if (interior) begin int_total++; int_hist[(D<=3)?D:4]++; end
                if (deepint)  begin deep_total++; deep_hist[(D<=3)?D:4]++; end
                else if (interior && D>1) seam_bad++;   // interior-but-near-a-seam, >1 texel
            end
            // every covered pixel must be a real CLUT (palette) entry.
            begin bit f; f=1'b0;
                for (int i=0;i<256;i++) if (pal[i][23:0]===fb) f=1'b1;
                if (!f) clut_bad++;
            end
            end                                                     // close if (rm[31])
        end
        // render non-trivial: count distinct covered colors
        begin logic [23:0] seen [0:63]; int ns; ns=0;
            for (int py=0; py<FBH && ns<64; py++) for (int px=0; px<FBPXW && ns<64; px++)
                if (refmap[py*FBPXW+px][31]) begin
                    logic [23:0] c; bit dup; c=dut.u_vram.mem[py*FBPXW+px][23:0]; dup=0;
                    for (int k=0;k<ns;k++) if (seen[k]===c) dup=1;
                    if (!dup) begin seen[ns]=c; ns++; end
                end
            distinct_seen=ns;
        end

        $display("[tb_top_psmct32_sh3_real_draw_demo] min-texel-dist hist ALL(%0d px): D0=%0d D1=%0d D2=%0d D3=%0d none=%0d",
                 match_total, hist[0],hist[1],hist[2],hist[3],hist[4]);
        $display("[tb_top_psmct32_sh3_real_draw_demo] min-texel-dist hist INTERIOR(%0d px): D0=%0d D1=%0d D2=%0d D3=%0d none=%0d",
                 int_total, int_hist[0],int_hist[1],int_hist[2],int_hist[3],int_hist[4]);
        $display("[tb_top_psmct32_sh3_real_draw_demo] min-texel-dist hist DEEP-INTERIOR(%0d px, seam-free): D0=%0d D1=%0d D2=%0d D3=%0d none=%0d",
                 deep_total, deep_hist[0],deep_hist[1],deep_hist[2],deep_hist[3],deep_hist[4]);
        $display("[tb_top_psmct32_sh3_real_draw_demo] <=1texel ALL=%0d/%0d (%.1f%%) INT=%.1f%% DEEP=%.1f%% mean-delta=(%.3f,%.3f) clut_bad=%0d seam>1tex=%0d distinct=%0d cache_hits=%0d bram_hits=%0d",
                 match_ok, match_total, (match_total>0)?100.0*match_ok/match_total:0.0,
                 (int_total>0)?100.0*(int_hist[0]+int_hist[1])/int_total:0.0,
                 (deep_total>0)?100.0*(deep_hist[0]+deep_hist[1])/deep_total:0.0,
                 (n_drift>0)?sum_du/n_drift:0.0, (n_drift>0)?sum_dv/n_drift:0.0, clut_bad, seam_bad, distinct_seen,
                 tex_cache_hits, tex_bram_hits);

        // Ch351 board-gate acceptance (Codex-approved): authentic palette ONLY (clut_bad=0), texels from LPDDR,
        // NO systematic drift (|mean|<0.05), INTERIOR <=1 texel >=95%, and the seam-free DEEP interior is
        // pixel-clean (D2+D3+none==0). Residuals worse than 1 texel live only in the clip/seam band (interior
        // but near a triangle boundary, from the host-clip T-junctions) — reported as seam>1tex, not absorbed.
        if (match_total < 4000)                        begin $error("too few covered pixels (%0d)", match_total); errors++; end
        if (clut_bad != 0)                             begin $error("%0d covered pixels NOT a CLUT entry — wrong palette/CSM1", clut_bad); errors++; end
        if (tex_cache_hits == 32'd0)                   begin $error("tex_cache_hits=0 — texels NOT served from LPDDR"); errors++; end
        // DEEP (seam-free) zone must be tight: >=98% <=1 texel. The residual <2% is the irreducible dense-
        // texture sub-texel-boundary effect (a correct sample at an exact texel edge picks an adjacent color in
        // a 256-distinct texture) — NOT a perspective error (drift~0 proves the centers are right).
        if (deep_total>0 && (100.0*(deep_hist[0]+deep_hist[1])/deep_total) < 98.0)
                                                       begin $error("DEEP-interior <=1 texel only %.1f%% (<98%%)", 100.0*(deep_hist[0]+deep_hist[1])/deep_total); errors++; end
        if (int_total>0 && (100.0*(int_hist[0]+int_hist[1])/int_total) < 95.0)
                                                       begin $error("interior <=1 texel only %.1f%% (<95%%)", 100.0*(int_hist[0]+int_hist[1])/int_total); errors++; end
        if (n_drift>0 && (sum_du/n_drift>0.05 || sum_du/n_drift<-0.05 || sum_dv/n_drift>0.05 || sum_dv/n_drift<-0.05))
                                                       begin $error("systematic drift (%.3f,%.3f) — |mean| must be <0.05", sum_du/n_drift, sum_dv/n_drift); errors++; end
        if (distinct_seen < 8)                         begin $error("only %0d distinct colors — render collapsed", distinct_seen); errors++; end
        if (raster_overflow)                           begin $error("raster_overflow"); errors++; end

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