// retroDE_ps2 — tb_top_psmct32_perspective_demo (Ch301) // // TOP-LEVEL PERSPECTIVE-CORRECT textured-triangle demo for the BRAM board // variant. A receding "floor" quad (2 TME TRIANGLEs) textured with a 4x4-cell // checkerboard. Texture coords supplied via ST (S=u/w, T=v/w) + RGBAQ.Q (=1/w); // top edge FAR (w=8), bottom NEAR (w=1). With PERSPECTIVE_CORRECT=1 the // rasterizer recovers per-pixel (u,v)=(S/Q,T/Q) through the pipelined reciprocal // LUT + gs_persp_uv (NO per-pixel divider) — the checkerboard compresses toward // the far (top) edge. PSMCT32_SWIZZLE=0 (linear texture). // // Verification (matches bake.py persp_* fixture): // PROOF 1 — per-pixel vs a barycentric PERSPECTIVE reference (real math): // interior-of-cell pixels (>1 texel from a 4px cell edge) must match // the captured scanout exactly (the recip's sub-texel error can only // flip a cell exactly at a boundary, which we skip). // PROOF 2 — PERSPECTIVE != AFFINE: where perspective and affine references // disagree, the captured frame must match PERSPECTIVE (not affine). `timescale 1ns/1ps module tb_top_psmct32_perspective_demo; localparam int H_ACTIVE = 16; localparam int V_ACTIVE = 24; localparam int SCR_W = 16, SCR_H = 24, TEXW = 16, TEXH = 16; real W_FAR; real W_NEAR; initial begin W_FAR = 8.0; W_NEAR = 1.0; end 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) ); // expected checkerboard texel (mirrors bake.py persp_texel): packed {b,g,r}. 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 // single-return barycentric helpers (no output args — iverilog-12 friendly). 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 // distance from c to the nearest 4px cell edge. 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 // ---- frame 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;yTEXW-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)); // PROOF 1 — interior-of-cell pixels match perspective exactly. 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("[persp] (%0d,%0d) u=%.2f v=%.2f got(%02x,%02x,%02x) exp(%06x)", px,py,pu,pv, cap_r[py][px],cap_g[py][px],cap_b[py][px], pe); errors++; end end // PROOF 2 — where the perspective and affine references give // DIFFERENT cells (and the perspective pixel is cell-interior, // so its captured color is unambiguous), the captured frame // must match PERSPECTIVE — proving it is not the affine path. 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("[persp] too few interior pixels (%0d) — geometry/capture issue", interior_total); errors++; end if (interior_ok != interior_total) begin $error("[persp] interior match %0d/%0d", interior_ok, interior_total); errors++; end if (persp_ne_affine < 8) begin $error("[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("[persp] captured matches AFFINE not PERSPECTIVE at %0d/%0d divergent pixels", persp_ne_affine - persp_ne_affine_ok, persp_ne_affine); errors++; end if (!core_halt) begin $error("core_halt low"); errors++; end if (raster_overflow) begin $error("raster_overflow set"); errors++; end $display("[tb_top_psmct32_perspective_demo] 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) $display("[tb_top_psmct32_perspective_demo] PASS"); else $display("[tb_top_psmct32_perspective_demo] FAIL"); $finish; end initial begin #80000000; $error("[tb_top_psmct32_perspective_demo] TIMEOUT"); $finish; end endmodule : tb_top_psmct32_perspective_demo