// retroDE_ps2 — tb_top_psmct32_persp_floor_demo (Ch301b)
//
// PERSPECTIVE-CORRECT FLOOR demo TB — the human-recognizable perspective demo,
// and (per the architect) the one that proves SEAM CONTINUITY, which the Ch301
// per-triangle TB could NOT (it was fed the same inconsistent vertex data as the
// RTL, so it only proved RTL==fixture-math, not that the two triangles form ONE
// coherent surface).
//
// The fixture (bake.py persp_floor) derives all four trapezoid corners from ONE
// pinhole projection of a flat floor, so s/w, t/w, 1/w are screen-AFFINE over
// the WHOLE quad. This TB therefore builds a SINGLE affine S/T/Q plane (from the
// Tri1 corners NL,NR,FR, extrapolated) and checks EVERY inside pixel of BOTH
// triangles against that one plane:
//
//   PROOF 1 — FLOOR-PLANE MATCH: every interior-of-cell pixel (in Tri1 OR Tri2)
//             matches persp_texel(u,v) where (u,v)=(S/Q,T/Q) from the single
//             plane. A per-triangle divergence (the Ch301 shear bug) would make
//             Tri2 pixels mismatch this one-plane reference → caught.
//   PROOF 2 — SEAM CONTINUITY: Tri2's interior pixels are checked against the
//             SAME plane fitted from Tri1's vertices; requiring Tri2 to match it
//             is exactly the assertion that the two triangles agree across the
//             shared NL-FR diagonal. Tracked + asserted separately.
//   PROOF 3 — PERSPECTIVE != AFFINE: at interior pixels where the perspective
//             and affine references give different cells, captured matches
//             perspective (compression toward the far/top edge).

`timescale 1ns/1ps

module tb_top_psmct32_persp_floor_demo;

    localparam int H_ACTIVE = 34;
    localparam int V_ACTIVE = 19;

    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;
    logic        frame_toggle, dma_done_toggle;

    top_psmct32_raster_demo_bram #(
        .H_ACTIVE          (H_ACTIVE),
        .V_ACTIVE          (V_ACTIVE),
        .VRAM_BYTES        (8 * 1024),
        .PSMCT32_SWIZZLE   (1'b0),
        .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)
    );

    function automatic logic [23:0] exp_cell(input integer u, input integer v);
        if ((((u >> 2) + (v >> 2)) & 1)) exp_cell = 24'hFFFFFF;
        else                             exp_cell = 24'h902020;   // {b,g,r}
    endfunction
    function automatic real bdet(input real ax,input real ay,input real bx,input real by,input real cx,input real cy);
        bdet = (by-cy)*(ax-cx) + (cx-bx)*(ay-cy);
    endfunction
    function automatic real bwa(input real px,input real py,input real ax,input real ay,input real bx,input real by,input real cx,input real 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,input real py,input real ax,input real ay,input real bx,input real by,input real cx,input real cy);
        bwb = ((cy-ay)*(px-cx) + (ax-cx)*(py-cy)) / bdet(ax,ay,bx,by,cx,cy);
    endfunction
    function automatic real edge_dist(input real c);
        real m; m = c - (4.0 * $floor(c/4.0));
        edge_dist = (m < (4.0-m)) ? m : (4.0-m);
    endfunction

    // ---- capture ----
    logic [7:0] cap_r [0:V_ACTIVE-1][0:H_ACTIVE-1];
    logic [7:0] cap_g [0:V_ACTIVE-1][0:H_ACTIVE-1];
    logic [7:0] 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) begin
        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
    end

    int errors, t1_ok, t1_tot, t2_ok, t2_tot, pna, pna_ok;

    initial begin
        errors=0; t1_ok=0; t1_tot=0; t2_ok=0; t2_tot=0; pna=0; pna_ok=0;
        rst_n=1'b0; core_go=1'b0;
        repeat(4) @(posedge clk); rst_n=1'b1; repeat(8) @(posedge clk);
        @(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 (dut.u_gs.raster_active==1'b1) wait (dut.u_gs.raster_active==1'b0);
        repeat(10) @(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=0; py<V_ACTIVE; py++) begin
            for (int px=0; px<H_ACTIVE; px++) begin
                real w1a,w1b,w1c, w2a,w2b,w2c;
                real S,T,Q, pu,pv, au,av;
                bit  in1, in2;
                logic [23:0] pe, ae;
                // Tri1 = NL(0,18),NR(32,18),FR(20,0) ; Tri2 = NL(0,18),FR(20,0),FL(12,0)
                w1a=bwa(px,py, 0,18, 32,18, 20,0); w1b=bwb(px,py, 0,18, 32,18, 20,0); w1c=1.0-w1a-w1b;
                w2a=bwa(px,py, 0,18, 20,0, 12,0);  w2b=bwb(px,py, 0,18, 20,0, 12,0);  w2c=1.0-w2a-w2b;
                in1 = (w1a>=-0.001)&&(w1b>=-0.001)&&(w1c>=-0.001);
                in2 = (w2a>=-0.001)&&(w2b>=-0.001)&&(w2c>=-0.001);
                if (in1 || in2) begin
                    // SINGLE plane from Tri1 corners (NL,NR,FR), extrapolated for
                    // Tri2 pixels — this is the floor-plane / seam-continuity ref.
                    // u/w,v/w,1/w: NL(0,0,1) NR(15,0,1) FR(3.75,3.75,0.25)
                    S = w1a*0.0  + w1b*15.0 + w1c*3.75;
                    T = w1a*0.0  + w1b*0.0  + w1c*3.75;
                    Q = w1a*1.0  + w1b*1.0  + w1c*0.25;
                    pu = S/Q; pv = T/Q;
                    // affine ref (interpolate integer u,v directly via Tri1)
                    au = w1a*0.0 + w1b*15.0 + w1c*15.0;
                    av = w1a*0.0 + w1b*0.0  + w1c*15.0;
                    if (pu<0) pu=0; if (pu>15) pu=15;  if (pv<0) pv=0; if (pv>15) pv=15;
                    if (au<0) au=0; if (au>15) au=15;  if (av<0) av=0; if (av>15) av=15;
                    pe = exp_cell(int'(pu+0.5), int'(pv+0.5));
                    ae = exp_cell(int'(au+0.5), int'(av+0.5));

                    if (edge_dist(pu)>1.0 && edge_dist(pv)>1.0 && cap_de[py][px]) begin
                        bit match;
                        match = ({cap_b[py][px],cap_g[py][px],cap_r[py][px]} === pe);
                        // PROOF 1 + 2 — attribute to whichever triangle owns it (Tri1 priority).
                        if (in1) begin t1_tot++; if (match) t1_ok++; end
                        else     begin t2_tot++; if (match) t2_ok++; end
                        if (!match) begin
                            if (errors<12)
                                $error("[pfloor] (%0d,%0d) tri%0d u=%.2f v=%.2f got(%02x,%02x,%02x) exp(%06x)",
                                       px,py, in1?1:2, pu,pv, cap_r[py][px],cap_g[py][px],cap_b[py][px], pe);
                            errors++;
                        end
                        // PROOF 3
                        if (pe!==ae) begin pna++; if (match) pna_ok++; end
                    end
                end
            end
        end

        // Tri2 (the FL-side sliver) is geometrically small, so fewer interior-of-
        // cell samples than Tri1 — 8 is plenty to prove seam continuity (every
        // one must still match the single Tri1-derived plane).
        if (t1_tot < 20 || t2_tot < 8) begin
            $error("[pfloor] too few interior pixels (tri1=%0d tri2=%0d) — geometry/capture issue", t1_tot, t2_tot); errors++;
        end
        // PROOF 1: Tri1 matches the plane.
        if (t1_ok != t1_tot) begin $error("[pfloor] Tri1 plane match %0d/%0d", t1_ok, t1_tot); errors++; end
        // PROOF 2 (SEAM): Tri2 must ALSO match the SAME (Tri1-derived) plane.
        if (t2_ok != t2_tot) begin
            $error("[pfloor] SEAM DISCONTINUITY: Tri2 matched the single floor-plane only %0d/%0d — the two triangles disagree across the shared diagonal", t2_ok, t2_tot);
            errors++;
        end
        // PROOF 3
        if (pna < 8) begin $error("[pfloor] only %0d persp!=affine pixels — warp too weak", pna); errors++; end
        else if (pna_ok != pna) begin $error("[pfloor] captured matched AFFINE not PERSPECTIVE at %0d/%0d", pna-pna_ok, pna); errors++; end
        if (!core_halt)      begin $error("core_halt low"); errors++; end
        if (raster_overflow) begin $error("raster_overflow"); errors++; end

        $display("[tb_top_psmct32_persp_floor_demo] tri1=%0d/%0d tri2(seam)=%0d/%0d persp!=affine=%0d(ok=%0d) errors=%0d",
                 t1_ok,t1_tot, t2_ok,t2_tot, pna,pna_ok, errors);
        if (errors==0) $display("[tb_top_psmct32_persp_floor_demo] PASS");
        else           $display("[tb_top_psmct32_persp_floor_demo] FAIL");
        $finish;
    end

    initial begin #80000000; $error("[tb_top_psmct32_persp_floor_demo] TIMEOUT"); $finish; end

endmodule : tb_top_psmct32_persp_floor_demo
