// retroDE_ps2 — tb_top_psmct32_sh3_persp_demo (Ch348 authentic integration)
//
// Authentic SH3 PSMT8 texture + real CLUT rendered through the PROVEN perspective-triangle FEEDER path
// (Ch342 feeder_persp config + PERSPECTIVE_CORRECT=1) — composing Ch342 (perspective ST/Q) + Ch347 (SH3
// PSMT8->CLUT). The feeder staging (feeder_sh3_persp.mem) carries a perspective quad + TEX0(PSM=PSMT8,CLD=1);
// the feeder's TEX0 commit fires the clut_loader VRAM->CLUT load. The SH3 PSMT8 texture + CLUT are backdoored
// into VRAM (the upload path is proven by Ch347's board demo).
//
// Label (Codex): authentic SH3 PSMT8 texture + real CLUT through the perspective triangle path, chosen
// perspective geometry — NOT authentic SH3 draw geometry. DECAL/opaque.
//
// Proof: per-pixel barycentric PERSPECTIVE reference (u=S/Q -> SH3 idx -> CLUT). The SH3 texture is per-texel
// (no cells), so sub-texel aliasing at texel boundaries makes a small % differ (like the Ch343 cube 97.5%);
// PASS = high interior match-% + perspective!=affine (warp real) + every covered pixel is a real CLUT entry.
//
// Fixtures (LOCAL/gitignored): tools/gs_extract_sh3_clut.py + tools/gs_make_sh3_persp_fixture.py. NOT in the
// regression (reads dump-derived .mem); skip-guard if absent.

`timescale 1ns/1ps

module tb_top_psmct32_sh3_persp_demo;
    localparam int H_ACTIVE=64, V_ACTIVE=64;
    localparam int TBP=64, CBP=80;             // matches gs_make_sh3_persp_fixture.py
    localparam int TEX_W0=TBP*64, CLUT_W0=CBP*64;
    real W_FAR, W_NEAR; initial begin W_FAR=8.0; W_NEAR=1.0; end
    localparam int QX0=8, QX1=56, QY0=8, QY1=56;   // perspective quad in the FB (matches the fixture)
    localparam int TEXW=64, TEXH=64;

    logic clk, 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;

    top_psmct32_raster_demo_bram #(
        .H_ACTIVE(H_ACTIVE), .V_ACTIVE(V_ACTIVE),
        .VRAM_BYTES(32*1024), .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(256),
        .PERSPECTIVE_CORRECT(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()
    );

    // SH3 fixtures
    logic [31:0] tex_words [0:(TEXW*TEXH/4)-1];   // 1024 packed PSMT8 words (4 idx each)
    logic [31:0] clut_mem  [0:255];
    function automatic logic [7:0] sh3_idx(input integer u, input integer v);
        integer lin; logic [31:0] w;
        lin = v*TEXW + u; w = tex_words[lin/4]; sh3_idx = w[(8*(lin%4)) +: 8];
    endfunction
    function automatic logic [23:0] exp_cell(input integer u, input integer v);  // CLUT[idx] as {B,G,R}
        exp_cell = clut_mem[sh3_idx(u,v)][23:0];
    endfunction
    function automatic real bdet(input real ax,ay,bx,by,cx,cy); bdet=(by-cy)*(ax-cx)+(cx-bx)*(ay-cy); endfunction
    function automatic real bwa(input real px,py,ax,ay,bx,by,cx,cy);
        bwa=((by-cy)*(px-cx)+(cx-bx)*(py-cy))/bdet(ax,ay,bx,by,cx,cy); endfunction
    function automatic real bwb(input real px,py,ax,ay,bx,by,cx,cy);
        bwb=((cy-ay)*(px-cx)+(ax-cx)*(py-cy))/bdet(ax,ay,bx,by,cx,cy); endfunction

    logic [7:0] cap_r[0:V_ACTIVE-1][0:H_ACTIVE-1], cap_g[0:V_ACTIVE-1][0:H_ACTIVE-1], cap_b[0:V_ACTIVE-1][0:H_ACTIVE-1];
    logic       cap_de[0:V_ACTIVE-1][0:H_ACTIVE-1];
    bit capture_armed;
    initial begin
        for (int y=0;y<V_ACTIVE;y++) for (int x=0;x<H_ACTIVE;x++) begin cap_r[y][x]=0;cap_g[y][x]=0;cap_b[y][x]=0;cap_de[y][x]=0; end
        capture_armed=1'b0;
    end
    logic [31:0] hcnt_d, vcnt_d;
    always_ff @(posedge clk) begin
        if (!rst_n) begin hcnt_d<=0; vcnt_d<=0; end
        else begin hcnt_d<=32'(dut.u_pcrtc.hcnt); vcnt_d<=32'(dut.u_pcrtc.vcnt); end
    end
    always_ff @(posedge clk)
        if (rst_n && capture_armed && de && (vcnt_d<V_ACTIVE) && (hcnt_d<H_ACTIVE)) begin
            cap_r[vcnt_d][hcnt_d]<=r; cap_g[vcnt_d][hcnt_d]<=g; cap_b[vcnt_d][hcnt_d]<=b; cap_de[vcnt_d][hcnt_d]<=1'b1; end

    int errors, match_ok, match_total, persp_ne_affine, persp_ne_affine_ok, clut_bad;
    // bounded-mismatch report: histogram of min Chebyshev texel-distance at which the captured color matches
    // a reference texel (0=exact,1,2,3,4=no-match-within-3); separately for INTERIOR (away from tri edges).
    int hist[0:4], int_hist[0:4], int_total, n_drift; real sum_du, sum_dv;

    initial begin
        errors=0; match_ok=0; match_total=0; persp_ne_affine=0; persp_ne_affine_ok=0; clut_bad=0; feeder_go_tb=1'b0;
        int_total=0; n_drift=0; sum_du=0.0; sum_dv=0.0;
        for (int k=0;k<5;k++) begin hist[k]=0; int_hist[k]=0; end
        tex_words[0]='x;
        $readmemh(`FEEDER_SH3_PERSP_FILE, dut.g_feeder.feeder_stg);
        $readmemh("../../data/top_psmct32_raster_demo/sh3_tex_idx64.mem", tex_words);
        $readmemh("../../data/top_psmct32_raster_demo/sh3_clut.mem", clut_mem);
        if (tex_words[0] === 32'bx) begin
            $display("[tb_top_psmct32_sh3_persp_demo] SKIP — sh3_*.mem absent (run gs_extract_sh3_clut.py + gs_make_sh3_persp_fixture.py)");
            $finish;
        end

        rst_n=1'b0; core_go=1'b0;
        repeat(4) @(posedge clk); rst_n=1'b1; repeat(8) @(posedge clk);
        // backdoor the PSMT8 texture (packed 4 idx/word, linear) + the CLUT (256 PSMCT32, contiguous)
        for (int w=0; w<TEXW*TEXH/4; w++) dut.u_vram.mem[TEX_W0 + w]  = tex_words[w];
        for (int i=0; i<256; i++)         dut.u_vram.mem[CLUT_W0 + i] = clut_mem[i];

        @(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);
        // re-assert (the setup xfer may have touched VRAM); the feeder's TEX0 CLD=1 loads the CLUT
        for (int w=0; w<TEXW*TEXH/4; w++) dut.u_vram.mem[TEX_W0 + w]  = tex_words[w];
        for (int i=0; i<256; i++)         dut.u_vram.mem[CLUT_W0 + i] = clut_mem[i];
        wait (feeder_ready_tb==1'b1);
        repeat(200000) @(posedge clk);
        @(posedge dut.u_pcrtc.end_of_frame); @(posedge clk); capture_armed=1'b1;
        @(posedge dut.u_pcrtc.end_of_frame); @(posedge clk); capture_armed=1'b0;

        for (int py=QY0; py<=QY1; py++) for (int px=QX0; px<=QX1; px++) begin
            real wa,wb,wc, su,sv,sq, pu,pv, au,av;
            real uA,vA,wAv, uB,vB,wBv, uC,vC,wCv, lx,ly;
            bit inA, outside; logic [23:0] pe, ae;
            lx=px-QX0; ly=py-QY0; inA=1'b1; outside=1'b0;
            // triangle A = TL(0,0),TR(48,0),BL(0,48) in local coords (quad is 48x48)
            wa=bwa(lx,ly, 0,0, 48,0, 0,48); wb=bwb(lx,ly, 0,0, 48,0, 0,48); wc=1.0-wa-wb;
            if (wa<-0.001||wb<-0.001||wc<-0.001) begin
                inA=1'b0;
                wa=bwa(lx,ly, 48,0, 0,48, 48,48); wb=bwb(lx,ly, 48,0, 0,48, 48,48); wc=1.0-wa-wb;
                if (wa<-0.001||wb<-0.001||wc<-0.001) outside=1'b1;
            end
            if (!outside && cap_de[py][px]) begin
                if (inA) begin uA=0; vA=0; wAv=W_FAR; uB=TEXW; vB=0;    wBv=W_FAR; uC=0;    vC=TEXH; wCv=W_NEAR; end
                else      begin uA=TEXW; vA=0; wAv=W_FAR; uB=0; vB=TEXH; wBv=W_NEAR; uC=TEXW; vC=TEXH; wCv=W_NEAR; end
                su=wa*(uA/wAv)+wb*(uB/wBv)+wc*(uC/wCv);
                sv=wa*(vA/wAv)+wb*(vB/wBv)+wc*(vC/wCv);
                sq=wa*(1.0/wAv)+wb*(1.0/wBv)+wc*(1.0/wCv);
                pu=su/sq; pv=sv/sq;
                au=wa*uA+wb*uB+wc*uC; av=wa*vA+wb*vB+wc*vC;
                if (pu<0) pu=0; if (pu>TEXW-1) pu=TEXW-1; if (pv<0) pv=0; if (pv>TEXH-1) pv=TEXH-1;
                if (au<0) au=0; if (au>TEXW-1) au=TEXW-1; if (av<0) av=0; if (av>TEXH-1) av=TEXH-1;
                pe=exp_cell(int'(pu),int'(pv)); ae=exp_cell(int'(au),int'(av));   // truncate (hardware floor)
                match_total++;
                // Min Chebyshev texel-distance D where the captured color matches a reference texel (search
                // +/-3), with the signed delta at that min. Histogram D + accumulate the delta (drift check).
                // The hardware's fixed-point gs_reciprocal picks a texel within ~+/-1 of the FLOAT reference
                // at boundaries; this MACHINE-PROVES every residual is a bounded <=1-texel reciprocal delta
                // (away from triangle edges) with no systematic drift — not a hand-waved %.
                begin
                    int D, mdu, mdv; real minw;
                    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, tu, tv; chev=(du<0?-du:du); if ((dv<0?-dv:dv)>chev) chev=(dv<0?-dv:dv);
                        tu=int'(pu)+du; tv=int'(pv)+dv;
                        if (chev==rad && D==99 && tu>=0 && tu<TEXW && tv>=0 && tv<TEXH &&
                            {cap_b[py][px],cap_g[py][px],cap_r[py][px]} === exp_cell(tu,tv)) 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
                    minw=wa; if (wb<minw) minw=wb; if (wc<minw) minw=wc;   // bary min weight -> distance to nearest tri edge
                    if (minw > 0.04) begin int_total++; int_hist[(D<=3)?D:4]++; end   // INTERIOR (away from edges/seam)
                end
                if (pe!==ae) persp_ne_affine++;   // warp real: perspective texel differs from affine
                begin bit f; logic [31:0] pc; f=1'b0;   // every covered quad pixel must be a real CLUT entry
                    for (int i=0;i<256;i++) begin pc=clut_mem[i]; if (pc[23:0]==={cap_b[py][px],cap_g[py][px],cap_r[py][px]}) f=1'b1; end
                    if (!f) clut_bad++;
                end
            end
        end

        $display("[tb_top_psmct32_sh3_persp_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_persp_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_persp_demo] <=1texel=%0d/%0d (%.1f%%) mean-delta=(%.3f,%.3f) persp!=affine=%0d clut_bad=%0d",
                 match_ok, match_total, 100.0*match_ok/match_total,
                 (n_drift>0)?sum_du/n_drift:0.0, (n_drift>0)?sum_dv/n_drift:0.0, persp_ne_affine, clut_bad);
        // CRISP bounded acceptance (Codex): authentic palette ONLY (clut_bad=0) + every INTERIOR residual is
        // <=1 texel (no >1-texel error away from tri edges) + no systematic drift + perspective real. Edge/seam
        // pixels (partial coverage) are reported in ALL but not gated. This bounds the residual to fixed-point
        // reciprocal sub-texel noise vs the float reference — NOT a hand-waved percentage.
        if (match_total < 200)                              begin $error("too few quad pixels (%0d)", match_total); errors++; end
        if (clut_bad != 0)                                  begin $error("%0d covered pixels NOT a CLUT entry — wrong palette", clut_bad); errors++; end
        if (int_hist[2]+int_hist[3]+int_hist[4] != 0)       begin $error("%0d INTERIOR pixels >1 texel off — residual NOT bounded to +/-1", int_hist[2]+int_hist[3]+int_hist[4]); errors++; end
        if (n_drift>0 && (sum_du/n_drift>0.3 || sum_du/n_drift<-0.3 || sum_dv/n_drift>0.3 || sum_dv/n_drift<-0.3))
                                                            begin $error("systematic drift (%.3f,%.3f) — not symmetric reciprocal noise", sum_du/n_drift, sum_dv/n_drift); errors++; end
        if (persp_ne_affine < 8)                            begin $error("only %0d persp!=affine — warp too weak", persp_ne_affine); errors++; end
        if (raster_overflow)                                begin $error("raster_overflow"); errors++; end

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