// retroDE_ps2 — tb_top_psmct32_feeder_persp_demo (Ch342)
//
// HARD GATE: perspective-correct ST/Q through the FEEDER on the actual tiled/multiprim feeder profile
// (FEEDER_ENABLE + TILE_LOCAL + TILE_MULTIPRIM + BIN_BUFFER + COMBINED_TAZ) with PERSPECTIVE_CORRECT=1.
// Drives feeder_persp.mem (word0[32]=1 perspective format, per-vertex RGBAQ/ST/XYZ2) through the
// staging RAM and backdoors the 16x16 checkerboard into VRAM at the bound TBP (=100). A receding quad
// (top FAR w=8, bottom NEAR w=1) so the checkerboard compresses toward the top under correct perspective.
//
// Proofs (mirroring the Ch301 tb_top_psmct32_perspective_demo, but through the FEEDER + tiled path):
//   SEQ   — the feeder emits RGBAQ -> ST -> XYZ2 per vertex (NOT RGBAQ -> UV -> XYZ2): ST(0x02) writes
//           == 6 (2 tris x 3), UV(0x03) writes == 0.
//   PROOF1— interior-of-cell pixels match a per-pixel barycentric PERSPECTIVE reference exactly.
//   PROOF2— where perspective and affine references disagree, the capture matches PERSPECTIVE.

`timescale 1ns/1ps

module tb_top_psmct32_feeder_persp_demo;

    localparam int H_ACTIVE = 64, V_ACTIVE = 64;
    localparam int TEXW = 16, TEXH = 16;
    localparam int TBP = 100;                  // texture base (matches bake.py PERSP_FEEDER_TBP)
    localparam int TEX_W0 = TBP*64;            // VRAM word base = TBP*256/4
    real W_FAR; real W_NEAR;
    initial begin W_FAR = 8.0; W_NEAR = 1.0; end
    // quad corners (screen) + uv + w — must match bake.py PERSP_QUAD.
    localparam int QX0=16, QX1=48, QY0=12, QY1=52;

    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;
    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),
        // Ch342 — DEDICATED perspective profile: FEEDER + S1 perspective path, tiling OFF (the cube is
        // ABE=0 -> non-combined -> S1 path where gs_persp_uv launches). COMBINED_TAZ perspective is a
        // separate documented follow-on bug, NOT this gate.
        .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),
`ifdef GRAD_BOARDPATH
        // Ch417 — verify the ACTUAL BOARD gradient config: registered numerator feeding the single combinational
        // divider, held for the five-cycle settle window.  This used to select GRAD_SEQ_DIVIDER=1 by mistake and
        // therefore did not test the production control path its target name claimed to cover.
        .GRAD_SEQ_DIVIDER(1'b0), .GRAD_DIV_CYCLES(5),
`endif
        .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()
    );

    function automatic logic [23:0] exp_cell(input integer u, input integer v);
        if ((((u >> 2) + (v >> 2)) & 1)) exp_cell = 24'hFFFFFF;   // white
        else                             exp_cell = 24'h902020;   // {b,g,r} dark-blue
    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
    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

    // ---- feeder register-sequence monitor (RGBAQ/ST/XYZ2, not UV) ----
    int n_rgbaq, n_st, n_uv, n_xyz2; logic [7:0] seq_first [0:8]; int seq_n;
    always_ff @(posedge clk) begin
        if (!rst_n) begin n_rgbaq<=0; n_st<=0; n_uv<=0; n_xyz2<=0; seq_n<=0; end
        else if (dut.feeder_gif_reg_wr_en) begin
            unique case (dut.feeder_gif_reg_num)
                8'h01: n_rgbaq<=n_rgbaq+1;  8'h02: n_st<=n_st+1;
                8'h03: n_uv<=n_uv+1;        8'h05: n_xyz2<=n_xyz2+1;
                default: ;
            endcase
            if ((dut.feeder_gif_reg_num==8'h01 || dut.feeder_gif_reg_num==8'h02 || dut.feeder_gif_reg_num==8'h05) && seq_n<9) begin
                seq_first[seq_n]<=dut.feeder_gif_reg_num; seq_n<=seq_n+1;
            end
        end
    end

    // ---- frame capture ----
    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

    logic [31:0] tex_tmp [0:TEXW*TEXH-1];
    int errors, interior_ok, interior_total, persp_ne_affine, persp_ne_affine_ok;

    initial begin
        errors=0; interior_ok=0; interior_total=0; persp_ne_affine=0; persp_ne_affine_ok=0; feeder_go_tb=1'b0;
        $readmemh(`FEEDER_PERSP_FILE, dut.g_feeder.feeder_stg);
        $readmemh(`FEEDER_PERSP_TEX_FILE, tex_tmp);

        rst_n=1'b0; core_go=1'b0;
        repeat(4) @(posedge clk); rst_n=1'b1; repeat(8) @(posedge clk);
        // backdoor the checkerboard into VRAM at TBP (linear, TBW=1 -> 64-word row stride).
        for (int vv=0; vv<TEXH; vv++) for (int uu=0; uu<TEXW; uu++)
            dut.u_vram.mem[TEX_W0 + vv*64 + uu] = tex_tmp[vv*TEXW + uu];

        @(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 texture after the boot xfer (in case setup touched nearby), then render settles.
        for (int vv=0; vv<TEXH; vv++) for (int uu=0; uu<TEXW; uu++)
            dut.u_vram.mem[TEX_W0 + vv*64 + uu] = tex_tmp[vv*TEXW + uu];
        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;

        // ---- SEQ: feeder emitted RGBAQ/ST/XYZ2, not UV ----
        $display("[feeder_persp] reg counts: RGBAQ=%0d ST=%0d UV=%0d XYZ2=%0d ; first-seq=%02x %02x %02x %02x %02x %02x",
                 n_rgbaq, n_st, n_uv, n_xyz2, seq_first[0],seq_first[1],seq_first[2],seq_first[3],seq_first[4],seq_first[5]);
        if (n_st != 6)  begin $error("[feeder_persp] ST writes=%0d (want 6 = 2 tris x 3)", n_st); errors++; end
        if (n_uv != 0)  begin $error("[feeder_persp] UV writes=%0d (want 0 in perspective mode)", n_uv); errors++; end
        if (!(seq_first[0]==8'h01 && seq_first[1]==8'h02 && seq_first[2]==8'h05))
            begin $error("[feeder_persp] vertex seq not RGBAQ->ST->XYZ2 (got %02x %02x %02x)", seq_first[0],seq_first[1],seq_first[2]); errors++; end

        // ---- PROOF1/PROOF2 over the quad region ----
        for (int py=QY0; py<=QY1; py++) begin
            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;
                bit inA, outside; logic [23:0] pe, ae;
                real lx, ly;                       // local quad coords (0..32 x, 0..40 y) for barycentric
                lx = px - QX0; ly = py - QY0;
                inA=1'b1; outside=1'b0;
                // triangle A = TL(0,0),TR(32,0),BL(0,40) in local coords
                wa=bwa(lx,ly, 0,0, 32,0, 0,40); wb=bwb(lx,ly, 0,0, 32,0, 0,40); wc=1.0-wa-wb;
                if (wa<-0.001||wb<-0.001||wc<-0.001) begin
                    inA=1'b0;
                    // triangle B = TR(32,0),BL(0,40),BR(32,40)
                    wa=bwa(lx,ly, 32,0, 0,40, 32,40); wb=bwb(lx,ly, 32,0, 0,40, 32,40); wc=1.0-wa-wb;
                    if (wa<-0.001||wb<-0.001||wc<-0.001) outside=1'b1;
                end
                if (!outside) begin
                    if (inA) begin uA=0;  vA=0;  wAv=W_FAR; uB=16; vB=0;  wBv=W_FAR; uC=0;  vC=16; wCv=W_NEAR; end
                    else      begin uA=16; vA=0;  wAv=W_FAR; uB=0;  vB=16; wBv=W_NEAR; uC=16; vC=16; 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+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
                        interior_total++;
                        if ({cap_b[py][px],cap_g[py][px],cap_r[py][px]} === pe) interior_ok++;
                        else begin
                            if (errors<12) $error("[feeder_persp] (%0d,%0d) u=%.2f v=%.2f got(%02x%02x%02x) exp(%06x)",
                                                   px,py,pu,pv, cap_b[py][px],cap_g[py][px],cap_r[py][px], pe);
                            errors++;
                        end
                    end
                    if (pe!==ae && edge_dist(pu)>1.0 && edge_dist(pv)>1.0 && cap_de[py][px]) begin
                        persp_ne_affine++;
                        if ({cap_b[py][px],cap_g[py][px],cap_r[py][px]} === pe) persp_ne_affine_ok++;
                    end
                end
            end
        end

        if (interior_total < 50) begin $error("[feeder_persp] too few interior pixels (%0d)", interior_total); errors++; end
        if (interior_ok != interior_total) begin $error("[feeder_persp] interior match %0d/%0d", interior_ok, interior_total); errors++; end
        if (persp_ne_affine < 8) begin $error("[feeder_persp] only %0d persp!=affine pixels — warp too weak", persp_ne_affine); errors++; end
        else if (persp_ne_affine_ok != persp_ne_affine) begin
            $error("[feeder_persp] capture matches AFFINE not PERSPECTIVE at %0d/%0d", persp_ne_affine-persp_ne_affine_ok, persp_ne_affine); errors++; end
        if (raster_overflow) begin $error("[feeder_persp] raster_overflow set"); errors++; end

        $display("[feeder_persp] interior_ok=%0d/%0d persp!=affine=%0d (matched persp=%0d) errors=%0d",
                 interior_ok, interior_total, persp_ne_affine, persp_ne_affine_ok, errors);
        if (errors==0) begin $display("[tb_top_psmct32_feeder_persp_demo] PASS"); $finish; end
        else $fatal(1, "[tb_top_psmct32_feeder_persp_demo] FAIL (%0d errors)", errors);  // nonzero exit
    end
    initial begin #1500000000; $error("[tb_top_psmct32_feeder_persp_demo] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_feeder_persp_demo
