ec82764bef
RTL (GS rasterizer, EE core stub, platform bridge, LPDDR4B path), sim regression (272 TBs), docs, and tooling. Copyrighted PS2 content (BIOS, game code, GS dumps, and all dump-derived textures/traces) is excluded via .gitignore and stays local. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
238 lines
11 KiB
Systemverilog
238 lines
11 KiB
Systemverilog
// 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;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, 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;
|
|
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;
|
|
|
|
// TEMP: dump the captured frame as ASCII (W=white B=blue .=other/none)
|
|
$display("=== captured frame (W=white B=blue) ===");
|
|
for (int dy=0; dy<SCR_H; dy++) begin
|
|
string row;
|
|
row = "";
|
|
for (int dx=0; dx<SCR_W; dx++) begin
|
|
if (!cap_de[dy][dx]) row = {row, "_"};
|
|
else if (cap_r[dy][dx]==8'hFF && cap_g[dy][dx]==8'hFF && cap_b[dy][dx]==8'hFF) row = {row, "W"};
|
|
else if (cap_b[dy][dx]==8'h90) row = {row, "B"};
|
|
else row = {row, "?"};
|
|
end
|
|
$display("y=%2d |%s|", dy, row);
|
|
end
|
|
|
|
for (int py=0; py<SCR_H; py++) begin
|
|
for (int px=0; px<SCR_W; px++) begin
|
|
real wa, wb, wc;
|
|
real 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;
|
|
// triangle A = TL(0,0),TR(15,0),BL(0,23)
|
|
inA = 1'b1; outside = 1'b0;
|
|
wa = bwa(px,py, 0,0, 15,0, 0,23);
|
|
wb = bwb(px,py, 0,0, 15,0, 0,23);
|
|
wc = 1.0 - wa - wb;
|
|
if (wa < -0.001 || wb < -0.001 || wc < -0.001) begin
|
|
inA = 1'b0;
|
|
// triangle B = TR(15,0),BR(15,23),BL(0,23)
|
|
wa = bwa(px,py, 15,0, 15,23, 0,23);
|
|
wb = bwb(px,py, 15,0, 15,23, 0,23);
|
|
wc = 1.0 - wa - wb;
|
|
if (wa < -0.001 || wb < -0.001 || wc < -0.001) outside = 1'b1; // outside quad
|
|
end
|
|
if (!outside) begin
|
|
// per-vertex (u,v,w) for the chosen triangle
|
|
if (inA) begin
|
|
uA=0; vA=0; wAv=W_FAR; uB=15; vB=0; wBv=W_FAR; uC=0; vC=15; wCv=W_NEAR;
|
|
end else begin
|
|
uA=15; vA=0; wAv=W_FAR; uB=15; vB=15; wBv=W_NEAR; uC=0; vC=15; wCv=W_NEAR;
|
|
end
|
|
// perspective: interp u/w,v/w,1/w then divide
|
|
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;
|
|
// affine: interp u,v directly
|
|
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));
|
|
|
|
// 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
|