ba74bbd5aa
Per-vertex GS fog end-to-end (gs_stub emit incl. persp_emit5, gs_prim_list_feeder XYZ2->XYZF2 on PRIM.FGE, gs_make_sh3_scheduler_fixture.py F/FGE packing), new fog TBs, fidelity attribution tooling. Functional baseline before removing the dead bilinear lerp8 clamps (Codex: 161-node comb loop -> -0.042ns setup fail). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
448 lines
30 KiB
Systemverilog
448 lines
30 KiB
Systemverilog
// retroDE_ps2 — tb_top_psmct32_sh3_zsched (Ch356 — N-TEXTURE SCHEDULER, data-driven epoch descriptors)
|
|
//
|
|
// Generalizes Ch355's two-group flow to N authentic SH3 draw epochs, each with a DIFFERENT TEX0/CLUT, accumulating
|
|
// into ONE LPDDR framebuffer via a data-driven scheduler over epoch descriptors (sh3_zsched_params.vh). Default
|
|
// N_EPOCHS=3: E0=idx11671 E1=idx19562 E2=idx89761. Proves Codex's Ch356 integration acceptance:
|
|
// * preclear EXACTLY once.
|
|
// * N FRESH cache fills, each fill_done low->high, expected beats/bytes, 0 read errors, epoch CRC (EPk_CRC).
|
|
// * N lists STREAMED through the feeder staging WRITE PORT (feeder_stg_we/waddr/wdata) — NOT the $readmemh backdoor
|
|
// (closes the Ch355 sim gap where the write-port sequencing was untested). Each streams the full STG_WORDS so
|
|
// staging is fully reset per epoch (no stale words), and word0 (the ntris header) is exercised first.
|
|
// * N FRESH ORDERED drains: epoch 0 low->high (first render); epochs 1..N-1 high->low->high (stale cleared).
|
|
// * scanout stays disabled until the LAST fresh drain; exact BEATS_PER_FRAME (=ROW_BEATS*H).
|
|
// * final FB scored vs the independent composed reference using the PER-PIXEL OWNER epoch (exact palette per owner);
|
|
// multi-epoch (>=2) overlap scored separately as the accumulation proof.
|
|
// * +ONLY=<k> renders a SINGLE epoch (+FBDUMP dumps its FB) for the composition==isolated bit-for-bit check.
|
|
// LOCAL/gitignored fixtures; skip-guard if absent.
|
|
`timescale 1ns/1ps
|
|
|
|
module tb_top_psmct32_sh3_zsched;
|
|
`include "sh3_zsched_params.vh" // FBPXW=384, FBH=381, N_EPOCHS=3, EPk_CRC/EPk_NTRIS/EPk_CBP, TEX_*, ...
|
|
localparam int W = FBPXW, H = FBH;
|
|
localparam int STRIDE = W*4; // 1536
|
|
localparam int ROW_BEATS = STRIDE/32; // 48
|
|
localparam int FB_BYTES = STRIDE*H; // 585216
|
|
localparam int FB_WORDS = W*H;
|
|
localparam int BEATS_PER_FRAME = ROW_BEATS*H; // 18288
|
|
localparam int H_ACT=W, V_ACT=H, H_BP=32, H_FP=8, V_BP=16, V_FP=8;
|
|
localparam int H_TOT=H_BP+H_ACT+H_FP, V_TOT=V_BP+V_ACT+V_FP;
|
|
localparam int NEP = N_EPOCHS; // 3
|
|
|
|
// per-epoch expected CRC / records (from the descriptor params)
|
|
logic [31:0] EP_CRC [0:2]; int EP_REC [0:2];
|
|
initial begin
|
|
EP_CRC[0]=EP0_CRC; EP_CRC[1]=EP1_CRC; EP_CRC[2]=EP2_CRC;
|
|
EP_REC[0]=EP0_NTRIS; EP_REC[1]=EP1_NTRIS; EP_REC[2]=EP2_NTRIS;
|
|
end
|
|
|
|
logic clk=0; always #5 clk=~clk;
|
|
logic emif_clk=0; always #2 emif_clk=~emif_clk;
|
|
logic video_clk=0; always #7 video_clk=~video_clk;
|
|
logic rst_n; logic fb_commit=0; always #13 fb_commit=~fb_commit;
|
|
int errors; initial errors=0;
|
|
|
|
// ===== bram-top (FB_LPDDR_ONLY, AUTOSTART off, STG 2048), H/V_ACTIVE = 384x381 =====
|
|
logic core_go; logic [7:0] r,g,b; logic hsync,vsync_o,de;
|
|
logic core_halt,dma_done_seen,frame_seen,raster_overflow,frame_toggle,dma_done_toggle;
|
|
logic feeder_go_tb, feeder_ready_tb; logic [15:0] feeder_records_w; logic [31:0] feeder_waits_w;
|
|
logic gs_tex_rd_en_o; logic [31:0] gs_tex_rd_addr_o; logic [31:0] tex_cache_data; logic tex_cache_ready;
|
|
logic [31:0] tex_cache_hits, tex_bram_hits;
|
|
logic flush_emit_w; logic [31:0] flush_addr_w; logic [31:0] flush_color32_w; logic [5:0] flush_psm_w;
|
|
logic [11:0] flush_x_w, flush_y_w; logic [31:0] flush_z_w; // Ch357 — fragment coords + persp_z5
|
|
int cap_ep; int cap_fh; int tex_fh; int issue_fh; // fragment capture (verify persp_z5 vs the clamp16 oracle)
|
|
logic [10:0] cap_pu_q, cap_pv_q, cap_pu_qq, cap_pv_qq; // persp_u/v delayed from texture issue to flush/color
|
|
logic [31:0] tex_issue_addr_q, tex_issue_raw_q; // debug: texture request that returns on the next cycle
|
|
logic [10:0] tex_issue_u_q, tex_issue_v_q;
|
|
// feeder staging WRITE PORT (streamed per epoch)
|
|
logic stg_we; logic [11:0] stg_waddr; logic [63:0] stg_wdata;
|
|
|
|
top_psmct32_raster_demo_bram #(
|
|
.H_ACTIVE(FBPXW), .V_ACTIVE(FBH),
|
|
.VRAM_BYTES(VRAM_BYTES_P), .VRAM_ENABLE_READ2(1'b0), .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(STG_WORDS), .FEEDER_AUTOSTART(1'b0),
|
|
.PERSPECTIVE_CORRECT(1'b1), .PERSP_RECIP_IDX_BITS(11),
|
|
.GRAD_SEQ_DIVIDER(1'b1), .GRAD_DIV_CYCLES(4),
|
|
.GS_LPDDR_TEX(1'b1), .TEX_VRAM_BASE(TEX_VRAM_BASE), .TEX_CACHE_BYTES(TEX_BYTES),
|
|
.CLUT_CSM1_ENABLE(1'b1), .FB_LPDDR_ONLY(1'b1)
|
|
) dut (
|
|
.clk(clk), .rst_n(rst_n), .core_go(core_go),
|
|
.r(r), .g(g), .b(b), .hsync(hsync), .vsync(vsync_o), .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(stg_we), .feeder_stg_waddr_i(stg_waddr), .feeder_stg_wdata_i(stg_wdata),
|
|
.feeder_go_i(feeder_go_tb), .feeder_ready_o(feeder_ready_tb),
|
|
.feeder_records_o(feeder_records_w), .feeder_waits_o(feeder_waits_w),
|
|
.flush_emit_o(flush_emit_w), .flush_addr_o(flush_addr_w),
|
|
.flush_pix16_o(), .flush_color32_o(flush_color32_w), .flush_psm_o(flush_psm_w),
|
|
.flush_x_o(flush_x_w), .flush_y_o(flush_y_w), .flush_z_o(flush_z_w),
|
|
.gs_tex_rd_en_o(gs_tex_rd_en_o), .gs_tex_rd_addr_o(gs_tex_rd_addr_o),
|
|
.tex_cache_data_i(tex_cache_data), .tex_cache_ready_i(tex_cache_ready),
|
|
.tex_cache_hits_o(tex_cache_hits), .tex_bram_hits_o(tex_bram_hits)
|
|
);
|
|
|
|
// Ch357 — capture every emitted fragment {epoch, x, y, persp_z, color}. An external Python check replays these through
|
|
// the clamp16 persistent-Z model and compares owner/reject to the oracle -> validates persp_z5 end-to-end (the new RTL).
|
|
always_ff @(posedge clk) begin
|
|
if (rst_n) begin
|
|
if (dut.u_gs.persp_outvalid) begin
|
|
cap_pu_qq <= cap_pu_q;
|
|
cap_pv_qq <= cap_pv_q;
|
|
cap_pu_q <= dut.u_gs.persp_u;
|
|
cap_pv_q <= dut.u_gs.persp_v;
|
|
end
|
|
if (gs_tex_rd_en_o) begin
|
|
tex_issue_addr_q <= gs_tex_rd_addr_o;
|
|
tex_issue_raw_q <= dut.u_gs.u_tex.addr;
|
|
tex_issue_u_q <= dut.u_gs.u_tex.u_eff;
|
|
tex_issue_v_q <= dut.u_gs.u_tex.v_eff;
|
|
end
|
|
if (flush_emit_w && (flush_psm_w==6'h00) && cap_fh!=0)
|
|
$fwrite(cap_fh, "%0d %0d %0d %0d %08x %0d %0d\n",
|
|
cap_ep, flush_x_w, flush_y_w, flush_z_w, flush_color32_w, cap_pu_qq, cap_pv_qq);
|
|
if (flush_emit_w && (flush_psm_w==6'h00) && tex_fh!=0)
|
|
$fwrite(tex_fh, "%0d %0d %0d %0d %08x %08x %08x %0d %0d %08x %0d %02x %08x %08x %08x\n",
|
|
cap_ep, flush_x_w, flush_y_w, flush_z_w, flush_color32_w,
|
|
tex_issue_addr_q, tex_issue_raw_q, tex_issue_u_q, tex_issue_v_q,
|
|
dut.u_gs.u_tex.tex_rd_data, dut.u_gs.u_tex.sel_lo, dut.u_gs.u_tex.clut_rd_idx,
|
|
dut.u_gs.u_tex.clut_rd_data, dut.u_gs.u_tex.near_color, dut.u_gs.s1_tex_color);
|
|
end
|
|
end
|
|
|
|
always @(posedge clk) begin
|
|
if (rst_n) begin
|
|
#1;
|
|
if (dut.u_gs.persp_outvalid && issue_fh!=0)
|
|
$fwrite(issue_fh, "%0d %0d %0d %0d %0d %0d %0d\n",
|
|
cap_ep,
|
|
dut.u_gs.g_persp_emit.u_persp_uv.uq_pipe[2],
|
|
dut.u_gs.g_persp_emit.u_persp_uv.vq_pipe[2],
|
|
dut.u_gs.g_persp_emit.u_persp_uv.w_recip,
|
|
dut.u_gs.persp_u, dut.u_gs.persp_v,
|
|
dut.u_gs.g_persp_emit.u_persp_uv.out_valid);
|
|
end
|
|
end
|
|
|
|
// texture cache + behavioral texture LPDDR (RELOADED between fills for each epoch's rebind)
|
|
logic [29:0] araddr; logic [1:0] arburst; logic [6:0] arid; logic [7:0] arlen; logic [2:0] arsize;
|
|
logic arvalid, arready; logic [255:0] rdata; logic [1:0] rresp; logic rlast, rvalid, rready;
|
|
logic fill_start, fill_done; logic [31:0] fill_beats, fill_bytes, tex_rd_errs, fill_crc_w;
|
|
gs_texture_cache #(.LPDDR_TEX_BASE(LPDDR_TEX_BASE), .TEX_VRAM_BASE(TEX_VRAM_BASE),
|
|
.TEX_BYTES(TEX_BYTES), .N_BEATS(N_BEATS)) u_cache (
|
|
.axi_clk(clk), .axi_rst_n(rst_n), .fill_start(fill_start), .fill_done(fill_done),
|
|
.fill_beats(fill_beats), .fill_bytes(fill_bytes), .rd_errs(tex_rd_errs), .fill_crc(fill_crc_w),
|
|
.araddr(araddr), .arburst(arburst), .arid(arid), .arlen(arlen), .arsize(arsize), .arvalid(arvalid), .arready(arready),
|
|
.rdata(rdata), .rresp(rresp), .rlast(rlast), .rvalid(rvalid), .rready(rready),
|
|
.sample_clk(clk), .tex_rd_en(gs_tex_rd_en_o), .tex_rd_addr(gs_tex_rd_addr_o),
|
|
.tex_rd_data(tex_cache_data), .tex_ready(tex_cache_ready)
|
|
);
|
|
logic [31:0] lpddr_mem [0:(TEX_BYTES/4)-1]; // reloaded per epoch: tex0, tex1, tex2
|
|
typedef enum logic [1:0] { S_IDLE, S_WAIT, S_DATA } sstate_t; sstate_t sst; logic [3:0] tdly; int tbeat;
|
|
always_ff @(posedge clk) begin
|
|
if (!rst_n) begin sst<=S_IDLE; arready<=0; rvalid<=0; rlast<=0; rresp<=0; rdata<=0; tdly<=0; end
|
|
else begin arready<=0; rvalid<=0; rlast<=0;
|
|
case (sst)
|
|
S_IDLE: if (arvalid) begin arready<=1; tbeat<=(araddr-LPDDR_TEX_BASE)>>5; tdly<=4'd2; sst<=S_WAIT; end
|
|
S_WAIT: if (tdly==0) sst<=S_DATA; else tdly<=tdly-1'b1;
|
|
S_DATA: if (rready) begin for (int w=0;w<8;w++) rdata[w*32 +: 32]<=lpddr_mem[tbeat*8+w];
|
|
rresp<=2'b00; rvalid<=1; rlast<=1; sst<=S_IDLE; end
|
|
endcase
|
|
end
|
|
end
|
|
|
|
// render epoch (per scene) + PSMCT32 writer + precleared LPDDR FB
|
|
logic fb_flush=0, render_inflight=0, feeder_ready_q=0; int eof_count;
|
|
wire feeder_ready_rise = feeder_ready_tb && !feeder_ready_q;
|
|
always_ff @(posedge clk or negedge rst_n) begin
|
|
if (!rst_n) begin render_inflight<=0; feeder_ready_q<=0; fb_flush<=0; eof_count<=0; end
|
|
else begin feeder_ready_q<=feeder_ready_tb; fb_flush<=1'b0;
|
|
if (feeder_go_tb && feeder_ready_tb) render_inflight<=1'b1;
|
|
if (render_inflight && feeder_ready_rise) begin fb_flush<=1'b1; render_inflight<=1'b0; eof_count<=eof_count+1; end
|
|
end
|
|
end
|
|
logic wr_arm=0; logic [255:0] fbw_wdata; logic [31:0] fbw_wstrb, fbw_awaddr; logic fbw_awvalid, fbw_wvalid;
|
|
logic [31:0] fbw_beats, fbw_ovf, fbw_bresp_err; logic fbw_idle, fbw_drained;
|
|
gs_lpddr_axi_master #(.FIFO_DEPTH(64), .PIX_BYTES(4)) u_wr (
|
|
.gs_clk(clk), .gs_rst_n(rst_n), .enable(1'b1),
|
|
.arm(wr_arm), .canary(1'b0), .fb_base(32'h0), .ctrl_commit(fb_commit),
|
|
.px_emit(flush_emit_w && (flush_psm_w==6'h00)), .px_addr(flush_addr_w), .px_pix32(flush_color32_w), .flush(fb_flush),
|
|
.axi_clk(emif_clk), .axi_rst_n(rst_n),
|
|
.awaddr(fbw_awaddr), .awlen(), .awsize(), .awburst(), .awid(), .awvalid(fbw_awvalid), .awready(1'b1),
|
|
.wdata(fbw_wdata), .wstrb(fbw_wstrb), .wlast(), .wvalid(fbw_wvalid), .wready(1'b1),
|
|
.bvalid(1'b1), .bready(), .bresp(2'b00),
|
|
.beats_written(fbw_beats), .bursts_issued(), .bresp_err_count(fbw_bresp_err),
|
|
.fifo_overflow_count(fbw_ovf), .idle(fbw_idle), .frame_drained(fbw_drained)
|
|
);
|
|
logic [7:0] fb [0:FB_BYTES-1]; logic [31:0] fb_awlat;
|
|
always_ff @(posedge emif_clk) begin
|
|
if (fbw_awvalid) fb_awlat<=fbw_awaddr;
|
|
if (fbw_wvalid) for (int i=0;i<32;i++) if (fbw_wstrb[i]) begin
|
|
int aa; aa=fb_awlat+i; if (aa>=0 && aa<FB_BYTES) fb[aa]<=fbw_wdata[i*8 +: 8]; end
|
|
end
|
|
logic [31:0] ideal [0:FB_WORDS-1];
|
|
always_ff @(posedge clk) if (rst_n && flush_emit_w && (flush_psm_w==6'h00)) ideal[flush_addr_w>>2]<=flush_color32_w;
|
|
|
|
// FRESH-DRAIN observer: count frame_drained low->high and high->low edges (emif domain).
|
|
logic fd_q; int fd_rises, fd_falls;
|
|
always_ff @(posedge emif_clk or negedge rst_n) begin
|
|
if (!rst_n) begin fd_q<=0; fd_rises<=0; fd_falls<=0; end
|
|
else begin fd_q<=fbw_drained;
|
|
if (fbw_drained && !fd_q) fd_rises<=fd_rises+1;
|
|
if (!fbw_drained && fd_q) fd_falls<=fd_falls+1;
|
|
end
|
|
end
|
|
|
|
// scanout (384x381), host-gated: enable = scan_en (video_src) AND frame_drained
|
|
logic scan_en=0; wire so_enable = scan_en & fbw_drained;
|
|
logic [11:0] px, py; logic vsync, in_win; logic [7:0] so_r, so_g, so_b;
|
|
logic so_underflow; logic [31:0] so_rd_errs; logic so_line_valid;
|
|
logic [29:0] so_araddr; logic [1:0] so_arburst; logic [6:0] so_arid; logic [7:0] so_arlen;
|
|
logic [2:0] so_arsize; logic so_arvalid, so_arready;
|
|
logic [255:0] so_rdata; logic [1:0] so_rresp; logic so_rlast, so_rvalid, so_rready;
|
|
gs_lpddr_scanout_lb #(.FB_BASE(30'd0), .STRIDE_BYTES(STRIDE), .ROW_BEATS(ROW_BEATS),
|
|
.N_ROWS(H), .PSMCT32(1'b1)) u_scan (
|
|
.axi_clk(emif_clk), .axi_rst_n(rst_n), .enable(so_enable),
|
|
.video_clk(video_clk), .frame_start(vsync), .pixel_x(px), .pixel_y(py), .in_window(in_win),
|
|
.r(so_r), .g(so_g), .b(so_b), .line_valid(so_line_valid), .underflow(so_underflow), .rd_errs(so_rd_errs),
|
|
.araddr(so_araddr), .arburst(so_arburst), .arid(so_arid), .arlen(so_arlen), .arsize(so_arsize),
|
|
.arvalid(so_arvalid), .arready(so_arready), .rdata(so_rdata), .rresp(so_rresp),
|
|
.rlast(so_rlast), .rvalid(so_rvalid), .rready(so_rready)
|
|
);
|
|
logic [7:0] rlfsr=8'h3C; always_ff @(posedge emif_clk) rlfsr<={rlfsr[6:0], rlfsr[7]^rlfsr[5]^rlfsr[4]^rlfsr[3]};
|
|
typedef enum logic [1:0] { R_IDLE, R_WAIT, R_DATA } rst_t; rst_t rst_state;
|
|
logic [3:0] rdly; logic [29:0] rd_addr_l; int read_beats;
|
|
logic [2:0] vs_e; wire vs_edge_e = vs_e[1] && !vs_e[2]; int fb_reads, fb_reads_last;
|
|
always_ff @(posedge emif_clk or negedge rst_n) begin
|
|
if (!rst_n) begin rst_state<=R_IDLE; so_arready<=0; so_rvalid<=0; so_rlast<=0; so_rresp<=0; so_rdata<=0; rdly<=0;
|
|
read_beats<=0; vs_e<=0; fb_reads<=0; fb_reads_last<=0; end
|
|
else begin so_arready<=0; so_rvalid<=0; so_rlast<=0; vs_e<={vs_e[1:0], vsync};
|
|
if (vs_edge_e) begin fb_reads_last<=fb_reads; fb_reads<=0; end
|
|
case (rst_state)
|
|
R_IDLE: if (so_arvalid) begin so_arready<=1; rd_addr_l<=so_araddr; rdly<=rlfsr[2:0]; rst_state<=R_WAIT; end
|
|
R_WAIT: if (rdly==0) rst_state<=R_DATA; else rdly<=rdly-1'b1;
|
|
R_DATA: if (so_rready) begin
|
|
for (int w=0;w<8;w++) begin int aa; aa=rd_addr_l+w*4;
|
|
so_rdata[w*32 +: 32] <= (aa+3<FB_BYTES) ? {fb[aa+3],fb[aa+2],fb[aa+1],fb[aa]} : 32'd0; end
|
|
so_rresp<=2'b00; so_rvalid<=1; so_rlast<=1; read_beats<=read_beats+1;
|
|
if (!vs_edge_e) fb_reads<=fb_reads+1; rst_state<=R_IDLE; end
|
|
endcase
|
|
end
|
|
end
|
|
logic vid_run=0; logic [11:0] rawx, rawy;
|
|
always_ff @(posedge video_clk) begin
|
|
if (!vid_run) begin rawx<=0; rawy<=0; end
|
|
else if (rawx==H_TOT-1) begin rawx<=0; rawy<=(rawy==V_TOT-1)?12'd0:rawy+1'b1; end
|
|
else rawx<=rawx+1'b1;
|
|
end
|
|
wire active = (rawx>=H_BP)&&(rawx<H_BP+H_ACT)&&(rawy>=V_BP)&&(rawy<V_BP+V_ACT);
|
|
assign px=active?(rawx-H_BP):12'd0; assign py=(rawy>=V_BP&&rawy<V_BP+V_ACT)?(rawy-V_BP):12'd0;
|
|
assign in_win=active; assign vsync=vid_run&&(rawx==0)&&(rawy==0);
|
|
logic [11:0] px_q, py_q; logic inwin_q, run_q;
|
|
always_ff @(posedge video_clk) begin px_q<=px; py_q<=py; inwin_q<=in_win; run_q<=vid_run; end
|
|
int checked; initial checked=0; logic scoring=0;
|
|
always_ff @(posedge video_clk) if (scoring && run_q) begin
|
|
logic [7:0] er,eg,eb; logic [31:0] w;
|
|
if (inwin_q) begin int aa; aa=py_q*STRIDE+px_q*4; w={fb[aa+3],fb[aa+2],fb[aa+1],fb[aa]}; er=w[7:0]; eg=w[15:8]; eb=w[23:16]; end
|
|
else begin er=0; eg=0; eb=0; end
|
|
checked++;
|
|
if (so_r!==er||so_g!==eg||so_b!==eb) begin
|
|
if (errors<12) $error("[scan] px(%0d,%0d) got(%02x,%02x,%02x) exp(%02x,%02x,%02x)", px_q,py_q, so_r,so_g,so_b, er,eg,eb); errors++; end
|
|
end
|
|
|
|
// per-epoch idx/pal (for the oracle) + composed reference (owner epoch in [26:24], multi in [28]) + per-epoch refmaps
|
|
logic [31:0] idx [0:2][0:(512*512/4)-1]; logic [31:0] pal [0:2][0:255];
|
|
logic [31:0] refmap [0:FB_WORDS-1]; logic [31:0] refmap_ep [0:2][0:FB_WORDS-1];
|
|
logic [63:0] stg_buf [0:STG_WORDS-1]; // one epoch's staging list, streamed through the write port
|
|
// temps: iverilog can't $readmemh into a 2D-array slice, so load flat then copy into the [e] plane
|
|
logic [31:0] t_idx [0:(512*512/4)-1]; logic [31:0] t_pal [0:255]; logic [31:0] t_rm [0:FB_WORDS-1];
|
|
|
|
// ---- stream one epoch's list into the feeder staging via the WRITE PORT (word0 first, full STG reset) ----
|
|
task automatic stream_list(input int k);
|
|
string fn; fn=$sformatf("../../data/top_psmct32_raster_demo/feeder_sh3_zsched%0d.mem", k);
|
|
for (int i=0;i<STG_WORDS;i++) stg_buf[i]=64'd0;
|
|
$readmemh(fn, stg_buf);
|
|
@(negedge clk);
|
|
for (int i=0;i<STG_WORDS;i++) begin
|
|
stg_we<=1'b1; stg_waddr<=i[11:0]; stg_wdata<=stg_buf[i]; @(negedge clk);
|
|
end
|
|
stg_we<=1'b0; stg_waddr<=12'd0; stg_wdata<=64'd0; @(negedge clk);
|
|
// sanity: header word0 (ntris) landed correctly in the DUT staging
|
|
if (dut.g_feeder.feeder_stg[0][31:0] !== stg_buf[0][31:0]) begin
|
|
$error("[sched] epoch %0d: staged word0=%08x exp %08x (write-port mis-slot)", k, dut.g_feeder.feeder_stg[0][31:0], stg_buf[0][31:0]); errors++; end
|
|
$display("[sched] epoch %0d: streamed %0d words via write port; word0(ntris)=%0d", k, STG_WORDS, dut.g_feeder.feeder_stg[0][31:0]);
|
|
endtask
|
|
|
|
// ---- one-scene runner: GO, wait render+drain, REQUIRE fresh frame_drained (high->low->high, or low->high on first) ----
|
|
task automatic run_scene(input int k, input int exp_records);
|
|
int r0, f0, d=0; logic fd_before;
|
|
r0=fd_rises; f0=fd_falls; fd_before=fbw_drained;
|
|
@(negedge clk); feeder_go_tb=1'b1; @(negedge clk); feeder_go_tb=1'b0;
|
|
d=0; while (feeder_ready_tb && d<4000) begin @(posedge clk); d++; end
|
|
if (feeder_ready_tb!==1'b0) begin $error("[sched] epoch %0d: feeder_ready did not drop after GO", k); errors++; end
|
|
if (fd_before) begin
|
|
d=0; while (fd_falls==f0 && d<800000) begin @(posedge emif_clk); d++; end
|
|
if (fd_falls==f0) begin $error("[sched] epoch %0d: frame_drained never fell from stale high — STALE drain", k); errors++; end
|
|
end
|
|
d=0; while (fd_rises<=r0 && d<800000) begin @(posedge emif_clk); d++; end
|
|
if (fd_rises<=r0) begin $error("[sched] epoch %0d: frame_drained never rose — scene did not drain", k); errors++; end
|
|
repeat(100) @(posedge clk);
|
|
if (feeder_records_w!==exp_records) begin $error("[sched] epoch %0d: records_emitted=%0d exp %0d", k, feeder_records_w, exp_records); errors++; end
|
|
if (fbw_ovf!==0 || fbw_bresp_err!==0) begin $error("[sched] epoch %0d: writer ovf=%0d bresp=%0d", k, fbw_ovf, fbw_bresp_err); errors++; end
|
|
$display("[sched] epoch %0d: fresh drain (falls %0d->%0d, rises %0d->%0d), records=%0d, ovf=0", k, f0, fd_falls, r0, fd_rises, feeder_records_w);
|
|
endtask
|
|
|
|
task automatic fill_cache(input int k, input logic [31:0] exp_crc);
|
|
int d=0;
|
|
@(posedge clk) fill_start<=1'b1; @(posedge clk) fill_start<=1'b0;
|
|
d=0; while (fill_done && d<1000) begin @(posedge clk); d++; end // wait it drops (busy)
|
|
d=0; while (!fill_done && d<400000) begin @(posedge clk); d++; end // then rises (done)
|
|
if (!fill_done) begin $error("[sched] fill %0d: fill_done never rose (rearm failed)", k); errors++; end
|
|
if (fill_crc_w!==exp_crc) begin $error("[sched] fill %0d: crc=%08x exp %08x", k, fill_crc_w, exp_crc); errors++; end
|
|
if (fill_beats!==N_BEATS) begin $error("[sched] fill %0d: beats=%0d exp %0d", k, fill_beats, N_BEATS); errors++; end
|
|
if (fill_bytes!==TEX_BYTES) begin $error("[sched] fill %0d: bytes=%0d exp %0d", k, fill_bytes, TEX_BYTES); errors++; end
|
|
if (tex_rd_errs!==0) begin $error("[sched] fill %0d: rd_errs=%0d", k, tex_rd_errs); errors++; end
|
|
$display("[sched] cache fill %0d: fresh done, crc=0x%08x (exp %08x), beats=%0d bytes=%0d rd_errs=0", k, fill_crc_w, exp_crc, fill_beats, fill_bytes);
|
|
endtask
|
|
|
|
// texel lookup in epoch e's palette (module-level arrays, no per-call copy)
|
|
function automatic logic [23:0] cell_e(input int e, input integer u, input integer v);
|
|
integer lin; logic [31:0] w; logic [7:0] ix;
|
|
lin=v*TW+u; w=idx[e][lin/4]; ix=w[(8*(lin%4)) +: 8]; cell_e=pal[e][ix][23:0];
|
|
endfunction
|
|
|
|
// run ONE epoch fully (rebind tex -> fresh fill+CRC -> stream list -> GO -> fresh ordered drain)
|
|
task automatic run_epoch(input int k);
|
|
string fn; fn=$sformatf("../../data/top_psmct32_raster_demo/sh3_zsched%0d_tex_lpddr.mem", k);
|
|
$readmemh(fn, lpddr_mem);
|
|
fill_cache(k, EP_CRC[k]);
|
|
stream_list(k);
|
|
run_scene(k, EP_REC[k]);
|
|
endtask
|
|
|
|
string only_m; string fdump; int only_k; // +ONLY=ALL | +ONLY=<k> ; +FBDUMP=<file>
|
|
initial begin
|
|
errors=0; feeder_go_tb=1'b0; fill_start=1'b0; wr_arm=0; scan_en=0; stg_we=0; stg_waddr=0; stg_wdata=0; cap_fh=0; tex_fh=0; issue_fh=0; cap_ep=0;
|
|
tex_issue_addr_q=0; tex_issue_raw_q=0; tex_issue_u_q=0; tex_issue_v_q=0;
|
|
if (!$value$plusargs("ONLY=%s", only_m)) only_m="ALL";
|
|
only_k = (only_m=="ALL") ? -1 : only_m.atoi();
|
|
for (int i=0;i<FB_WORDS;i++) ideal[i]=32'd0;
|
|
for (int i=0;i<FB_BYTES;i++) fb[i]=8'h00; // PRECLEAR EXACTLY ONCE
|
|
idx[0][0]='x;
|
|
for (int e=0;e<NEP;e++) begin
|
|
$readmemh($sformatf("../../data/top_psmct32_raster_demo/sh3_zsched%0d_idx.mem", e), t_idx);
|
|
for (int i=0;i<(512*512/4);i++) idx[e][i]=t_idx[i];
|
|
$readmemh($sformatf("../../data/top_psmct32_raster_demo/sh3_zsched%0d_pal.mem", e), t_pal);
|
|
for (int i=0;i<256;i++) pal[e][i]=t_pal[i];
|
|
$readmemh($sformatf("../../data/top_psmct32_raster_demo/sh3_zsched%0d_refmap.mem", e), t_rm);
|
|
for (int i=0;i<FB_WORDS;i++) refmap_ep[e][i]=t_rm[i];
|
|
end
|
|
$readmemh("../../data/top_psmct32_raster_demo/sh3_zsched_refmap.mem", refmap);
|
|
$display("[sched] ONLY=%s (only_k=%0d), N_EPOCHS=%0d", only_m, only_k, NEP);
|
|
if (idx[0][0]===32'bx) begin $display("[tb_top_psmct32_sh3_zsched] SKIP — sh3_zsched*.mem absent (run gs_make_sh3_zscheduler_fixture.py --emit)"); $finish; end
|
|
|
|
rst_n=1'b0; core_go=1'b0; repeat(6) @(posedge clk); rst_n=1'b1; repeat(8) @(posedge clk);
|
|
|
|
// boot the bootlet (uploads ALL N relocated CLUTs to their distinct CBPs)
|
|
@(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);
|
|
wait (feeder_ready_tb==1'b1); repeat(50) @(posedge clk);
|
|
wr_arm<=1'b1; repeat(40) @(posedge clk);
|
|
|
|
// ===== scheduler: iterate the epoch descriptors in dump order (or a single epoch for the isolation check) =====
|
|
cap_fh = $fopen("zsched_frags.txt","w"); // Ch357 fragment capture for external persp_z5 verification
|
|
tex_fh = $fopen("zsched_textrace.txt","w"); // sim-only texture lookup trace for perspective-color diagnosis
|
|
issue_fh = $fopen("zsched_issue.txt","w"); // sim-only perspective divide output trace
|
|
if (only_k<0) begin
|
|
for (int k=0;k<NEP;k++) begin
|
|
cap_ep = k;
|
|
run_epoch(k);
|
|
if (k<NEP-1 && scan_en!==1'b0) begin $error("[sched] scanout enabled before the last epoch"); errors++; end
|
|
end
|
|
end else begin
|
|
run_epoch(only_k);
|
|
end
|
|
|
|
// enable scanout only after the final fresh drain
|
|
scan_en<=1'b1; repeat(20) @(posedge clk);
|
|
if (only_k<0) begin
|
|
if (fd_rises<NEP) begin $error("[sched] expected %0d ordered drains: rises=%0d", NEP, fd_rises); errors++; end
|
|
if (fd_falls<NEP-1)begin $error("[sched] epochs after the first never cleared stale drains (falls=%0d)", fd_falls); errors++; end
|
|
end else if (fd_rises<1) begin $error("[sched] isolated epoch did not drain: rises=%0d", fd_rises); errors++; end
|
|
|
|
// ===== scanout: score final FB vs the composed reference (per-pixel OWNER epoch), overlap separately =====
|
|
vid_run=1;
|
|
begin int d=0; while(!vsync && d<300000) begin @(posedge video_clk); d++; end end
|
|
@(posedge video_clk); while(!vsync) @(posedge video_clk);
|
|
begin scoring=1; @(posedge video_clk); while(!vsync) @(posedge video_clk); scoring=0; repeat(60) @(posedge emif_clk);
|
|
if (fb_reads_last!==BEATS_PER_FRAME) begin $error("[sched] scanout beats/frame=%0d exp %0d", fb_reads_last, BEATS_PER_FRAME); errors++; end
|
|
end
|
|
if (so_underflow!==0) begin $error("[sched] scanout underflow"); errors++; end
|
|
if (so_rd_errs !==0) begin $error("[sched] scanout rd_errs=%0d", so_rd_errs); errors++; end
|
|
if (checked < H_ACT*V_ACT) begin $error("[sched] only %0d px checked (exp >= %0d)", checked, H_ACT*V_ACT); errors++; end
|
|
|
|
// oracle (multi-texture correctness): a covered pixel must equal ONE of the COVERING epochs' texels. Each epoch
|
|
// stores its OWN (tu,tv) per pixel in refmap_ep[k]; we accept if the RTL colour matches any covering epoch's texel
|
|
// in the <=1-texel neighbourhood (owner-first, then neighbours — handles coverage-edge owner ambiguity between
|
|
// adjacent-order epochs, exactly the tolerance Ch355 used). multi-epoch pixels (>=2 covering) scored separately as
|
|
// the accumulation composite proof. clut_bad = an RTL-written pixel (ideal!=0) whose colour is in NO epoch palette
|
|
// (palettes are pairwise-distinct, so this still proves the pixel came from a real texture+CLUT, not garbage).
|
|
begin
|
|
int m_tot,m_ok,o_tot,o_ok,cb,D,ncov; bit mt; m_tot=0;m_ok=0;o_tot=0;o_ok=0;cb=0;
|
|
for (int y=0;y<H;y++) for (int x=0;x<W;x++) begin
|
|
int o; logic [23:0] fbc; bit cov; o=y*W+x;
|
|
cov = (only_k<0) ? refmap[o][31] : refmap_ep[only_k][o][31];
|
|
if (cov) begin
|
|
fbc=ideal[o][23:0]; m_tot++; D=9; ncov=0;
|
|
for (int e=0;e<NEP;e++) begin
|
|
if (!(only_k>=0 && e!=only_k) && refmap_ep[e][o][31]) begin
|
|
int tu,tv; tu=(refmap_ep[e][o]>>9)&9'h1FF; tv=refmap_ep[e][o]&9'h1FF; ncov++;
|
|
for (int rad=0;rad<=1;rad++) for (int du=-rad;du<=rad;du++) for (int dv=-rad;dv<=rad;dv++) begin
|
|
int ch; ch=(du<0?-du:du); if((dv<0?-dv:dv)>ch) ch=(dv<0?-dv:dv);
|
|
if (ch==rad && (tu+du)>=0 && (tu+du)<TW && (tv+dv)>=0 && (tv+dv)<TH) begin
|
|
mt=(fbc===cell_e(e,tu+du,tv+dv)); if (mt && rad<D) D=rad;
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if (D<=1) m_ok++;
|
|
if (only_k<0 && refmap[o][28]) begin o_tot++; if (D<=1) o_ok++; end // multi-epoch (>=2 covering)
|
|
if (ideal[o]!==32'd0) begin bit f; f=1'b0;
|
|
for (int e=0;e<NEP;e++) for (int i=0;i<256;i++) if (pal[e][i][23:0]===fbc) f=1'b1;
|
|
if(!f) cb++; end
|
|
end
|
|
end
|
|
$display("[sched][oracle] ONLY=%s <=1texel ALL=%0d/%0d (%.1f%%) MULTI(>=2)=%0d/%0d (%.1f%%) clut_bad=%0d",
|
|
only_m, m_ok,m_tot,(m_tot>0)?100.0*m_ok/m_tot:0.0, o_ok,o_tot,(o_tot>0)?100.0*o_ok/o_tot:0.0, cb);
|
|
if (cb!==0) begin $error("[sched][oracle] ONLY=%s: %0d covered px in NO epoch palette", only_m, cb); errors++; end
|
|
if (only_k<0) begin
|
|
if (o_tot<500) begin $error("[sched][oracle] only %0d multi-epoch px — accumulation not exercised", o_tot); errors++; end
|
|
// MULTI <=1texel is TEXTURE FIDELITY (RTL 11-bit reciprocal LUT precision), NOT accumulation correctness.
|
|
// It tracks the per-epoch isolated fidelity (ONLY=k gives 93.0/93.3/95.6%); the multi-epoch subset sits at
|
|
// the same reciprocal floor (~92%). The ACCUMULATION proof is separate and EXACT: compose_sched.py shows
|
|
// the joint ALL render == the composited ONLY=k isolation dumps 100% BIT-FOR-BIT. So gate this at the
|
|
// documented reciprocal floor, not an accumulation-implying bar.
|
|
if (o_tot>0 && (100.0*o_ok/o_tot)<90.0) begin $error("[sched][oracle] MULTI <=1texel %.1f%% < 90%% reciprocal floor", 100.0*o_ok/o_tot); errors++; end
|
|
end
|
|
end
|
|
// FB dump for the composition==isolated check (per-epoch dumps composed externally -> compare vs ALL dump)
|
|
if ($value$plusargs("FBDUMP=%s", fdump)) begin
|
|
int fh; fh=$fopen(fdump,"w");
|
|
for (int y=0;y<H;y++) for (int x=0;x<W;x++) begin int aa; aa=y*STRIDE+x*4; $fwrite(fh,"%08x\n",{fb[aa+3],fb[aa+2],fb[aa+1],fb[aa]}); end
|
|
$fclose(fh); $display("[sched] dumped RTL FB (%0dx%0d) -> %s", W, H, fdump);
|
|
end
|
|
|
|
$display("[tb_top_psmct32_sh3_zsched] checked=%0d beats/frame=%0d (exp %0d) fresh_drains(rise/fall)=%0d/%0d records=%0d underflow=%0b ovf=%0d errors=%0d",
|
|
checked, fb_reads_last, BEATS_PER_FRAME, fd_rises, fd_falls, feeder_records_w, so_underflow, fbw_ovf, errors);
|
|
if (cap_fh!=0) $fclose(cap_fh);
|
|
if (tex_fh!=0) $fclose(tex_fh);
|
|
if (issue_fh!=0) $fclose(issue_fh);
|
|
if (errors==0) $display("[tb_top_psmct32_sh3_zsched] PASS"); else $display("[tb_top_psmct32_sh3_zsched] FAIL");
|
|
$finish;
|
|
end
|
|
initial begin #160000000; $error("[tb_top_psmct32_sh3_zsched] TIMEOUT"); $finish; end
|
|
endmodule : tb_top_psmct32_sh3_zsched
|