Snapshot: fog implementation + fidelity tooling baseline (pre bilinear-clamp fix)

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>
This commit is contained in:
2026-07-20 19:56:46 -04:00
parent ec82764bef
commit ba74bbd5aa
476 changed files with 696247 additions and 130119 deletions
@@ -46,6 +46,12 @@ module tb_top_psmct32_feeder_persp_demo;
.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),
+60
View File
@@ -0,0 +1,60 @@
// Integration proof for the runtime CLUT writer mux in the BRAM board top.
`timescale 1ns/1ps
module tb_top_psmct32_runtime_clut;
logic clk = 1'b0;
always #5 clk = ~clk;
logic rst_n, core_go;
logic runtime_wr_en, runtime_busy;
logic [7:0] runtime_wr_idx;
logic [31:0] runtime_wr_data;
logic [7:0] r,g,b;
logic hsync,vsync,de,core_halt,dma_done_seen,frame_seen,raster_overflow,frame_toggle,dma_done_toggle;
int errors;
top_psmct32_raster_demo_bram #(.PSMCT32_SWIZZLE(1'b0)) dut (
.clk, .rst_n, .core_go, .r, .g, .b, .hsync, .vsync, .de,
.core_halt, .dma_done_seen, .frame_seen, .raster_overflow,
.frame_toggle, .dma_done_toggle,
.joy_a_pressed_i(1'b0), .joy_b_pressed_i(1'b0),
.runtime_clut_wr_en_i(runtime_wr_en),
.runtime_clut_wr_idx_i(runtime_wr_idx),
.runtime_clut_wr_data_i(runtime_wr_data),
.runtime_clut_busy_i(runtime_busy)
);
task automatic runtime_write(input logic [7:0] idx, input logic [31:0] data);
@(posedge clk);
runtime_wr_idx <= idx;
runtime_wr_data <= data;
runtime_wr_en <= 1'b1;
@(posedge clk);
runtime_wr_en <= 1'b0;
endtask
task automatic check(input string label, input logic [31:0] got, input logic [31:0] want);
if (got !== want) begin
$error("[%s] got 0x%08x expected 0x%08x",label,got,want);
errors = errors + 1;
end
endtask
initial begin
rst_n=0; core_go=0; runtime_wr_en=0; runtime_wr_idx=0; runtime_wr_data=0; runtime_busy=0; errors=0;
repeat(4) @(posedge clk);
rst_n=1;
runtime_write(8'h00,32'h10203040);
runtime_write(8'h37,32'hA1B2C3D4);
runtime_write(8'hFF,32'h55667788);
repeat(2) @(posedge clk);
check("clut[0]",dut.u_clut.mem[0],32'h10203040);
check("clut[55]",dut.u_clut.mem[8'h37],32'hA1B2C3D4);
check("clut[255]",dut.u_clut.mem[8'hFF],32'h55667788);
runtime_busy=1;
repeat(2) @(posedge clk);
if (dut.clut_busy !== 1'b1) begin $error("runtime busy did not stall CLUT consumer"); errors=errors+1; end
if(errors) $fatal(1,"tb_top_psmct32_runtime_clut FAILED: %0d errors",errors);
$display("tb_top_psmct32_runtime_clut PASS: writer mux + busy gate");
$finish;
end
endmodule
+246
View File
@@ -0,0 +1,246 @@
// retroDE_ps2 — tb_top_psmct32_sh3_lpddr_fb (Ch353 Brick 1c — LPDDR-only direct PSMCT32 framebuffer proof)
//
// Same SH3 draw as tb_top_psmct32_sh3_real_draw_demo, but the framebuffer is NOT mirrored into BRAM:
// - FB_LPDDR_ONLY=1 suppresses the BRAM raster write (xfer/CLUT upload still lands in BRAM);
// - the exposed PSMCT32 raster stream (flush_color32_o/flush_addr_o/flush_emit_o, gated by PSMCT32 psm)
// drives gs_lpddr_axi_master#(.PIX_BYTES(4)) -> a behavioral, strobe-honouring LPDDR framebuffer;
// - an end-of-scene `flush` pulse pushes the final partial beat.
// PROOF: every emitted raster pixel (captured into an "ideal" frame) must appear byte-exact in the LPDDR FB.
// This validates the LPDDR-only direct path itself (writer fidelity), independent of render correctness.
// LOCAL/gitignored fixtures (dump-derived); skip-guard if absent.
`timescale 1ns/1ps
module tb_top_psmct32_sh3_lpddr_fb;
`ifdef SH3_FULL_FRAME
`include "sh3_full_params.vh" // 256x334 full-frame bounding box
`else
`include "sh3_real_params.vh" // 256x120 cropped (default)
`endif
localparam int FB_WORDS = FBPXW*FBH; // cropped 256x120 here; full-frame swaps the fixture (FBH=334)
localparam int FB_BYTES = FB_WORDS*4;
logic clk; logic rst_n; initial clk=1'b0; always #5 clk=~clk; // GS/design clock
logic axi_clk2; initial axi_clk2=1'b0; always #2 axi_clk2=~axi_clk2; // faster EMIF-like AXI clock (models the board's 310MHz drain; exercises the real async-FIFO CDC)
logic core_go; logic [7:0] r,g,b; logic hsync,vsync,de;
logic core_halt,dma_done_seen,frame_seen,raster_overflow,frame_toggle,dma_done_toggle;
logic feeder_go_tb, feeder_ready_tb;
// texture-cache tap + cache<->behavioral texture LPDDR (cloned from the oracle)
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 [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;
// exposed PSMCT32 raster stream (Ch323 ports) -> LPDDR FB writer
logic flush_emit_w; logic [31:0] flush_addr_w; logic [31:0] flush_color32_w; logic [5:0] flush_psm_w;
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),
.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) // Ch353 — suppress BRAM raster mirror (CLUT/xfer still write)
) 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(),
.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),
.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)
);
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)
);
// behavioral texture LPDDR (cloned from the oracle)
logic [31:0] lpddr_mem [0:(TEX_BYTES/4)-1];
typedef enum logic [1:0] { S_IDLE, S_WAIT, S_DATA } sstate_t; sstate_t sst;
logic [3:0] dly; int beat_idx;
always_ff @(posedge clk) begin
if (!rst_n) begin sst<=S_IDLE; arready<=0; rvalid<=0; rlast<=0; rresp<=2'b00; rdata<='0; dly<='0; end
else begin
arready<=0; rvalid<=0; rlast<=0;
case (sst)
S_IDLE: if (arvalid) begin arready<=1; beat_idx<=(araddr-LPDDR_TEX_BASE)>>5; dly<=4'd2; sst<=S_WAIT; end
S_WAIT: if (dly==0) sst<=S_DATA; else dly<=dly-1'b1;
S_DATA: if (rready) begin
for (int w=0; w<8; w++) rdata[w*32 +: 32] <= lpddr_mem[beat_idx*8 + w];
rresp<=2'b00; rvalid<=1; rlast<=1; sst<=S_IDLE;
end
endcase
end
end
// ---- Ch353 LPDDR FRAMEBUFFER writer (PSMCT32) + behavioral strobe-honouring LPDDR FB ----
logic fb_commit=0; always #13 fb_commit=~fb_commit; // free-run ctrl_commit so arm latches (PIX_BYTES=4)
logic fb_flush=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; logic fbw_idle, fbw_drained;
gs_lpddr_axi_master #(.FIFO_DEPTH(64), .PIX_BYTES(4)) u_fbwriter (
.gs_clk(clk), .gs_rst_n(rst_n), .enable(1'b1),
.arm(1'b1), .canary(1'b0), .fb_base(32'h0), .ctrl_commit(fb_commit),
.px_emit(flush_emit_w && (flush_psm_w == 6'h00)), // PSMCT32 raster emits only
.px_addr(flush_addr_w), .px_pix32(flush_color32_w), .flush(fb_flush),
.axi_clk(axi_clk2), .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(),
.fifo_overflow_count(fbw_ovf), .idle(fbw_idle), .frame_drained(fbw_drained)
);
// behavioral LPDDR FB: write only strobed bytes at the latched AW addr
logic [7:0] fb_lpddr [0:FB_BYTES-1];
logic [31:0] fb_aw_lat;
always_ff @(posedge axi_clk2) begin // writer's AXI domain
if (fbw_awvalid) fb_aw_lat <= fbw_awaddr;
if (fbw_wvalid) for (int i=0;i<32;i++) if (fbw_wstrb[i]) begin
int a; a = fb_aw_lat + i; if (a>=0 && a<FB_BYTES) fb_lpddr[a] <= fbw_wdata[i*8 +: 8];
end
end
// "ideal" frame: every emitted PSMCT32 pixel, captured straight off the raster stream
logic [31:0] ideal [0:FB_WORDS-1]; logic ideal_set [0:FB_WORDS-1];
always_ff @(posedge clk) begin
if (rst_n && flush_emit_w && (flush_psm_w == 6'h00)) begin
ideal [flush_addr_w>>2] <= flush_color32_w;
ideal_set[flush_addr_w>>2] <= 1'b1;
end
end
// ---- Ch353 (Codex #1) FULL-FRAME bounded ORACLE: score the rendered frame vs the reference texel map.
// Distinct from the transport proof above — this proves the UNCROPPED renderer is correct, not just that
// bytes reached LPDDR. Same bounded <=1-texel acceptance as tb_top_psmct32_sh3_real_draw_demo.
logic [31:0] idx_words [0:(512*512/4)-1];
logic [31:0] pal [0:255];
logic [31:0] refmap [0:FB_WORDS-1];
function automatic logic [7:0] sh3_idx(input integer u, input integer v);
integer lin; logic [31:0] w; lin=v*TW+u; w=idx_words[lin/4]; sh3_idx=w[(8*(lin%4)) +: 8];
endfunction
function automatic logic [23:0] exp_cell(input integer u, input integer v);
exp_cell = pal[sh3_idx(u,v)][23:0]; endfunction
int errors;
initial begin
errors=0; feeder_go_tb=1'b0;
for (int i=0;i<FB_WORDS;i++) begin ideal_set[i]=1'b0; ideal[i]=32'd0; end // model the precleared FB (Brick-3 preclear)
lpddr_mem[0]='x;
$readmemh(`FEEDER_SH3_REAL_FILE, dut.g_feeder.feeder_stg);
$readmemh("../../data/top_psmct32_raster_demo/sh3_real_tex_lpddr.mem", lpddr_mem);
$readmemh("../../data/top_psmct32_raster_demo/sh3_real_idx.mem", idx_words);
$readmemh("../../data/top_psmct32_raster_demo/sh3_real_pal.mem", pal);
`ifdef SH3_FULL_FRAME
$readmemh("../../data/top_psmct32_raster_demo/sh3_full_refmap.mem", refmap);
`else
$readmemh("../../data/top_psmct32_raster_demo/sh3_real_refmap.mem", refmap);
`endif
if (lpddr_mem[0] === 32'bx) begin
$display("[tb_top_psmct32_sh3_lpddr_fb] SKIP — sh3_real_*.mem absent (run gs_make_sh3_real_draw_fixture.py)");
$finish;
end
rst_n=1'b0; core_go=1'b0; fill_start=1'b0;
repeat(4) @(posedge clk); rst_n=1'b1; repeat(8) @(posedge clk);
// warm the texture cache
@(posedge clk) fill_start<=1'b1;
begin int gd=0; while (!fill_done && gd<200000) begin @(posedge clk); gd++; end end
if (!fill_done) begin $error("cache fill_done never asserted"); errors++; end
if (fill_crc_w!==32'hfbdeaa32) begin $error("cache fill_crc=%08x exp fbdeaa32", fill_crc_w); errors++; end
// boot the EE bootlet (uploads CLUT via BITBLT into BRAM) + render the scene
@(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 (!tex_cache_ready) begin $error("tex cache not ready before raster"); errors++; end
wait (feeder_ready_tb==1'b1); // whole scene rendered + drained
repeat(50) @(posedge clk);
// end-of-scene flush, then wait on the ORDERED drain ack. frame_drained (axi-domain) is set only when the
// EOF marker pops — i.e. AFTER the last data beat's BRESP (in-order FIFO + per-beat B). It's a stable level
// (not a transient like fbw_idle), so sampling it here is race-free. This is Codex's explicit drain ack.
@(negedge clk) fb_flush<=1'b1; @(negedge clk) fb_flush<=1'b0;
begin int d=0; while(!fbw_drained && d<400000) begin @(posedge clk); d++; end end
if (!fbw_drained) begin $error("[fbproof] frame_drained never asserted — writer not drained"); errors++; end
if (fbw_ovf!==0) begin $error("[fbproof] FIFO overflow=%0d", fbw_ovf); errors++; end
// PROOF: every covered (emitted) pixel must be byte-exact in the LPDDR FB
begin
int nset, mism; nset=0; mism=0;
for (int w=0; w<FB_WORDS; w++) if (ideal_set[w]) begin
logic [31:0] fbw;
nset++;
fbw = {fb_lpddr[w*4+3], fb_lpddr[w*4+2], fb_lpddr[w*4+1], fb_lpddr[w*4]};
if (fbw !== ideal[w]) begin
if (mism<10) $error("[fbproof] word %0d (byte %0d) LPDDR=%08x ideal=%08x", w, w*4, fbw, ideal[w]);
mism++;
end
end
if (nset==0) begin $error("[fbproof] no PSMCT32 pixels captured — psm gate (0x00) wrong?"); errors++; end
errors += mism;
$display("[fbproof] covered=%0d words, mismatches=%0d, writer beats=%0d", nset, mism, fbw_beats);
end
// ===== Ch353 (Codex #1) FULL-FRAME bounded ORACLE: rendered frame vs reference texel map, <=1 texel =====
begin
int m_total, m_ok, int_total, int_ok, cb;
m_total=0; m_ok=0; int_total=0; int_ok=0; cb=0;
for (int py=0; py<FBH; py++) for (int px=0; px<FBPXW; px++) begin
logic [31:0] rm; logic [23:0] fb; int tu, tv, D;
rm = refmap[py*FBPXW+px];
if (rm[31]) begin // covered by the draw
tu = (rm>>9)&9'h1FF; tv = rm&9'h1FF;
fb = ideal[py*FBPXW+px][23:0]; // rendered color (== LPDDR FB, proven equal above)
m_total++;
D=9;
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 && D==9 && (tu+du)>=0 && (tu+du)<TW && (tv+dv)>=0 && (tv+dv)<TH
&& fb===exp_cell(tu+du, tv+dv)) D=rad;
end
if (D<=1) m_ok++;
if (rm[30]) begin int_total++; if (D<=1) int_ok++; end // interior (seam-free band excluded)
begin bit f; f=1'b0; for (int i=0;i<256;i++) if (pal[i][23:0]===fb) f=1'b1; if (!f) cb++; end
end
end
$display("[oracle] full-frame <=1texel ALL=%0d/%0d (%.1f%%) INT=%.1f%% clut_bad=%0d",
m_ok, m_total, (m_total>0)?100.0*m_ok/m_total:0.0,
(int_total>0)?100.0*int_ok/int_total:0.0, cb);
if (cb !== 0) begin $error("[oracle] %0d covered px NOT a CLUT entry (wrong palette)", cb); errors++; end
if (int_total>0 && (100.0*int_ok/int_total) < 95.0)
begin $error("[oracle] interior <=1texel %.1f%% < 95%% (render wrong)", 100.0*int_ok/int_total); errors++; end
end
$display("[tb_top_psmct32_sh3_lpddr_fb] errors=%0d", errors);
if (errors==0) $display("[tb_top_psmct32_sh3_lpddr_fb] PASS");
else $display("[tb_top_psmct32_sh3_lpddr_fb] FAIL");
$finish;
end
initial begin #20000000; $error("[tb_top_psmct32_sh3_lpddr_fb] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_lpddr_fb
@@ -0,0 +1,352 @@
// retroDE_ps2 — tb_top_psmct32_sh3_lpddr_fb_seq (Ch353 Brick 3 — LPDDR-only FB host-start SEQUENCE proof)
//
// Proves Codex's round-5 board sequence at the module level, ahead of the owner fit:
// * FEEDER_AUTOSTART=0 -> boot setup reaches C_READY WITHOUT a boot render.
// * RENDER EPOCH: the end-of-scene flush (EOF) fires ONLY on a feeder_ready RISE that follows an accepted
// host GO (render_inflight), so the setup->ready edge can NOT enqueue a false EOF.
// * frame_drained is a HARD gate (no timeout): scanout enable is driven by it, EMIF-domain coherent.
//
// Acceptance (Codex): boot reaches ready; BEFORE GO zero raster writes / zero EOF / frame_drained=0; preclear (FB=0)
// no BRESP; texture fill verifies (crc); host sets base 0 / canary off / arms writer; GO drops then re-raises ready;
// exactly ONE EOF -> exactly ONE frame_drained; scanout reads exactly 334*32=10688 beats and matches ALL 256x334
// pixels (incl. black); underflow=0, rd/wr errors=0, FIFO overflow=0; a synchronized frame_drained reaches "bridge".
// LOCAL/gitignored fixtures (dump-derived); skip-guard if absent.
`timescale 1ns/1ps
`define SH3_FULL_FRAME 1
module tb_top_psmct32_sh3_lpddr_fb_seq;
`include "sh3_full_params.vh" // 256x334 full-frame bounding box (FBPXW,FBH,STG_WORDS,TEX_*,TW,TH,...)
localparam int W = FBPXW, H = FBH; // 256 x 334
localparam int STRIDE = W*4; // 1024 B (PSMCT32, no padding)
localparam int ROW_BEATS = STRIDE/32; // 32
localparam int FB_BYTES = STRIDE*H; // 342016
localparam int FB_WORDS = W*H;
localparam int BEATS_PER_FRAME = ROW_BEATS*H; // 10688
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;
logic clk=0; always #5 clk=~clk; // GS/design clock (feeder + writer gs-side)
logic emif_clk=0; always #2 emif_clk=~emif_clk; // EMIF (fast, independent) — writer axi-side + scanout axi-side
logic video_clk=0; always #7 video_clk=~video_clk;// video (independent of both)
logic bridge_clk=0;always #11 bridge_clk=~bridge_clk; // "HPS bridge" domain (frame_drained diagnostic sync)
logic rst_n;
logic fb_commit=0; always #13 fb_commit=~fb_commit; // free-run ctrl_commit so arm/base/canary latch
int errors; initial errors=0;
// ================= bram-top: SH3 draw via the feeder, FB_LPDDR_ONLY, AUTOSTART OFF =================
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 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;
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), // Ch353 — no boot render
.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(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(),
.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),
.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)
);
// texture cache + behavioral texture LPDDR (from Brick 1c)
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];
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 (Codex round 5) — flush only on a ready-rise that FOLLOWS an accepted GO =================
// render_inflight set when a GO is accepted (feeder_go pulse while ready); the end-of-scene EOF flush fires on the
// NEXT feeder_ready rise (render+drain done) and clears the epoch. The FEEDER_AUTOSTART=0 setup->ready edge has
// render_inflight=0, so it can NOT produce a false EOF.
logic fb_flush=0; logic 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; // accepted host GO opens the epoch
if (render_inflight && feeder_ready_rise) begin
fb_flush <= 1'b1; // exactly one EOF per render epoch
render_inflight <= 1'b0;
eof_count <= eof_count + 1;
end
end
end
// ================= Ch353 PSMCT32 writer + behavioral (precleared) LPDDR FB =================
logic wr_arm=0, wr_canary=0; logic [31:0] wr_base=32'h0; // host-controlled (Codex step 5)
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(wr_canary), .fb_base(wr_base), .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 a; a = fb_awlat + i; if (a>=0 && a<FB_BYTES) fb[a] <= fbw_wdata[i*8 +: 8];
end
end
// "ideal" render (every emitted PSMCT32 pixel) — for the bounded oracle
logic [31:0] ideal [0:FB_WORDS-1]; logic ideal_set [0:FB_WORDS-1];
always_ff @(posedge clk) if (rst_n && flush_emit_w && (flush_psm_w==6'h00)) begin
ideal[flush_addr_w>>2] <= flush_color32_w; ideal_set[flush_addr_w>>2] <= 1'b1;
end
// ================= SCANOUT reads the SAME FB back (Brick 2), gated on frame_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(fbw_drained), // HARD gate — frame_drained, no timeout
.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)
);
// FB read model — single-beat, VARIABLE AR/R latency; counts read beats per EMIF-domain frame
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 a; a=rd_addr_l+w*4;
so_rdata[w*32 +: 32] <= (a+3<FB_BYTES) ? {fb[a+3],fb[a+2],fb[a+1],fb[a]} : 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
// REAL video raster (independent video_clk, active + blanking incl. vertical back porch)
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);
// pixel compare — 1-video-cycle registered latency; compare vs the FB (incl. black background)
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 a; a=py_q*STRIDE+px_q*4; w={fb[a+3],fb[a+2],fb[a+1],fb[a]}; 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) in=%b got(%02x,%02x,%02x) exp(%02x,%02x,%02x)",
px_q,py_q,inwin_q, so_r,so_g,so_b, er,eg,eb);
errors++;
end
end
// ================= "HPS bridge" frame_drained diagnostic sync (Codex step: expose synchronized frame_drained) =================
logic [2:0] bd_sync; wire bridge_drained = bd_sync[2];
always_ff @(posedge bridge_clk or negedge rst_n) begin
if (!rst_n) bd_sync <= 3'd0; else bd_sync <= {bd_sync[1:0], fbw_drained};
end
// pre-GO raster-write monitor (raster emits before GO would be a boot-render leak)
int raster_emits_preGO; logic count_preGO=0;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) raster_emits_preGO<=0;
else if (count_preGO && flush_emit_w && (flush_psm_w==6'h00)) raster_emits_preGO<=raster_emits_preGO+1;
end
// ============================================ SEQUENCE ============================================
// full-frame bounded oracle helpers
logic [31:0] idx_words [0:(512*512/4)-1]; logic [31:0] pal [0:255]; logic [31:0] refmap [0:FB_WORDS-1];
function automatic logic [7:0] sh3_idx(input integer u, input integer v);
integer lin; logic [31:0] w; lin=v*TW+u; w=idx_words[lin/4]; sh3_idx=w[(8*(lin%4)) +: 8]; endfunction
function automatic logic [23:0] exp_cell(input integer u, input integer v); exp_cell=pal[sh3_idx(u,v)][23:0]; endfunction
initial begin
errors=0; feeder_go_tb=1'b0; fill_start=1'b0; wr_arm=0; wr_canary=0; wr_base=32'h0;
for (int i=0;i<FB_WORDS;i++) begin ideal_set[i]=1'b0; ideal[i]=32'd0; end
for (int i=0;i<FB_BYTES;i++) fb[i]=8'h00; // Codex step 2 — model the HPS FB preclear (starts black)
lpddr_mem[0]='x;
$readmemh(`FEEDER_SH3_REAL_FILE, dut.g_feeder.feeder_stg);
$readmemh("../../data/top_psmct32_raster_demo/sh3_real_tex_lpddr.mem", lpddr_mem);
$readmemh("../../data/top_psmct32_raster_demo/sh3_real_idx.mem", idx_words);
$readmemh("../../data/top_psmct32_raster_demo/sh3_real_pal.mem", pal);
$readmemh("../../data/top_psmct32_raster_demo/sh3_full_refmap.mem", refmap);
if (lpddr_mem[0] === 32'bx) begin
$display("[tb_top_psmct32_sh3_lpddr_fb_seq] SKIP — sh3_*.mem absent (run gs_make_sh3_real_draw_fixture.py --full-frame)");
$finish;
end
rst_n=1'b0; core_go=1'b0;
repeat(6) @(posedge clk); rst_n=1'b1; repeat(8) @(posedge clk);
// ---- Codex step 4: fill + verify the texture cache ----
@(posedge clk) fill_start<=1'b1;
begin int gd=0; while(!fill_done && gd<200000) begin @(posedge clk); gd++; end end
if (!fill_done) begin $error("[seq] cache fill_done never asserted"); errors++; end
if (fill_crc_w!==32'hfbdeaa32)begin $error("[seq] cache fill_crc=%08x exp fbdeaa32", fill_crc_w); errors++; end
if (tex_rd_errs!==0) begin $error("[seq] tex fill read errors=%0d", tex_rd_errs); errors++; end
// ---- Codex step 1: boot setup + CLUT upload; feeder must reach READY with NO boot render (AUTOSTART=0) ----
count_preGO<=1'b1;
@(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); // setup -> C_READY (no render)
repeat(200) @(posedge clk); // dwell: a boot render (if any) would have emitted by now
// ---- Codex acceptance: BEFORE GO — zero raster writes, zero EOF, frame_drained=0 ----
if (raster_emits_preGO!==0) begin $error("[seq] %0d raster emits BEFORE GO (boot-render leak — AUTOSTART!=0?)", raster_emits_preGO); errors++; end
if (fbw_beats!==0) begin $error("[seq] writer wrote %0d beats BEFORE GO (should be 0)", fbw_beats); errors++; end
if (eof_count!==0) begin $error("[seq] %0d EOF markers BEFORE GO (render-epoch leaked on setup ready-rise)", eof_count); errors++; end
if (fbw_drained!==1'b0) begin $error("[seq] frame_drained asserted BEFORE GO"); errors++; end
if (fbw_bresp_err!==0) begin $error("[seq] preclear/idle BRESP errors=%0d", fbw_bresp_err); errors++; end
$display("[seq] pre-GO clean: raster_emits=0 writer_beats=0 eof=0 frame_drained=0 (ready reached without boot render)");
// ---- Codex step 5: host configures base 0, canary off, arms the writer (latched via ctrl_commit) ----
wr_base<=32'h0; wr_canary<=1'b0; wr_arm<=1'b1;
repeat(40) @(posedge clk); // let a ctrl_commit edge snapshot arm/base/canary
count_preGO<=1'b0;
// ---- Codex step 6: GO — ready must DROP then RISE after rendering ----
@(negedge clk); feeder_go_tb=1'b1; @(negedge clk); feeder_go_tb=1'b0;
begin int d=0; while(feeder_ready_tb && d<2000) begin @(posedge clk); d++; end end
if (feeder_ready_tb!==1'b0) begin $error("[seq] feeder_ready did not drop after GO"); errors++; end
wait (feeder_ready_tb==1'b1); // whole scene rendered + drained -> ready rises (fires the EOF flush)
// ---- Codex step 7: exactly ONE EOF -> exactly ONE frame_drained ----
begin int d=0; while(!fbw_drained && d<400000) begin @(posedge clk); d++; end end
if (!fbw_drained) begin $error("[seq] frame_drained never asserted after GO"); errors++; end
repeat(200) @(posedge clk);
if (eof_count!==1) begin $error("[seq] EOF count=%0d exp 1 (render epoch must emit exactly one)", eof_count); errors++; end
if (fbw_ovf!==0) begin $error("[seq] writer FIFO overflow=%0d", fbw_ovf); errors++; end
if (fbw_bresp_err!==0) begin $error("[seq] writer BRESP errors=%0d", fbw_bresp_err); errors++; end
$display("[seq] post-GO: eof_count=%0d frame_drained=1 writer_beats=%0d ovf=0", eof_count, fbw_beats);
// ---- Codex step: bridge receives a synchronized frame_drained ----
begin int d=0; while(!bridge_drained && d<2000) begin @(posedge bridge_clk); d++; end end
if (!bridge_drained) begin $error("[seq] synchronized frame_drained never reached the bridge domain"); errors++; end
// ---- Codex step 8: scanout auto-enables from the ack; score exactly one frame ----
vid_run=1;
begin int d=0; while(!vsync && d<200000) begin @(posedge video_clk); d++; end end
@(posedge video_clk); while(!vsync) @(posedge video_clk); // prime one frame -> 2nd vsync
begin scoring=1;
@(posedge video_clk); while(!vsync) @(posedge video_clk); // score exactly one frame
scoring=0;
repeat(60) @(posedge emif_clk);
if (fb_reads_last!==BEATS_PER_FRAME) begin
$error("[seq] scanout read beats/frame=%0d exp %0d (%0dx%0d)", fb_reads_last, BEATS_PER_FRAME, H, ROW_BEATS); errors++;
end
end
if (so_underflow!==0) begin $error("[seq] scanout underflow asserted"); errors++; end
if (so_rd_errs !==0) begin $error("[seq] scanout rd_errs=%0d", so_rd_errs); errors++; end
if (checked < H_ACT*V_ACT) begin $error("[seq] only %0d pixels checked (exp >= %0d)", checked, H_ACT*V_ACT); errors++; end
// ---- bounded oracle: the rendered frame is a correct SH3 draw (<=1 texel) ----
begin
int m_total,m_ok,int_total,int_ok,cb; m_total=0;m_ok=0;int_total=0;int_ok=0;cb=0;
for (int y=0;y<H;y++) for (int x=0;x<W;x++) begin
logic [31:0] rm; logic [23:0] fbc; int tu,tv,D; rm=refmap[y*W+x];
if (rm[31]) begin
tu=(rm>>9)&9'h1FF; tv=rm&9'h1FF; fbc=ideal[y*W+x][23:0]; m_total++; D=9;
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 && D==9 && (tu+du)>=0 && (tu+du)<TW && (tv+dv)>=0 && (tv+dv)<TH && fbc===exp_cell(tu+du,tv+dv)) D=rad;
end
if (D<=1) m_ok++;
if (rm[30]) begin int_total++; if (D<=1) int_ok++; end
begin bit f; f=1'b0; for (int i=0;i<256;i++) if (pal[i][23:0]===fbc) f=1'b1; if (!f) cb++; end
end
end
$display("[seq][oracle] <=1texel ALL=%0d/%0d (%.1f%%) INT=%.1f%% clut_bad=%0d",
m_ok,m_total,(m_total>0)?100.0*m_ok/m_total:0.0,(int_total>0)?100.0*int_ok/int_total:0.0,cb);
if (cb!==0) begin $error("[seq][oracle] %0d covered px not a CLUT entry", cb); errors++; end
if (int_total>0 && (100.0*int_ok/int_total)<95.0) begin $error("[seq][oracle] interior <=1texel %.1f%% < 95%%", 100.0*int_ok/int_total); errors++; end
end
$display("[tb_top_psmct32_sh3_lpddr_fb_seq] checked=%0d px, read_beats/frame=%0d (exp %0d), eof=%0d underflow=%0b rd_errs=%0d ovf=%0d errors=%0d",
checked, fb_reads_last, BEATS_PER_FRAME, eof_count, so_underflow, so_rd_errs, fbw_ovf, errors);
if (errors==0) $display("[tb_top_psmct32_sh3_lpddr_fb_seq] PASS");
else $display("[tb_top_psmct32_sh3_lpddr_fb_seq] FAIL");
$finish;
end
initial begin #40000000; $error("[tb_top_psmct32_sh3_lpddr_fb_seq] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_lpddr_fb_seq
+313
View File
@@ -0,0 +1,313 @@
// retroDE_ps2 — tb_top_psmct32_sh3_multidraw (Ch354 Brick 1 — multi-draw accumulation into the LPDDR framebuffer)
//
// Composites N authentic SH3 draws (sharing one texture/CLUT) into ONE LPDDR PSMCT32 framebuffer through the SAME
// Ch353 LPDDR-FB path (FEEDER_AUTOSTART=0 + render-epoch EOF + frame_drained hard gate + line-buffer scanout), and
// proves Codex's Brick-1 acceptance:
// * records_emitted == NTRIS (all draws' triangles emitted)
// * max staging address stays < STG_WORDS (2048); FIFO/writer overflow == 0
// * ALL batches complete before ONE ordered frame_drained
// * scanout/preclear geometry = 256xFBH (338); exactly FBH*32 read beats/frame
// * bounded <=1-texel oracle vs the INDEPENDENT combined refmap, with the OVERLAP region scored separately
// (that is where multi-draw accumulation is actually proven).
// LOCAL/gitignored fixtures (dump-derived); skip-guard if absent.
`timescale 1ns/1ps
module tb_top_psmct32_sh3_multidraw;
`include "sh3_multi_params.vh" // FBPXW=256, FBH=338, STG_WORDS=2048, NTRIS=204, NDRAWS, TW/TH, ...
localparam int W = FBPXW, H = FBH;
localparam int STRIDE = W*4; // 1024 B
localparam int ROW_BEATS = STRIDE/32; // 32
localparam int FB_BYTES = STRIDE*H;
localparam int FB_WORDS = W*H;
localparam int BEATS_PER_FRAME = ROW_BEATS*H; // 338*32 = 10816
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;
logic clk=0; always #5 clk=~clk; // GS/design clock
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: multi-draw SH3 via the feeder, FB_LPDDR_ONLY, AUTOSTART off, STG_WORDS=2048 =====
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;
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), // 2048 staging, no boot render
.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(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_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),
.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)
);
// texture cache + behavioral texture LPDDR
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];
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 (same as the board) + 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]; logic ideal_set [0:FB_WORDS-1];
always_ff @(posedge clk) if (rst_n && flush_emit_w && (flush_psm_w==6'h00)) begin
ideal[flush_addr_w>>2]<=flush_color32_w; ideal_set[flush_addr_w>>2]<=1'b1;
end
// ===== scanout reads the FB back (256x338) =====
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(fbw_drained),
.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
// ===== combined INDEPENDENT oracle + overlap scoring =====
logic [31:0] idx_words [0:(512*512/4)-1]; logic [31:0] pal [0:255]; logic [31:0] refmap [0:FB_WORDS-1];
function automatic logic [7:0] sh3_idx(input integer u, input integer v);
integer lin; logic [31:0] w; lin=v*TW+u; w=idx_words[lin/4]; sh3_idx=w[(8*(lin%4)) +: 8]; endfunction
function automatic logic [23:0] exp_cell(input integer u, input integer v); exp_cell=pal[sh3_idx(u,v)][23:0]; endfunction
// diagnostic: +TAG=<tag> selects the fixture set (multi | d548 | d761 | d974 ...); +FBDUMP=<file> dumps the RTL FB.
string ftag; string fdump; int is_multi;
initial begin
errors=0; feeder_go_tb=1'b0; fill_start=1'b0; wr_arm=0;
if (!$value$plusargs("TAG=%s", ftag)) ftag="multi";
is_multi = (ftag=="multi");
for (int i=0;i<FB_WORDS;i++) begin ideal_set[i]=1'b0; ideal[i]=32'd0; end
for (int i=0;i<FB_BYTES;i++) fb[i]=8'h00; // preclear
lpddr_mem[0]='x;
$readmemh($sformatf("../../data/top_psmct32_raster_demo/feeder_sh3_%s.mem", ftag), dut.g_feeder.feeder_stg);
$readmemh($sformatf("../../data/top_psmct32_raster_demo/sh3_%s_tex_lpddr.mem", ftag), lpddr_mem);
$readmemh($sformatf("../../data/top_psmct32_raster_demo/sh3_%s_idx.mem", ftag), idx_words);
$readmemh($sformatf("../../data/top_psmct32_raster_demo/sh3_%s_pal.mem", ftag), pal);
$readmemh($sformatf("../../data/top_psmct32_raster_demo/sh3_%s_refmap.mem", ftag), refmap);
$display("[md] TAG=%s (is_multi=%0d)", ftag, is_multi);
if (lpddr_mem[0]===32'bx) begin
$display("[tb_top_psmct32_sh3_multidraw] SKIP — sh3_%s_*.mem absent (run gs_make_sh3_multidraw_fixture.py --emit)", ftag);
$finish;
end
rst_n=1'b0; core_go=1'b0; repeat(6) @(posedge clk); rst_n=1'b1; repeat(8) @(posedge clk);
@(posedge clk) fill_start<=1'b1;
begin int gd=0; while(!fill_done && gd<200000) begin @(posedge clk); gd++; end end
if (!fill_done) begin $error("[seq] cache fill_done never asserted"); errors++; end
if (fill_crc_w!==32'hfbdeaa32)begin $error("[seq] cache fill_crc=%08x exp fbdeaa32", fill_crc_w); errors++; end
@(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(200) @(posedge clk); // C_READY, no boot render
if (fbw_beats!==0) begin $error("[seq] writer wrote %0d beats BEFORE GO", fbw_beats); errors++; end
if (eof_count!==0) begin $error("[seq] %0d EOF BEFORE GO", eof_count); errors++; end
wr_arm<=1'b1; repeat(40) @(posedge clk);
@(negedge clk); feeder_go_tb=1'b1; @(negedge clk); feeder_go_tb=1'b0;
begin int d=0; while(feeder_ready_tb && d<4000) begin @(posedge clk); d++; end end
if (feeder_ready_tb!==1'b0) begin $error("[seq] ready did not drop after GO"); errors++; end
wait (feeder_ready_tb==1'b1); // ALL batches drained -> ready rises
begin int d=0; while(!fbw_drained && d<800000) begin @(posedge clk); d++; end end
if (!fbw_drained) begin $error("[seq] frame_drained never asserted"); errors++; end
repeat(200) @(posedge clk);
// ---- Codex acceptance ----
if (eof_count!==1) begin $error("[md] eof_count=%0d exp 1 (one ordered drain after ALL batches)", eof_count); errors++; end
if (is_multi && feeder_records_w!==NTRIS) begin $error("[md] records_emitted=%0d exp %0d (NTRIS)", feeder_records_w, NTRIS); errors++; end
if (fbw_ovf!==0) begin $error("[md] writer FIFO overflow=%0d", fbw_ovf); errors++; end
if (fbw_bresp_err!==0) begin $error("[md] writer BRESP errors=%0d", fbw_bresp_err); errors++; end
if (raster_overflow!==1'b0) begin $error("[md] raster_overflow asserted"); errors++; end
$display("[md] post-GO: eof=%0d records_emitted=%0d (exp %0d) writer_beats=%0d ovf=0", eof_count, feeder_records_w, NTRIS, fbw_beats);
vid_run=1;
begin int d=0; while(!vsync && d<200000) 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("[md] scanout beats/frame=%0d exp %0d (%0dx%0d)", fb_reads_last, BEATS_PER_FRAME, H, ROW_BEATS); errors++; end
end
if (so_underflow!==0) begin $error("[md] scanout underflow"); errors++; end
if (so_rd_errs !==0) begin $error("[md] scanout rd_errs=%0d", so_rd_errs); errors++; end
if (checked < H_ACT*V_ACT) begin $error("[md] only %0d px checked (exp >= %0d)", checked, H_ACT*V_ACT); errors++; end
// ---- combined oracle: ALL covered + the OVERLAP subset (refmap bit28) scored SEPARATELY ----
// Score four cohorts. The ACCEPTANCE gate is on INTERIOR (bit30 = away from triangle edges) — the SAME
// seam-free standard Ch353 used (its ALL was only ~96%, gated INTERIOR>=95%). ALL and the raw OVERLAP
// region are seam-heavy (draw boundaries = sub-pixel which-draw-wins ambiguity + the Ch352 perspective/8b
// reciprocal limit) and reported informationally. OVERLAP-INTERIOR is the real multi-draw ACCUMULATION proof:
// pixels covered by >1 draw AND away from edges must render correctly.
begin
int m_tot,m_ok,mi_tot,mi_ok,o_tot,o_ok,oi_tot,oi_ok,cb;
m_tot=0;m_ok=0;mi_tot=0;mi_ok=0;o_tot=0;o_ok=0;oi_tot=0;oi_ok=0;cb=0;
for (int y=0;y<H;y++) for (int x=0;x<W;x++) begin
logic [31:0] rm; logic [23:0] fbc; int tu,tv,D; rm=refmap[y*W+x];
if (rm[31]) begin
tu=(rm>>9)&9'h1FF; tv=rm&9'h1FF; fbc=ideal[y*W+x][23:0]; m_tot++; D=9;
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 && D==9 && (tu+du)>=0 && (tu+du)<TW && (tv+dv)>=0 && (tv+dv)<TH && fbc===exp_cell(tu+du,tv+dv)) D=rad;
end
if (D<=1) m_ok++;
if (rm[30]) begin mi_tot++; if (D<=1) mi_ok++; end // interior (seam-free)
if (rm[28]) begin o_tot++; if (D<=1) o_ok++; end // overlap (accumulation, seam-incl)
if (rm[28]&&rm[30]) begin oi_tot++; if (D<=1) oi_ok++; end // overlap AND interior
begin bit f; f=1'b0; for (int i=0;i<256;i++) if (pal[i][23:0]===fbc) f=1'b1; if (!f) cb++; end
end
end
$display("[md][oracle] <=1texel ALL=%0d/%0d (%.1f%%) INTERIOR=%0d/%0d (%.1f%%) OVERLAP=%0d/%0d (%.1f%%) OVERLAP-INT=%0d/%0d (%.1f%%) clut_bad=%0d",
m_ok,m_tot,(m_tot>0)?100.0*m_ok/m_tot:0.0, mi_ok,mi_tot,(mi_tot>0)?100.0*mi_ok/mi_tot:0.0,
o_ok,o_tot,(o_tot>0)?100.0*o_ok/o_tot:0.0, oi_ok,oi_tot,(oi_tot>0)?100.0*oi_ok/oi_tot:0.0, cb);
// ACCEPTANCE (Codex): the Brick-1 claim is MULTI-DRAW ACCUMULATION, proven bit-exact externally
// (combined RTL == RTL-paint-order composition of the isolated single renders, 0/86528 px diff, see
// compose_check + docs/ch354_audit_log.md). clut_bad and the overlap-exercised count ARE gated (a valid
// composited render). The <=1-texel fidelity above is a SEPARATE, draw-dependent metric (the existing
// perspective-sampling fidelity limitation: 89761=97.5%, 89548=95.6%, 89974=93.2% individually) and is
// REPORTED, NOT gated — it is not part of the accumulation acceptance.
if (cb!==0) begin $error("[md][oracle] %0d covered px not a CLUT entry", cb); errors++; end
if (is_multi && o_tot<1000) begin $error("[md][oracle] only %0d overlap px (<1000) — accumulation not exercised", o_tot); errors++; end
end
// diagnostic: dump the RTL framebuffer (one 32-bit PSMCT32 word per FB pixel, row-major) for Python composition
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("[md] dumped RTL FB (%0dx%0d) -> %s", W, H, fdump);
end
$display("[tb_top_psmct32_sh3_multidraw] checked=%0d px, beats/frame=%0d (exp %0d), records=%0d eof=%0d underflow=%0b ovf=%0d errors=%0d",
checked, fb_reads_last, BEATS_PER_FRAME, feeder_records_w, eof_count, so_underflow, fbw_ovf, errors);
if (errors==0) $display("[tb_top_psmct32_sh3_multidraw] PASS");
else $display("[tb_top_psmct32_sh3_multidraw] FAIL");
$finish;
end
initial begin #80000000; $error("[tb_top_psmct32_sh3_multidraw] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_multidraw
+353
View File
@@ -0,0 +1,353 @@
// retroDE_ps2 — tb_top_psmct32_sh3_multitex (Ch355 Brick 1 — MULTI-TEXTURE composition)
//
// Two authentic SH3 draws with DIFFERENT TEX0/CLUT accumulate into ONE LPDDR framebuffer via scene-level texture
// rebind + staged-list retriggering. Dump order: A=idx19562 (tbp=11264/CBP_A) THEN B=idx89761 (tbp=9216/CBP_B).
// Proves Codex's Ch355 gates:
// * TWO cache fills, each fill_done low->high, expected beats/bytes, 0 read errors, texture-specific CRC (A then B).
// * NO stale-frame_drained race: EACH scene requires an observed frame_drained high->low->high (not a lingering 1).
// * preclear EXACTLY once; scanout stays disabled until the SECOND fresh drain.
// * records_emitted per list; final FB scored vs the independent A->B reference; OVERLAP scored separately.
// * FB 320x381 (FBW=5), stride 1280, scanout 15240 beats/frame.
// LOCAL/gitignored fixtures; skip-guard if absent.
`timescale 1ns/1ps
module tb_top_psmct32_sh3_multitex;
`include "sh3_mt_params.vh" // FBPXW=320, FBH=381, CBP_A/CBP_B, NTRIS_A/B, CRC_TEX_A/B, ...
localparam int W = FBPXW, H = FBH;
localparam int STRIDE = W*4; // 1280
localparam int ROW_BEATS = STRIDE/32; // 40
localparam int FB_BYTES = STRIDE*H; // 487680
localparam int FB_WORDS = W*H;
localparam int BEATS_PER_FRAME = ROW_BEATS*H; // 15240
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;
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 = 320x381 =====
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;
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(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_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),
.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)
);
// texture cache + behavioral texture LPDDR (RELOADED between fills for the 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: tex A, then tex B
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 (Codex): 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 (320x381), 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
// A->B composed reference + per-group palette/idx (for the oracle)
logic [31:0] idxA [0:(512*512/4)-1]; logic [31:0] palA [0:255];
logic [31:0] idxB [0:(512*512/4)-1]; logic [31:0] palB [0:255]; logic [31:0] refmap [0:FB_WORDS-1];
// ---- one-scene runner: GO, wait render+drain, and REQUIRE frame_drained high->low->high (fresh, not stale) ----
task automatic run_scene(input string nm, input int exp_records);
int r0, f0, d=0; logic fd_before;
r0=fd_rises; f0=fd_falls; fd_before=fbw_drained; // stale high from a PRIOR scene?
@(negedge clk); feeder_go_tb=1'b1; @(negedge clk); feeder_go_tb=1'b0;
// ready drops (render started)
d=0; while (feeder_ready_tb && d<4000) begin @(posedge clk); d++; end
if (feeder_ready_tb!==1'b0) begin $error("[mt] %s: feeder_ready did not drop after GO", nm); errors++; end
// ANTI-STALE (Codex): if frame_drained was HIGH before GO (a prior scene's drain), require a FALL first — proves
// THIS scene's beats actually flowed and cleared the stale high (never accept a lingering 1 as this scene done).
if (fd_before) begin
d=0; while (fd_falls==f0 && d<800000) begin @(posedge emif_clk); d++; end
if (fd_falls==f0) begin $error("[mt] %s: frame_drained never fell from stale high — STALE drain (scene did not render fresh)", nm); errors++; end
end
// require a fresh RISE (this scene's ordered drain) either way -> high->low->high for B, low->high for A.
d=0; while (fd_rises<=r0 && d<800000) begin @(posedge emif_clk); d++; end
if (fd_rises<=r0) begin $error("[mt] %s: frame_drained never rose — scene did not drain", nm); errors++; end
repeat(100) @(posedge clk);
if (feeder_records_w!==exp_records) begin $error("[mt] %s: records_emitted=%0d exp %0d", nm, feeder_records_w, exp_records); errors++; end
if (fbw_ovf!==0 || fbw_bresp_err!==0) begin $error("[mt] %s: writer ovf=%0d bresp=%0d", nm, fbw_ovf, fbw_bresp_err); errors++; end
$display("[mt] scene %s: fresh drain (falls %0d->%0d, rises %0d->%0d), records=%0d, ovf=0", nm, f0, fd_falls, r0, fd_rises, feeder_records_w);
endtask
task automatic fill_cache(input string nm, input logic [31:0] exp_crc);
int d=0; logic d0;
d0=fill_done;
@(posedge clk) fill_start<=1'b1; @(posedge clk) fill_start<=1'b0;
// require a FRESH fill_done low->high (cache rearm), not a lingering high
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("[mt] fill %s: fill_done never rose (rearm failed)", nm); errors++; end
if (fill_crc_w!==exp_crc) begin $error("[mt] fill %s: crc=%08x exp %08x", nm, fill_crc_w, exp_crc); errors++; end
if (fill_beats!==N_BEATS) begin $error("[mt] fill %s: beats=%0d exp %0d", nm, fill_beats, N_BEATS); errors++; end
if (fill_bytes!==TEX_BYTES) begin $error("[mt] fill %s: bytes=%0d exp %0d", nm, fill_bytes, TEX_BYTES); errors++; end
if (tex_rd_errs!==0) begin $error("[mt] fill %s: rd_errs=%0d", nm, tex_rd_errs); errors++; end
$display("[mt] cache fill %s: fresh done, crc=0x%08x (exp %08x), beats=%0d bytes=%0d rd_errs=0", nm, fill_crc_w, exp_crc, fill_beats, fill_bytes);
endtask
// group-flag texel lookup — reads the module-level arrays (NO per-call array copy). gb=1 -> group B, else A.
function automatic logic [23:0] cellg(input bit gb, input integer u, input integer v);
integer lin; logic [31:0] w; logic [7:0] ix;
lin=v*TW+u; w = gb ? idxB[lin/4] : idxA[lin/4]; ix=w[(8*(lin%4)) +: 8];
cellg = gb ? palB[ix][23:0] : palA[ix][23:0];
endfunction
string only_m; string fdump; int mode; // +ONLY=AB|A|B ; +FBDUMP=<file> (Codex isolation/composition diagnostic)
initial begin
errors=0; feeder_go_tb=1'b0; fill_start=1'b0; wr_arm=0; scan_en=0;
if (!$value$plusargs("ONLY=%s", only_m)) only_m="AB";
mode = (only_m=="A") ? 0 : (only_m=="B") ? 1 : 2; // 0=A-only(palA) 1=B-only(palB) 2=AB(both)
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
idxA[0]='x;
$readmemh("../../data/top_psmct32_raster_demo/sh3_mtA_idx.mem", idxA);
$readmemh("../../data/top_psmct32_raster_demo/sh3_mtA_pal.mem", palA);
$readmemh("../../data/top_psmct32_raster_demo/sh3_mtB_idx.mem", idxB);
$readmemh("../../data/top_psmct32_raster_demo/sh3_mtB_pal.mem", palB);
if (mode==0) $readmemh("../../data/top_psmct32_raster_demo/sh3_mtA_refmap.mem", refmap);
else if (mode==1) $readmemh("../../data/top_psmct32_raster_demo/sh3_mtB_refmap.mem", refmap);
else $readmemh("../../data/top_psmct32_raster_demo/sh3_mt_refmap.mem", refmap);
$display("[mt] ONLY=%s (mode=%0d)", only_m, mode);
if (idxA[0]===32'bx) begin $display("[tb_top_psmct32_sh3_multitex] SKIP — sh3_mt*.mem absent (run gs_make_sh3_multitex_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 BOTH relocated CLUTs to CBP_A, CBP_B)
@(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);
// ===== SCENE A (runs in AB and A-only): texture A -> cache (fill #1), stage list A, GO, fresh drain =====
if (mode!=1) begin
$readmemh("../../data/top_psmct32_raster_demo/sh3_mtA_tex_lpddr.mem", lpddr_mem);
fill_cache("A", CRC_TEX_A);
$readmemh("../../data/top_psmct32_raster_demo/feeder_sh3_mtA.mem", dut.g_feeder.feeder_stg);
run_scene("A", NTRIS_A);
if (mode==2 && scan_en!==1'b0) begin $error("[mt] scanout enabled before scene B"); errors++; end
end
// ===== SCENE B (runs in AB and B-only): REBIND texture B -> cache (fill #2 rearm), stage list B, GO, drain =====
if (mode!=0) begin
$readmemh("../../data/top_psmct32_raster_demo/sh3_mtB_tex_lpddr.mem", lpddr_mem);
fill_cache("B", CRC_TEX_B);
$readmemh("../../data/top_psmct32_raster_demo/feeder_sh3_mtB.mem", dut.g_feeder.feeder_stg);
run_scene("B", NTRIS_B);
end
// enable scanout only after the final fresh drain
scan_en<=1'b1; repeat(20) @(posedge clk);
if (mode==2) begin
if (fd_rises<2) begin $error("[mt] expected 2 ordered drains (A,B): rises=%0d", fd_rises); errors++; end
if (fd_falls<1) begin $error("[mt] scene B never cleared the stale drain (falls=%0d) — stale-drain race", fd_falls); errors++; end
end else if (fd_rises<1) begin $error("[mt] isolated scene did not drain: rises=%0d", fd_rises); errors++; end
// ===== scanout: score final FB vs the A->B reference; 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("[mt] scanout beats/frame=%0d exp %0d", fb_reads_last, BEATS_PER_FRAME); errors++; end
end
if (so_underflow!==0) begin $error("[mt] scanout underflow"); errors++; end
if (so_rd_errs !==0) begin $error("[mt] scanout rd_errs=%0d", so_rd_errs); errors++; end
if (checked < H_ACT*V_ACT) begin $error("[mt] only %0d px checked (exp >= %0d)", checked, H_ACT*V_ACT); errors++; end
// The reference stored (tu,tv) of the WINNING group per pixel (B on overlap, else the covering group). Since
// the per-pixel owner-group is not encoded, accept the RTL colour if it matches the <=1-texel neighbourhood in
// EITHER palette A or B (multi-texture: the winning group's texel is one of the two). overlap (bit28) scored
// separately. clut_bad requires the colour to exist in palette A OR B.
begin
int m_tot,m_ok,o_tot,o_ok,cb,D; 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
logic [31:0] rm; logic [23:0] fbc; int tu,tv; bit ovl; rm=refmap[y*W+x];
if (rm[31]) begin
tu=(rm>>9)&9'h1FF; tv=rm&9'h1FF; ovl=rm[28]; fbc=ideal[y*W+x][23:0]; m_tot++; D=9;
for (int rad=0;rad<=1 && D==9;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 && D==9 && (tu+du)>=0 && (tu+du)<TW && (tv+dv)>=0 && (tv+dv)<TH) begin
mt = (mode==0) ? (fbc===cellg(1'b0,tu+du,tv+dv)) // A-only: palette A
: (mode==1) ? (fbc===cellg(1'b1,tu+du,tv+dv)) // B-only: palette B
: (fbc===cellg(1'b0,tu+du,tv+dv) || fbc===cellg(1'b1,tu+du,tv+dv));
if (mt) D=rad;
end
end
if (D<=1) m_ok++;
if (ovl) begin o_tot++; if (D<=1) o_ok++; end
// clut_bad = a pixel the RTL ACTUALLY WROTE (ideal!=0) whose colour is not in the expected palette.
// Reference-covered pixels the RTL left black at a coverage edge (ideal==0) are texel misses (already
// penalized in <=1texel), NOT wrong-CLUT — do not count them here.
if (ideal[y*W+x]!==32'd0) begin bit f; f=1'b0; for (int i=0;i<256;i++)
if ((mode!=1 && palA[i][23:0]===fbc) || (mode!=0 && palB[i][23:0]===fbc)) f=1'b1;
if(!f) cb++; end
end
end
$display("[mt][oracle] ONLY=%s <=1texel ALL=%0d/%0d (%.1f%%) OVERLAP(A&B)=%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("[mt][oracle] ONLY=%s: %0d covered px not in the expected palette", only_m, cb); errors++; end
if (mode==2) begin
if (o_tot<500) begin $error("[mt][oracle] only %0d overlap px — accumulation not exercised", o_tot); errors++; end
// OVERLAP is the composite proof (B over A). ALL reported. Per-draw A/B fidelity settled by the ONLY=A/B runs.
if (o_tot>0 && (100.0*o_ok/o_tot)<95.0) begin $error("[mt][oracle] OVERLAP composite <=1texel %.1f%% < 95%%", 100.0*o_ok/o_tot); errors++; end
end
end
// FB dump for the Codex composition check (A-only + B-only -> compose -> compare AB combined)
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("[mt] dumped RTL FB (%0dx%0d) -> %s", W, H, fdump);
end
$display("[tb_top_psmct32_sh3_multitex] 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 (errors==0) $display("[tb_top_psmct32_sh3_multitex] PASS"); else $display("[tb_top_psmct32_sh3_multitex] FAIL");
$finish;
end
initial begin #120000000; $error("[tb_top_psmct32_sh3_multitex] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_multitex
+391
View File
@@ -0,0 +1,391 @@
// retroDE_ps2 — tb_top_psmct32_sh3_sched (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_sched_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_sched;
`include "sh3_sched_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;
// 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),
.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)
);
// 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_sched%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_sched%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;
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_sched%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_sched%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_sched%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_sched_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_sched] SKIP — sh3_sched*.mem absent (run gs_make_sh3_scheduler_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) =====
if (only_k<0) begin
for (int k=0;k<NEP;k++) begin
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_sched] 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 (errors==0) $display("[tb_top_psmct32_sh3_sched] PASS"); else $display("[tb_top_psmct32_sh3_sched] FAIL");
$finish;
end
initial begin #160000000; $error("[tb_top_psmct32_sh3_sched] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_sched
+392
View File
@@ -0,0 +1,392 @@
// retroDE_ps2 — tb_top_psmct32_sh3_sched640 (Ch357 — NATIVE 640x480 LPDDR framebuffer)
//
// Ch357 = the Ch356 N-texture scheduler on a NATIVE 640x480 LPDDR framebuffer (FBW=10, stride 2560, size 0x12C000,
// 80 beats/row, 38400 beats/frame). Draws land at their AUTHENTIC screen coordinates (NO union-origin translation);
// the scheduler/flow is UNCHANGED from Ch356, only the FB geometry differs (from sh3_s640_params.vh). Default
// N_EPOCHS=3: E0=idx11671 E1=idx19562 E2=idx89761. Proves Codex's Ch357 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_sched640;
`include "sh3_s640_params.vh" // FBPXW=640, FBH=480, N_EPOCHS=3, EPk_CRC/EPk_NTRIS/EPk_CBP, TEX_*, ...
localparam int W = FBPXW, H = FBH;
localparam int STRIDE = W*4; // 2560
localparam int ROW_BEATS = STRIDE/32; // 80
localparam int FB_BYTES = STRIDE*H; // 1228800 (0x12C000)
localparam int FB_WORDS = W*H;
localparam int BEATS_PER_FRAME = ROW_BEATS*H; // 38400
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;
// 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),
.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)
);
// 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_s640%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_s640%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;
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_s640%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_s640%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_s640%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_s640_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_sched640] SKIP — sh3_s640*.mem absent (run gs_make_sh3_scheduler_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) =====
if (only_k<0) begin
for (int k=0;k<NEP;k++) begin
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_sched640] 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 (errors==0) $display("[tb_top_psmct32_sh3_sched640] PASS"); else $display("[tb_top_psmct32_sh3_sched640] FAIL");
$finish;
end
initial begin #320000000; $error("[tb_top_psmct32_sh3_sched640] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_sched640
+204
View File
@@ -0,0 +1,204 @@
// retroDE_ps2 — tb_top_psmct32_sh3_zarb (Ch357 — SHARED-LPDDR arbitration integration sim, randomized backpressure)
//
// Codex pre-fit gate 1: "Shared-LPDDR integration sim with randomized backpressure. Arbitration is part of Ch357, not
// just wiring." This routes gs_lpddr_zc_emit's THREE AXI streams through the REAL de25 arbiters onto ONE shared LPDDR:
// * color writes (c_*) -> gs_lpddr_wr_arb port s0 (FB-writer, priority)
// * Z writes (z_aw/w/b) -> gs_lpddr_wr_arb port s2 (repurposed tile-Z-flush port)
// * Z reads (z_ar/r) -> gs_lpddr_rd_arb port s0
// During render only color/Z traffic is active (scanout/texture idle, per gate 2). One behavioral LPDDR serves BOTH the
// merged write channel (m_aw/w/b) and the read channel (m_ar/r) with RANDOM backpressure on every channel. Replays the
// raster fragment trace (zsched_frags.txt) + an independent clamp16 scoreboard; asserts final color+Z == scoreboard,
// suppression, per-epoch ordered drain, disjoint ranges, no deadlock. This proves the arbitration under contention before
// wiring the de25 top. Strong-reject primary fixture, FB 256x210.
`timescale 1ns/1ps
module tb_top_psmct32_sh3_zarb;
localparam int FB_PXW=256, FB_H=210;
localparam int NPX=FB_PXW*FB_H;
localparam [31:0] COLBASE=32'h0000_0000, ZBASE=32'h0014_0000, TEXBASE=32'h0020_0000;
localparam int MEMBEATS=(TEXBASE>>5); // one array up to the texture base (0x200000/32 = 65536 beats)
logic gs_clk=0; always #5 gs_clk=~gs_clk;
logic axi_clk=0; always #2 axi_clk=~axi_clk;
logic gs_rst_n, axi_rst_n; int errors; initial errors=0;
logic enable, clear_start, clear_done, frame_drained;
logic g_valid, g_ready; logic [11:0] g_x, g_y; logic [15:0] g_zq; logic g_zmsk, g_ztest, g_scene; logic [31:0] g_color;
// zc_emit Z AXI
logic [31:0] z_araddr; logic [7:0] z_arlen; logic [2:0] z_arsize; logic [1:0] z_arburst; logic z_arvalid, z_arready;
logic [255:0] z_rdata; logic [1:0] z_rresp; logic z_rlast, z_rvalid, z_rready;
logic [31:0] z_awaddr; logic [7:0] z_awlen; logic [2:0] z_awsize; logic [1:0] z_awburst; logic z_awvalid, z_awready;
logic [255:0] z_wdata; logic [31:0] z_wstrb; logic z_wlast, z_wvalid, z_wready; logic z_bvalid, z_bready; logic [1:0] z_bresp;
// zc_emit Color AXI
logic [31:0] c_awaddr; logic [7:0] c_awlen; logic [2:0] c_awsize; logic [1:0] c_awburst; logic c_awvalid, c_awready;
logic [255:0] c_wdata; logic [31:0] c_wstrb; logic c_wlast, c_wvalid, c_wready; logic c_bvalid, c_bready; logic [1:0] c_bresp;
logic [31:0] z_beats_read, z_beats_written, c_beats_written, col_ovf, bresp_err; logic idle;
gs_lpddr_zc_emit #(.COLBASE(COLBASE), .ZBASE(ZBASE), .FB_PXW(FB_PXW), .FB_H(FB_H), .REQ_DEPTH(32), .COL_DEPTH(64)) dut (
.gs_clk(gs_clk), .gs_rst_n(gs_rst_n), .enable(enable),
.g_valid(g_valid), .g_ready(g_ready), .g_x(g_x), .g_y(g_y), .g_zq(g_zq), .g_zmsk(g_zmsk),
.g_ztest(g_ztest), .g_ztst(2'd2), .g_color(g_color), .g_be(4'hF), .g_scene(g_scene),
.axi_clk(axi_clk), .axi_rst_n(axi_rst_n), .clear_start(clear_start), .clear_done(clear_done), .frame_drained(frame_drained),
.z_araddr(z_araddr), .z_arlen(z_arlen), .z_arsize(z_arsize), .z_arburst(z_arburst), .z_arvalid(z_arvalid), .z_arready(z_arready),
.z_rdata(z_rdata), .z_rresp(z_rresp), .z_rlast(z_rlast), .z_rvalid(z_rvalid), .z_rready(z_rready),
.z_awaddr(z_awaddr), .z_awlen(z_awlen), .z_awsize(z_awsize), .z_awburst(z_awburst), .z_awvalid(z_awvalid), .z_awready(z_awready),
.z_wdata(z_wdata), .z_wstrb(z_wstrb), .z_wlast(z_wlast), .z_wvalid(z_wvalid), .z_wready(z_wready),
.z_bvalid(z_bvalid), .z_bready(z_bready), .z_bresp(z_bresp),
.c_awaddr(c_awaddr), .c_awlen(c_awlen), .c_awsize(c_awsize), .c_awburst(c_awburst), .c_awvalid(c_awvalid), .c_awready(c_awready),
.c_wdata(c_wdata), .c_wstrb(c_wstrb), .c_wlast(c_wlast), .c_wvalid(c_wvalid), .c_wready(c_wready),
.c_bvalid(c_bvalid), .c_bready(c_bready), .c_bresp(c_bresp),
.z_beats_read(z_beats_read), .z_beats_written(z_beats_written), .c_beats_written(c_beats_written),
.col_ovf(col_ovf), .bresp_err(bresp_err), .idle(idle)
);
// ---- write arbiter: s0 = color, s2 = Z, s1/s3 inert ; master -> shared LPDDR write ----
logic [29:0] m_awaddr; logic [1:0] m_awburst; logic [6:0] m_awid; logic [7:0] m_awlen; logic [2:0] m_awsize;
logic m_awvalid, m_awready; logic [255:0] m_wdata; logic [31:0] m_wstrb; logic m_wlast, m_wvalid, m_wready;
logic m_bvalid, m_bready; logic [1:0] m_bresp;
gs_lpddr_wr_arb u_wr_arb (
.clk(axi_clk), .rst_n(axi_rst_n),
.s0_awaddr(c_awaddr[29:0]), .s0_awburst(c_awburst), .s0_awid(7'd0), .s0_awlen(c_awlen), .s0_awsize(c_awsize),
.s0_awvalid(c_awvalid), .s0_awready(c_awready), .s0_wdata(c_wdata), .s0_wstrb(c_wstrb), .s0_wlast(c_wlast),
.s0_wvalid(c_wvalid), .s0_wready(c_wready), .s0_bresp(c_bresp), .s0_bvalid(c_bvalid), .s0_bready(c_bready),
.s1_awaddr(30'd0), .s1_awburst(2'b01), .s1_awid(7'd1), .s1_awlen(8'd0), .s1_awsize(3'b101),
.s1_awvalid(1'b0), .s1_awready(), .s1_wdata(256'd0), .s1_wstrb(32'd0), .s1_wlast(1'b0),
.s1_wvalid(1'b0), .s1_wready(), .s1_bresp(), .s1_bvalid(), .s1_bready(1'b0),
.s2_awaddr(z_awaddr[29:0]), .s2_awburst(z_awburst), .s2_awid(7'd2), .s2_awlen(z_awlen), .s2_awsize(z_awsize),
.s2_awvalid(z_awvalid), .s2_awready(z_awready), .s2_wdata(z_wdata), .s2_wstrb(z_wstrb), .s2_wlast(z_wlast),
.s2_wvalid(z_wvalid), .s2_wready(z_wready), .s2_bresp(z_bresp), .s2_bvalid(z_bvalid), .s2_bready(z_bready),
.s3_awaddr(30'd0), .s3_awburst(2'b01), .s3_awid(7'd3), .s3_awlen(8'd0), .s3_awsize(3'b101),
.s3_awvalid(1'b0), .s3_awready(), .s3_wdata(256'd0), .s3_wstrb(32'd0), .s3_wlast(1'b0),
.s3_wvalid(1'b0), .s3_wready(), .s3_bresp(), .s3_bvalid(), .s3_bready(1'b0),
.m_awaddr(m_awaddr), .m_awburst(m_awburst), .m_awid(m_awid), .m_awlen(m_awlen), .m_awsize(m_awsize),
.m_awvalid(m_awvalid), .m_awready(m_awready), .m_wdata(m_wdata), .m_wstrb(m_wstrb), .m_wlast(m_wlast),
.m_wvalid(m_wvalid), .m_wready(m_wready), .m_bvalid(m_bvalid), .m_bready(m_bready), .m_bresp(m_bresp)
);
// ---- read arbiter: s0 = Z read ; s1/s2/s3 inert (scanout/tex idle during render) ; master -> shared LPDDR read ----
logic [29:0] m_araddr; logic [1:0] m_arburst; logic [6:0] m_arid; logic [7:0] m_arlen; logic [2:0] m_arsize;
logic m_arvalid, m_arready; logic [255:0] m_rdata; logic [1:0] m_rresp; logic m_rlast, m_rvalid, m_rready;
gs_lpddr_rd_arb u_rd_arb (
.clk(axi_clk), .rst_n(axi_rst_n),
.s0_araddr(z_araddr[29:0]), .s0_arburst(z_arburst), .s0_arid(7'd0), .s0_arlen(z_arlen), .s0_arsize(z_arsize),
.s0_arvalid(z_arvalid), .s0_arready(z_arready), .s0_rdata(z_rdata), .s0_rresp(z_rresp), .s0_rlast(z_rlast),
.s0_rvalid(z_rvalid), .s0_rready(z_rready),
.s1_araddr(30'd0), .s1_arburst(2'b01), .s1_arid(7'd1), .s1_arlen(8'd0), .s1_arsize(3'b101),
.s1_arvalid(1'b0), .s1_arready(), .s1_rdata(), .s1_rresp(), .s1_rlast(), .s1_rvalid(), .s1_rready(1'b0),
.s2_araddr(30'd0), .s2_arburst(2'b01), .s2_arid(7'd2), .s2_arlen(8'd0), .s2_arsize(3'b101),
.s2_arvalid(1'b0), .s2_arready(), .s2_rdata(), .s2_rresp(), .s2_rlast(), .s2_rvalid(), .s2_rready(1'b0),
.s3_araddr(30'd0), .s3_arburst(2'b01), .s3_arid(7'd3), .s3_arlen(8'd0), .s3_arsize(3'b101),
.s3_arvalid(1'b0), .s3_arready(), .s3_rdata(), .s3_rresp(), .s3_rlast(), .s3_rvalid(), .s3_rready(1'b0),
.m_araddr(m_araddr), .m_arburst(m_arburst), .m_arid(m_arid), .m_arlen(m_arlen), .m_arsize(m_arsize),
.m_arvalid(m_arvalid), .m_arready(m_arready), .m_rdata(m_rdata), .m_rresp(m_rresp), .m_rlast(m_rlast),
.m_rvalid(m_rvalid), .m_rready(m_rready)
);
logic [15:0] lf=16'hF00D; always_ff @(posedge axi_clk) lf<={lf[14:0], lf[15]^lf[13]^lf[12]^lf[10]};
// ---- ONE shared behavioral LPDDR (write + read channels), address-decoded, random backpressure ----
logic [255:0] shmem [0:MEMBEATS-1];
logic [29:0] wa; logic [255:0] wd; logic [31:0] ws; logic aw_s, w_s; logic [3:0] bd; logic bp;
logic [29:0] ra; logic rp; logic [3:0] rd_dly;
always_ff @(posedge axi_clk or negedge axi_rst_n) begin
if(!axi_rst_n) begin
m_awready<=0; m_wready<=0; m_bvalid<=0; m_bresp<=0; aw_s<=0; w_s<=0; bp<=0; bd<=0;
m_arready<=0; m_rvalid<=0; m_rlast<=0; m_rresp<=0; m_rdata<=0; rp<=0; rd_dly<=0;
end else begin
// write channel
m_awready<=(!aw_s)?lf[0]:1'b0; m_wready<=(!w_s)?lf[1]:1'b0;
if(m_awvalid&&m_awready) begin wa<=m_awaddr; aw_s<=1; m_awready<=0; end
if(m_wvalid&&m_wready) begin wd<=m_wdata; ws<=m_wstrb; w_s<=1; m_wready<=0; end
if(aw_s&&w_s&&!bp&&!m_bvalid) begin
for(int b=0;b<32;b++) if(ws[b]) shmem[wa>>5][b*8+:8]<=wd[b*8+:8];
bp<=1; bd<={1'b0,lf[4:2]};
end
if(bp&&!m_bvalid) begin if(bd==0) begin m_bvalid<=1;m_bresp<=0;bp<=0;aw_s<=0;w_s<=0; end else bd<=bd-1; end
if(m_bvalid&&m_bready) m_bvalid<=0;
// read channel
m_arready<=(!rp&&!m_rvalid)?lf[8]:1'b0;
if(m_arvalid&&m_arready) begin ra<=m_araddr; rp<=1; rd_dly<={1'b0,lf[7:5]}; m_arready<=0; end
if(rp&&!m_rvalid) begin if(rd_dly==0) begin m_rdata<=shmem[ra>>5]; m_rresp<=0; m_rvalid<=1; m_rlast<=1; rp<=0; end else rd_dly<=rd_dly-1; end
if(m_rvalid&&m_rready) begin m_rvalid<=0; m_rlast<=0; end
end
end
// disjoint-range monitors on the MERGED master streams
always_ff @(posedge axi_clk) if(axi_rst_n) begin
if(m_awvalid) begin
if(!((m_awaddr<30'(COLBASE+18288*32)) || (m_awaddr>=30'(ZBASE) && m_awaddr<30'(ZBASE+13440*32))))
begin $error("[zarb] merged AW %h not in color/Z range",m_awaddr); errors++; end
end
if(m_arvalid && (m_araddr<30'(ZBASE) || m_araddr>=30'(ZBASE+13440*32))) begin $error("[zarb] merged AR %h not in Z range",m_araddr); errors++; end
end
// ---- scoreboard + trace feed (same clamp16 persistent-Z model as the ROP gate) ----
logic [15:0] sb_z [0:NPX-1]; logic [31:0] sb_col [0:NPX-1]; logic sb_seen [0:NPX-1];
function automatic logic [15:0] cl16(input logic [31:0] z); cl16=(|z[31:16])?16'hFFFF:z[15:0]; endfunction
int nfed;
task automatic feed_frag(input int ep, input int x, input int y, input int z, input logic [31:0] col);
int o; logic [15:0] zq; logic pass;
@(negedge gs_clk); g_valid<=1; g_scene<=0; g_x<=x[11:0]; g_y<=y[11:0]; g_zq<=cl16(z); g_zmsk<=0; g_ztest<=1; g_color<=col;
@(posedge gs_clk); while(!(g_valid && g_ready)) @(posedge gs_clk);
@(negedge gs_clk); g_valid<=0;
o=y*FB_PXW+x; zq=cl16(z); pass=(zq>=sb_z[o]);
if(pass) begin sb_col[o]=col; sb_seen[o]=1; sb_z[o]=zq; end
nfed++;
if(lf[7]) repeat(1+lf[1:0]) @(posedge gs_clk);
endtask
task automatic send_marker();
@(negedge gs_clk); g_valid<=1; g_scene<=1; @(posedge gs_clk);
while(!(g_valid && g_ready)) @(posedge gs_clk);
@(negedge gs_clk); g_valid<=0; g_scene<=0;
endtask
int fh, r, ep, x, y, z, cep, drains; logic [31:0] col;
initial begin
errors=0; enable=0; clear_start=0; g_valid=0; g_scene=0; g_x=0; g_y=0; g_zq=0; g_zmsk=0; g_ztest=1; g_color=0; nfed=0; drains=0;
for(int i=0;i<NPX;i++) begin sb_z[i]=16'h0000; sb_col[i]=32'd0; sb_seen[i]=1'b0; end
for(int b=0;b<MEMBEATS;b++) shmem[b]=256'd0; // host preclears color+Z (Z re-precleared by clear_start)
gs_rst_n=0; axi_rst_n=0; repeat(6) @(posedge axi_clk); gs_rst_n=1; axi_rst_n=1; repeat(4) @(posedge axi_clk);
enable=1;
@(negedge axi_clk) clear_start=1; @(negedge axi_clk) clear_start=0;
begin int g=0; while(!clear_done && g<600000) begin @(posedge axi_clk); g++; end end
if(!clear_done) begin $error("[zarb] Z preclear timeout"); errors++; end
fh=$fopen("zsched_frags.txt","r");
if(fh==0) begin $display("[tb_top_psmct32_sh3_zarb] SKIP — zsched_frags.txt absent (run make tb_top_psmct32_sh3_zsched)"); $finish; end
cep=0;
while(!$feof(fh)) begin
r=$fscanf(fh, "%d %d %d %d %h\n", ep, x, y, z, col);
if(r==5) begin
if(ep!=cep) begin
send_marker();
begin int g=0; while(!frame_drained && g<1000000) begin @(posedge axi_clk); g++; end end
if(!frame_drained) begin $error("[zarb] epoch %0d: frame_drained never rose",cep); errors++; end else drains++;
cep=ep;
end
feed_frag(ep, x, y, z, col);
end else r=$fgetc(fh);
end
$fclose(fh);
send_marker();
begin int g=0; while(!frame_drained && g<1000000) begin @(posedge axi_clk); g++; end end
if(!frame_drained) begin $error("[zarb] final: frame_drained never rose"); errors++; end else drains++;
repeat(80) @(posedge axi_clk);
// final: shared LPDDR (via the merged/arbitrated path) == scoreboard
for(int o=0;o<NPX;o++) begin
logic [15:0] zs; int zb, zl; zb=(ZBASE + o*2)>>5; zl=o&15; zs=shmem[zb][zl*16+:16];
if(zs!==sb_z[o]) begin if(errors<12)$error("[zarb] Z px%0d=%04x exp %04x",o,zs,sb_z[o]);errors++; end
end
for(int o=0;o<NPX;o++) begin
logic [31:0] cs; cs=shmem[o>>3][(o[2:0])*32+:32]; // COLBASE=0
if(sb_seen[o]) begin if(cs!==sb_col[o]) begin if(errors<12)$error("[zarb] COLOR px%0d=%08x exp %08x",o,cs,sb_col[o]);errors++; end end
else begin if(cs!==32'd0) begin if(errors<12)$error("[zarb] px%0d never-passed color=%08x (suppress)",o,cs);errors++; end end
end
if(col_ovf!==0) begin $error("[zarb] col_ovf=%0d",col_ovf);errors++; end
if(bresp_err!==0)begin $error("[zarb] bresp_err=%0d",bresp_err);errors++; end
$display("[zarb] fed=%0d drains=%0d z_wr=%0d c_wr=%0d col_ovf=%0d bresp_err=%0d errors=%0d (COL 0x%0h..0x%0h, Z 0x%0h..0x%0h)",
nfed, drains, z_beats_written, c_beats_written, col_ovf, bresp_err, errors, COLBASE, COLBASE+18288*32, ZBASE, ZBASE+13440*32);
if(errors==0) $display("[tb_top_psmct32_sh3_zarb] PASS"); else $display("[tb_top_psmct32_sh3_zarb] FAIL");
$finish;
end
initial begin #500000000; $error("[tb_top_psmct32_sh3_zarb] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_zarb
+423
View File
@@ -0,0 +1,423 @@
// retroDE_ps2 — tb_top_psmct32_sh3_zint (Ch357 — REAL-RASTER persistent-Z board integration sim)
//
// Codex's pre-fit gate: prove the persistent-Z ROP through the SAME path the de25 top wires (GS_SH3_LPDDR_FB_Z):
// REAL demo (feeder + raster, authz sh3_zsched scene) -> gs_lpddr_zc_emit -> REAL de25 arbiters
// (gs_lpddr_wr_arb s0=color / s2=Z-write ; gs_lpddr_rd_arb s3=Z-read) -> ONE shared behavioral LPDDR
// with randomized backpressure. Unlike tb_top_psmct32_sh3_zarb (which fed a handshake-throttled trace),
// the raster CANNOT honor g_ready, so this is the gate that catches request-FIFO fragment DROPS.
//
// Asserts (Codex list):
// * final LPDDR color + Z == independent clamp16 persistent-Z scoreboard over the ACTUAL emitted fragments;
// * zero fragment drops (g_valid && !g_ready) — the whole reason this sim exists;
// * exactly one ordered scene marker (sh3_fb_flush) per accepted GO; no fragment after a marker within a scene;
// * Z cache invalidated (clear_done) after preclear and before the first GO; Z persists across epochs;
// * a FRESH ordered frame_drained per epoch; scanout never referenced during render (rd_arb s0/s1/s2 idle);
// * merged AW only in color/Z ranges, merged AR only in Z range (disjoint-map monitor).
// LOCAL/gitignored fixtures; skip-guard if absent.
`timescale 1ns/1ps
module tb_top_psmct32_sh3_zint;
`include "sh3_zsched_params.vh" // FBPXW=384, FBH=381, N_EPOCHS=3, EPk_CRC/EPk_NTRIS, TEX_*, ...
localparam int W = FBPXW, H = FBH;
localparam int NPX = W*H;
localparam int NEP = N_EPOCHS;
localparam [31:0] COLBASE = 32'h0000_0000, ZBASE = 32'h0014_0000;
localparam int COL_TOP = NPX*4; // color region byte-size (disjoint below ZBASE)
localparam int Z_TOP = NPX*2; // Z region byte-size
localparam int MEMBEATS = 65536; // shared LPDDR: covers ZBASE + Z_TOP
// per-epoch expected CRC / records
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 rst_n; logic fb_commit=0; always #13 fb_commit=~fb_commit;
int errors; initial errors=0;
// clamp16 (vendored PCSX2 PSMZ16S source-Z saturation)
function automatic logic [15:0] cl16(input logic [31:0] z); cl16=(|z[31:16])?16'hFFFF:z[15:0]; endfunction
// ===== bram-top (FB_LPDDR_ONLY, AUTOSTART off), 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;
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)
);
// texture cache + behavioral texture LPDDR (reloaded per epoch)
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];
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: one ordered scene marker (sh3_fb_flush) per accepted GO =====
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
// ===== gs_lpddr_zc_emit — Z-then-color emit, fed by the REAL raster (NO handshake) =====
logic zc_enable, clear_start, clear_done, frame_drained;
wire [15:0] g_zq = cl16(flush_z_w);
wire frag_v = flush_emit_w && (flush_psm_w==6'h00);
wire g_valid = frag_v || fb_flush;
wire g_scene = fb_flush;
wire [11:0] g_x = flush_x_w, g_y = flush_y_w;
wire [31:0] g_color = flush_color32_w;
wire g_ready;
// Z AXI
logic [31:0] z_araddr; logic [7:0] z_arlen; logic [2:0] z_arsize; logic [1:0] z_arburst; logic z_arvalid, z_arready;
logic [255:0] z_rdata; logic [1:0] z_rresp; logic z_rlast, z_rvalid, z_rready;
logic [31:0] z_awaddr; logic [7:0] z_awlen; logic [2:0] z_awsize; logic [1:0] z_awburst; logic z_awvalid, z_awready;
logic [255:0] z_wdata; logic [31:0] z_wstrb; logic z_wlast, z_wvalid, z_wready; logic z_bvalid, z_bready; logic [1:0] z_bresp;
// Color AXI
logic [31:0] c_awaddr; logic [7:0] c_awlen; logic [2:0] c_awsize; logic [1:0] c_awburst; logic c_awvalid, c_awready;
logic [255:0] c_wdata; logic [31:0] c_wstrb; logic c_wlast, c_wvalid, c_wready; logic c_bvalid, c_bready; logic [1:0] c_bresp;
logic [31:0] z_beats_read, z_beats_written, c_beats_written, col_ovf, bresp_err; logic zc_idle;
gs_lpddr_zc_emit #(.COLBASE(COLBASE), .ZBASE(ZBASE), .FB_PXW(W), .FB_H(H), .REQ_DEPTH(1024), .COL_DEPTH(128)) u_zc (
.gs_clk(clk), .gs_rst_n(rst_n), .enable(zc_enable),
.g_valid(g_valid), .g_ready(g_ready), .g_x(g_x), .g_y(g_y), .g_zq(g_zq), .g_zmsk(1'b0),
.g_ztest(1'b1), .g_ztst(2'd2), .g_color(g_color), .g_be(4'hF), .g_scene(g_scene),
.axi_clk(emif_clk), .axi_rst_n(rst_n), .clear_start(clear_start), .clear_done(clear_done), .frame_drained(frame_drained),
.z_araddr(z_araddr), .z_arlen(z_arlen), .z_arsize(z_arsize), .z_arburst(z_arburst), .z_arvalid(z_arvalid), .z_arready(z_arready),
.z_rdata(z_rdata), .z_rresp(z_rresp), .z_rlast(z_rlast), .z_rvalid(z_rvalid), .z_rready(z_rready),
.z_awaddr(z_awaddr), .z_awlen(z_awlen), .z_awsize(z_awsize), .z_awburst(z_awburst), .z_awvalid(z_awvalid), .z_awready(z_awready),
.z_wdata(z_wdata), .z_wstrb(z_wstrb), .z_wlast(z_wlast), .z_wvalid(z_wvalid), .z_wready(z_wready),
.z_bvalid(z_bvalid), .z_bready(z_bready), .z_bresp(z_bresp),
.c_awaddr(c_awaddr), .c_awlen(c_awlen), .c_awsize(c_awsize), .c_awburst(c_awburst), .c_awvalid(c_awvalid), .c_awready(c_awready),
.c_wdata(c_wdata), .c_wstrb(c_wstrb), .c_wlast(c_wlast), .c_wvalid(c_wvalid), .c_wready(c_wready),
.c_bvalid(c_bvalid), .c_bready(c_bready), .c_bresp(c_bresp),
.z_beats_read(z_beats_read), .z_beats_written(z_beats_written), .c_beats_written(c_beats_written),
.col_ovf(col_ovf), .bresp_err(bresp_err), .idle(zc_idle)
);
// fragment-drop guard (design clk): the request FIFO silently drops when g_ready is low
int g_drops; int frags_fed;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin g_drops<=0; frags_fed<=0; end
else begin
if (frag_v && !g_ready) g_drops<=g_drops+1;
if (frag_v && g_ready) frags_fed<=frags_fed+1;
end
end
// clear ordering: clear_done must rise before the first GO. no psm0 fragment before clear_done.
logic saw_clear_done=0, first_go_done=0; int pre_clear_frags;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin saw_clear_done<=0; pre_clear_frags<=0; end
else begin
if (clear_done) saw_clear_done<=1;
if (frag_v && !saw_clear_done) pre_clear_frags<=pre_clear_frags+1;
end
end
// ===== REAL de25 write arbiter: s0=color, s2=Z-write ; s1/s3 inert =====
logic [29:0] m_awaddr; logic [1:0] m_awburst; logic [6:0] m_awid; logic [7:0] m_awlen; logic [2:0] m_awsize;
logic m_awvalid, m_awready; logic [255:0] m_wdata; logic [31:0] m_wstrb; logic m_wlast, m_wvalid, m_wready;
logic m_bvalid, m_bready; logic [1:0] m_bresp;
gs_lpddr_wr_arb u_wr_arb (
.clk(emif_clk), .rst_n(rst_n),
.s0_awaddr(c_awaddr[29:0]), .s0_awburst(c_awburst), .s0_awid(7'd0), .s0_awlen(c_awlen), .s0_awsize(c_awsize),
.s0_awvalid(c_awvalid), .s0_awready(c_awready), .s0_wdata(c_wdata), .s0_wstrb(c_wstrb), .s0_wlast(c_wlast),
.s0_wvalid(c_wvalid), .s0_wready(c_wready), .s0_bresp(c_bresp), .s0_bvalid(c_bvalid), .s0_bready(c_bready),
.s1_awaddr(30'd0), .s1_awburst(2'b01), .s1_awid(7'd1), .s1_awlen(8'd0), .s1_awsize(3'b101),
.s1_awvalid(1'b0), .s1_awready(), .s1_wdata(256'd0), .s1_wstrb(32'd0), .s1_wlast(1'b0),
.s1_wvalid(1'b0), .s1_wready(), .s1_bresp(), .s1_bvalid(), .s1_bready(1'b0),
.s2_awaddr(z_awaddr[29:0]), .s2_awburst(z_awburst), .s2_awid(7'd2), .s2_awlen(z_awlen), .s2_awsize(z_awsize),
.s2_awvalid(z_awvalid), .s2_awready(z_awready), .s2_wdata(z_wdata), .s2_wstrb(z_wstrb), .s2_wlast(z_wlast),
.s2_wvalid(z_wvalid), .s2_wready(z_wready), .s2_bresp(z_bresp), .s2_bvalid(z_bvalid), .s2_bready(z_bready),
.s3_awaddr(30'd0), .s3_awburst(2'b01), .s3_awid(7'd3), .s3_awlen(8'd0), .s3_awsize(3'b101),
.s3_awvalid(1'b0), .s3_awready(), .s3_wdata(256'd0), .s3_wstrb(32'd0), .s3_wlast(1'b0),
.s3_wvalid(1'b0), .s3_wready(), .s3_bresp(), .s3_bvalid(), .s3_bready(1'b0),
.m_awaddr(m_awaddr), .m_awburst(m_awburst), .m_awid(m_awid), .m_awlen(m_awlen), .m_awsize(m_awsize),
.m_awvalid(m_awvalid), .m_awready(m_awready), .m_wdata(m_wdata), .m_wstrb(m_wstrb), .m_wlast(m_wlast),
.m_wvalid(m_wvalid), .m_wready(m_wready), .m_bvalid(m_bvalid), .m_bready(m_bready), .m_bresp(m_bresp)
);
// ===== REAL de25 read arbiter: s3=Z-read (mirrors de25 reload port) ; s0/s1/s2 idle during render =====
logic [29:0] m_araddr; logic [1:0] m_arburst; logic [6:0] m_arid; logic [7:0] m_arlen; logic [2:0] m_arsize;
logic m_arvalid, m_arready; logic [255:0] m_rdata; logic [1:0] m_rresp; logic m_rlast, m_rvalid, m_rready;
gs_lpddr_rd_arb u_rd_arb (
.clk(emif_clk), .rst_n(rst_n),
.s0_araddr(30'd0), .s0_arburst(2'b01), .s0_arid(7'd0), .s0_arlen(8'd0), .s0_arsize(3'b101),
.s0_arvalid(1'b0), .s0_arready(), .s0_rdata(), .s0_rresp(), .s0_rlast(), .s0_rvalid(), .s0_rready(1'b0),
.s1_araddr(30'd0), .s1_arburst(2'b01), .s1_arid(7'd1), .s1_arlen(8'd0), .s1_arsize(3'b101),
.s1_arvalid(1'b0), .s1_arready(), .s1_rdata(), .s1_rresp(), .s1_rlast(), .s1_rvalid(), .s1_rready(1'b0),
.s2_araddr(30'd0), .s2_arburst(2'b01), .s2_arid(7'd2), .s2_arlen(8'd0), .s2_arsize(3'b101),
.s2_arvalid(1'b0), .s2_arready(), .s2_rdata(), .s2_rresp(), .s2_rlast(), .s2_rvalid(), .s2_rready(1'b0),
.s3_araddr(z_araddr[29:0]), .s3_arburst(z_arburst), .s3_arid(7'd3), .s3_arlen(z_arlen), .s3_arsize(z_arsize),
.s3_arvalid(z_arvalid), .s3_arready(z_arready), .s3_rdata(z_rdata), .s3_rresp(z_rresp), .s3_rlast(z_rlast),
.s3_rvalid(z_rvalid), .s3_rready(z_rready),
.m_araddr(m_araddr), .m_arburst(m_arburst), .m_arid(m_arid), .m_arlen(m_arlen), .m_arsize(m_arsize),
.m_arvalid(m_arvalid), .m_arready(m_arready), .m_rdata(m_rdata), .m_rresp(m_rresp), .m_rlast(m_rlast),
.m_rvalid(m_rvalid), .m_rready(m_rready)
);
logic [15:0] lf=16'hF00D; always_ff @(posedge emif_clk) lf<={lf[14:0], lf[15]^lf[13]^lf[12]^lf[10]};
// ===== ONE shared behavioral LPDDR (write + read), address-decoded, random backpressure =====
logic [255:0] shmem [0:MEMBEATS-1];
logic [29:0] wa; logic [255:0] wd; logic [31:0] ws; logic aw_s, w_s; logic [3:0] bd; logic bp;
logic [29:0] ra; logic rp; logic [3:0] rd_dly;
always_ff @(posedge emif_clk or negedge rst_n) begin
if(!rst_n) begin
m_awready<=0; m_wready<=0; m_bvalid<=0; m_bresp<=0; aw_s<=0; w_s<=0; bp<=0; bd<=0;
m_arready<=0; m_rvalid<=0; m_rlast<=0; m_rresp<=0; m_rdata<=0; rp<=0; rd_dly<=0;
end else begin
m_awready<=(!aw_s)?lf[0]:1'b0; m_wready<=(!w_s)?lf[1]:1'b0;
if(m_awvalid&&m_awready) begin wa<=m_awaddr; aw_s<=1; m_awready<=0; end
if(m_wvalid&&m_wready) begin wd<=m_wdata; ws<=m_wstrb; w_s<=1; m_wready<=0; end
if(aw_s&&w_s&&!bp&&!m_bvalid) begin
for(int bb=0;bb<32;bb++) if(ws[bb]) shmem[wa>>5][bb*8+:8]<=wd[bb*8+:8];
bp<=1; bd<={1'b0,lf[4:2]};
end
if(bp&&!m_bvalid) begin if(bd==0) begin m_bvalid<=1;m_bresp<=0;bp<=0;aw_s<=0;w_s<=0; end else bd<=bd-1; end
if(m_bvalid&&m_bready) m_bvalid<=0;
m_arready<=(!rp&&!m_rvalid)?lf[8]:1'b0;
if(m_arvalid&&m_arready) begin ra<=m_araddr; rp<=1; rd_dly<={1'b0,lf[7:5]}; m_arready<=0; end
if(rp&&!m_rvalid) begin if(rd_dly==0) begin m_rdata<=shmem[ra>>5]; m_rresp<=0; m_rvalid<=1; m_rlast<=1; rp<=0; end else rd_dly<=rd_dly-1; end
if(m_rvalid&&m_rready) begin m_rvalid<=0; m_rlast<=0; end
end
end
// disjoint-map monitors on the MERGED master streams
always_ff @(posedge emif_clk) if(rst_n) begin
if(m_awvalid) begin
if(!((m_awaddr<30'(COLBASE+COL_TOP)) || (m_awaddr>=30'(ZBASE) && m_awaddr<30'(ZBASE+Z_TOP))))
begin $error("[zint] merged AW %h not in color/Z range",m_awaddr); errors++; end
end
if(m_arvalid && (m_araddr<30'(ZBASE) || m_araddr>=30'(ZBASE+Z_TOP))) begin $error("[zint] merged AR %h not in Z range",m_araddr); errors++; end
end
// ===== clamp16 persistent-Z SCOREBOARD over the ACTUAL emitted fragments (design clk) =====
// Mirrors gs_lpddr_z_rmw: GEQUAL vs stored, Z_CLEAR=0. Persists across epochs. Gated active after clear_done.
logic [15:0] sb_z [0:NPX-1];
logic [31:0] sb_col[0:NPX-1];
logic sb_wr [0:NPX-1];
integer sb_frag, sb_pass;
// no-fragment-after-marker: a fragment must not appear in the same scene after fb_flush (before next GO)
logic scene_ended=0; int frag_after_marker;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sb_frag<=0; sb_pass<=0; scene_ended<=0; frag_after_marker<=0;
end else begin
if (feeder_go_tb && feeder_ready_tb) scene_ended<=0; // new scene opens
if (fb_flush) scene_ended<=1; // ordered marker closes the scene
if (frag_v && saw_clear_done) begin
int o; logic [15:0] zq;
if (scene_ended) frag_after_marker<=frag_after_marker+1; // fragment after this scene's marker = ordering bug
o = g_y*W + g_x; zq = cl16(flush_z_w);
sb_frag<=sb_frag+1;
if (o>=0 && o<NPX) begin
if (zq >= sb_z[o]) begin sb_z[o]<=zq; sb_col[o]<=flush_color32_w; sb_wr[o]<=1'b1; sb_pass<=sb_pass+1; end
end
end
end
end
// FRESH-DRAIN observer (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<=frame_drained;
if (frame_drained && !fd_q) fd_rises<=fd_rises+1;
if (!frame_drained && fd_q) fd_falls<=fd_falls+1;
end
end
// clear_start: mirror de25 — pulse once at the writer ARM (after preclear, before first GO)
logic wr_arm=0, arm_e1, arm_e2, arm_e3;
always_ff @(posedge emif_clk or negedge rst_n) begin
if (!rst_n) begin arm_e1<=0; arm_e2<=0; arm_e3<=0; clear_start<=0; end
else begin arm_e1<=wr_arm; arm_e2<=arm_e1; arm_e3<=arm_e2; clear_start<=(arm_e2 && !arm_e3); end
end
// ---- feeder staging stream (write port), per epoch ----
logic [63:0] stg_buf [0:STG_WORDS-1];
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);
if (dut.g_feeder.feeder_stg[0][31:0] !== stg_buf[0][31:0]) begin
$error("[zint] epoch %0d: staged word0=%08x exp %08x", k, dut.g_feeder.feeder_stg[0][31:0], stg_buf[0][31:0]); errors++; end
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
d=0; while (!fill_done && d<400000) begin @(posedge clk); d++; end
if (!fill_done) begin $error("[zint] fill %0d: fill_done never rose", k); errors++; end
if (fill_crc_w!==exp_crc) begin $error("[zint] fill %0d: crc=%08x exp %08x", k, fill_crc_w, exp_crc); errors++; end
if (tex_rd_errs!==0) begin $error("[zint] fill %0d: rd_errs=%0d", k, tex_rd_errs); errors++; end
endtask
// one-scene runner: GO, wait render+drain, REQUIRE a FRESH ordered frame_drained
task automatic run_scene(input int k, input int exp_records);
int r0, f0, d=0; logic fd_before; int gd0, fm0;
r0=fd_rises; f0=fd_falls; fd_before=frame_drained; gd0=g_drops; fm0=frag_after_marker;
@(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("[zint] epoch %0d: feeder_ready did not drop after GO", k); errors++; end
if (fd_before) begin
d=0; while (fd_falls==f0 && d<1500000) begin @(posedge emif_clk); d++; end
if (fd_falls==f0) begin $error("[zint] epoch %0d: frame_drained never fell from stale high", k); errors++; end
end
d=0; while (fd_rises<=r0 && d<1500000) begin @(posedge emif_clk); d++; end
if (fd_rises<=r0) begin $error("[zint] epoch %0d: frame_drained never rose", k); errors++; end
repeat(100) @(posedge clk);
if (feeder_records_w!==exp_records) begin $error("[zint] epoch %0d: records=%0d exp %0d", k, feeder_records_w, exp_records); errors++; end
if (col_ovf!==0 || bresp_err!==0) begin $error("[zint] epoch %0d: col_ovf=%0d bresp_err=%0d", k, col_ovf, bresp_err); errors++; end
if (g_drops!==gd0) begin $error("[zint] epoch %0d: %0d FRAGMENT DROPS (request FIFO overflow)", k, g_drops-gd0); errors++; end
if (frag_after_marker!==fm0) begin $error("[zint] epoch %0d: %0d fragment(s) after scene marker (ordering)", k, frag_after_marker-fm0); errors++; end
$display("[zint] epoch %0d: fresh drain (f %0d->%0d, r %0d->%0d) records=%0d drops=%0d fed=%0d pass=%0d",
k, f0, fd_falls, r0, fd_rises, feeder_records_w, g_drops, frags_fed, sb_pass);
endtask
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
initial begin
errors=0; feeder_go_tb=1'b0; fill_start=1'b0; wr_arm=0; zc_enable=1'b1; stg_we=0; stg_waddr=0; stg_wdata=0;
for (int i=0;i<NPX;i++) begin sb_z[i]=16'h0000; sb_col[i]=32'd0; sb_wr[i]=1'b0; end
for (int b=0;b<MEMBEATS;b++) shmem[b]=256'd0; // PRECLEAR color+Z EXACTLY ONCE
// skip-guard: probe a fixture (via $fopen — avoids the $readmemh range warning)
begin int pfh; pfh=$fopen("../../data/top_psmct32_raster_demo/sh3_zsched0_tex_lpddr.mem","r");
if (pfh==0) begin $display("[tb_top_psmct32_sh3_zint] SKIP — sh3_zsched*.mem absent (run gs_make_sh3_zscheduler_fixture.py --emit)"); $finish; end
$fclose(pfh);
end
rst_n=1'b0; core_go=1'b0; repeat(6) @(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);
wait (feeder_ready_tb==1'b1); repeat(50) @(posedge clk);
// ARM (-> clear_start) AFTER preclear, BEFORE first GO. z_rmw's clear_start writes Z_CLEAR to EVERY Z beat
// (NBEATS=NPX/16, through wr_arb s2 under random backpressure) then asserts clear_done -> it is the authoritative
// Z preclear. Wait for it (the board host likewise gates the first GO on the clear_done status bit).
wr_arm<=1'b1;
begin int d=0; while (!clear_done && d<600000) begin @(posedge emif_clk); d++; end end
if (!clear_done) begin $error("[zint] clear_done never rose after ARM (Z preclear did not finish)"); errors++; end
else $display("[zint] Z preclear complete (clear_done), z_beats_written(clear)=%0d", z_beats_written);
repeat(40) @(posedge clk);
for (int k=0;k<NEP;k++) run_epoch(k);
if (pre_clear_frags!==0) begin $error("[zint] %0d psm0 fragment(s) before clear_done", pre_clear_frags); errors++; end
if (fd_rises<NEP) begin $error("[zint] expected %0d ordered drains: rises=%0d", NEP, fd_rises); errors++; end
if (fd_falls<NEP-1) begin $error("[zint] epochs after the first never cleared stale (falls=%0d)", fd_falls); errors++; end
if (eof_count!==NEP)begin $error("[zint] scene markers=%0d != accepted GOs=%0d", eof_count, NEP); errors++; end
if (g_drops!==0) begin $error("[zint] TOTAL %0d fragment drops", g_drops); errors++; end
// ===== final memory compare: shared LPDDR color + Z == scoreboard =====
begin
int zmis, cmis, zchk, cchk; zmis=0; cmis=0; zchk=0; cchk=0;
for (int o=0;o<NPX;o++) begin
logic [15:0] zs; logic [31:0] cs; int zb, zl;
zb=(ZBASE + o*2)>>5; zl=o&15; zs=shmem[zb][zl*16+:16];
zchk++;
if (zs !== sb_z[o]) begin if (zmis<10) $error("[zint] Z px%0d got %04x exp %04x", o, zs, sb_z[o]); zmis++; end
if (sb_wr[o]) begin
cs=shmem[o>>3][(o[2:0])*32+:32]; cchk++;
if (cs[23:0] !== sb_col[o][23:0]) begin if (cmis<10) $error("[zint] COL px%0d got %06x exp %06x", o, cs[23:0], sb_col[o][23:0]); cmis++; end
end
end
$display("[zint] final compare: Z %0d/%0d mismatch, COLOR %0d/%0d mismatch (written px=%0d)", zmis, zchk, cmis, cchk, cchk);
end
begin
string fdump;
if ($value$plusargs("FBDUMP=%s", fdump)) begin
int fh;
fh = $fopen(fdump, "w");
if (fh == 0) begin
$error("[zint] could not open FBDUMP '%s'", fdump);
errors++;
end else begin
for (int o=0;o<NPX;o++) begin
logic [31:0] cs;
cs = shmem[o>>3][(o[2:0])*32+:32];
$fdisplay(fh, "%08x", cs);
end
$fclose(fh);
$display("[zint] dumped final COLOR FB (%0dx%0d) -> %s", W, H, fdump);
end
end
end
$display("[tb_top_psmct32_sh3_zint] fed=%0d pass=%0d drops=%0d markers=%0d drains(r/f)=%0d/%0d col_ovf=%0d bresp_err=%0d errors=%0d",
frags_fed, sb_pass, g_drops, eof_count, fd_rises, fd_falls, col_ovf, bresp_err, errors);
$display("[zint] map: COLOR 0x%0h..0x%0h Z 0x%0h..0x%0h", COLBASE, COLBASE+COL_TOP, ZBASE, ZBASE+Z_TOP);
if (errors==0) $display("[tb_top_psmct32_sh3_zint] PASS"); else $display("[tb_top_psmct32_sh3_zint] FAIL");
$finish;
end
initial begin #200000000; $error("[tb_top_psmct32_sh3_zint] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_zint
+423
View File
@@ -0,0 +1,423 @@
// retroDE_ps2 — tb_top_psmct32_sh3_zint640 (Ch358 — REAL-RASTER persistent-Z board integration sim at NATIVE 640x480)
//
// Codex's pre-fit gate: prove the persistent-Z ROP through the SAME path the de25 top wires (GS_SH3_LPDDR_FB_Z):
// REAL demo (feeder + raster, authz sh3_zs640 NATIVE-640x480 strong-reject scene) -> gs_lpddr_zc_emit -> REAL de25 arbiters
// (gs_lpddr_wr_arb s0=color / s2=Z-write ; gs_lpddr_rd_arb s3=Z-read) -> ONE shared behavioral LPDDR
// with randomized backpressure. Unlike tb_top_psmct32_sh3_zarb (which fed a handshake-throttled trace),
// the raster CANNOT honor g_ready, so this is the gate that catches request-FIFO fragment DROPS.
//
// Asserts (Codex list):
// * final LPDDR color + Z == independent clamp16 persistent-Z scoreboard over the ACTUAL emitted fragments;
// * zero fragment drops (g_valid && !g_ready) — the whole reason this sim exists;
// * exactly one ordered scene marker (sh3_fb_flush) per accepted GO; no fragment after a marker within a scene;
// * Z cache invalidated (clear_done) after preclear and before the first GO; Z persists across epochs;
// * a FRESH ordered frame_drained per epoch; scanout never referenced during render (rd_arb s0/s1/s2 idle);
// * merged AW only in color/Z ranges, merged AR only in Z range (disjoint-map monitor).
// LOCAL/gitignored fixtures; skip-guard if absent.
`timescale 1ns/1ps
module tb_top_psmct32_sh3_zint640;
`include "sh3_zs640_params.vh" // FBPXW=640, FBH=480, N_EPOCHS=3, EPk_CRC/EPk_NTRIS, TEX_*, ...
localparam int W = FBPXW, H = FBH;
localparam int NPX = W*H;
localparam int NEP = N_EPOCHS;
localparam [31:0] COLBASE = 32'h0000_0000, ZBASE = 32'h0014_0000;
localparam int COL_TOP = NPX*4; // color region byte-size (disjoint below ZBASE)
localparam int Z_TOP = NPX*2; // Z region byte-size
localparam int MEMBEATS = 65536; // shared LPDDR: covers ZBASE + Z_TOP
// per-epoch expected CRC / records
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 rst_n; logic fb_commit=0; always #13 fb_commit=~fb_commit;
int errors; initial errors=0;
// clamp16 (vendored PCSX2 PSMZ16S source-Z saturation)
function automatic logic [15:0] cl16(input logic [31:0] z); cl16=(|z[31:16])?16'hFFFF:z[15:0]; endfunction
// ===== bram-top (FB_LPDDR_ONLY, AUTOSTART off), 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;
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)
);
// texture cache + behavioral texture LPDDR (reloaded per epoch)
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];
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: one ordered scene marker (sh3_fb_flush) per accepted GO =====
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
// ===== gs_lpddr_zc_emit — Z-then-color emit, fed by the REAL raster (NO handshake) =====
logic zc_enable, clear_start, clear_done, frame_drained;
wire [15:0] g_zq = cl16(flush_z_w);
wire frag_v = flush_emit_w && (flush_psm_w==6'h00);
wire g_valid = frag_v || fb_flush;
wire g_scene = fb_flush;
wire [11:0] g_x = flush_x_w, g_y = flush_y_w;
wire [31:0] g_color = flush_color32_w;
wire g_ready;
// Z AXI
logic [31:0] z_araddr; logic [7:0] z_arlen; logic [2:0] z_arsize; logic [1:0] z_arburst; logic z_arvalid, z_arready;
logic [255:0] z_rdata; logic [1:0] z_rresp; logic z_rlast, z_rvalid, z_rready;
logic [31:0] z_awaddr; logic [7:0] z_awlen; logic [2:0] z_awsize; logic [1:0] z_awburst; logic z_awvalid, z_awready;
logic [255:0] z_wdata; logic [31:0] z_wstrb; logic z_wlast, z_wvalid, z_wready; logic z_bvalid, z_bready; logic [1:0] z_bresp;
// Color AXI
logic [31:0] c_awaddr; logic [7:0] c_awlen; logic [2:0] c_awsize; logic [1:0] c_awburst; logic c_awvalid, c_awready;
logic [255:0] c_wdata; logic [31:0] c_wstrb; logic c_wlast, c_wvalid, c_wready; logic c_bvalid, c_bready; logic [1:0] c_bresp;
logic [31:0] z_beats_read, z_beats_written, c_beats_written, col_ovf, bresp_err; logic zc_idle;
gs_lpddr_zc_emit #(.COLBASE(COLBASE), .ZBASE(ZBASE), .FB_PXW(W), .FB_H(H), .REQ_DEPTH(1024), .COL_DEPTH(128)) u_zc (
.gs_clk(clk), .gs_rst_n(rst_n), .enable(zc_enable),
.g_valid(g_valid), .g_ready(g_ready), .g_x(g_x), .g_y(g_y), .g_zq(g_zq), .g_zmsk(1'b0),
.g_ztest(1'b1), .g_ztst(2'd2), .g_color(g_color), .g_be(4'hF), .g_scene(g_scene),
.axi_clk(emif_clk), .axi_rst_n(rst_n), .clear_start(clear_start), .clear_done(clear_done), .frame_drained(frame_drained),
.z_araddr(z_araddr), .z_arlen(z_arlen), .z_arsize(z_arsize), .z_arburst(z_arburst), .z_arvalid(z_arvalid), .z_arready(z_arready),
.z_rdata(z_rdata), .z_rresp(z_rresp), .z_rlast(z_rlast), .z_rvalid(z_rvalid), .z_rready(z_rready),
.z_awaddr(z_awaddr), .z_awlen(z_awlen), .z_awsize(z_awsize), .z_awburst(z_awburst), .z_awvalid(z_awvalid), .z_awready(z_awready),
.z_wdata(z_wdata), .z_wstrb(z_wstrb), .z_wlast(z_wlast), .z_wvalid(z_wvalid), .z_wready(z_wready),
.z_bvalid(z_bvalid), .z_bready(z_bready), .z_bresp(z_bresp),
.c_awaddr(c_awaddr), .c_awlen(c_awlen), .c_awsize(c_awsize), .c_awburst(c_awburst), .c_awvalid(c_awvalid), .c_awready(c_awready),
.c_wdata(c_wdata), .c_wstrb(c_wstrb), .c_wlast(c_wlast), .c_wvalid(c_wvalid), .c_wready(c_wready),
.c_bvalid(c_bvalid), .c_bready(c_bready), .c_bresp(c_bresp),
.z_beats_read(z_beats_read), .z_beats_written(z_beats_written), .c_beats_written(c_beats_written),
.col_ovf(col_ovf), .bresp_err(bresp_err), .idle(zc_idle)
);
// fragment-drop guard (design clk): the request FIFO silently drops when g_ready is low
int g_drops; int frags_fed;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin g_drops<=0; frags_fed<=0; end
else begin
if (frag_v && !g_ready) g_drops<=g_drops+1;
if (frag_v && g_ready) frags_fed<=frags_fed+1;
end
end
// clear ordering: clear_done must rise before the first GO. no psm0 fragment before clear_done.
logic saw_clear_done=0, first_go_done=0; int pre_clear_frags;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin saw_clear_done<=0; pre_clear_frags<=0; end
else begin
if (clear_done) saw_clear_done<=1;
if (frag_v && !saw_clear_done) pre_clear_frags<=pre_clear_frags+1;
end
end
// ===== REAL de25 write arbiter: s0=color, s2=Z-write ; s1/s3 inert =====
logic [29:0] m_awaddr; logic [1:0] m_awburst; logic [6:0] m_awid; logic [7:0] m_awlen; logic [2:0] m_awsize;
logic m_awvalid, m_awready; logic [255:0] m_wdata; logic [31:0] m_wstrb; logic m_wlast, m_wvalid, m_wready;
logic m_bvalid, m_bready; logic [1:0] m_bresp;
gs_lpddr_wr_arb u_wr_arb (
.clk(emif_clk), .rst_n(rst_n),
.s0_awaddr(c_awaddr[29:0]), .s0_awburst(c_awburst), .s0_awid(7'd0), .s0_awlen(c_awlen), .s0_awsize(c_awsize),
.s0_awvalid(c_awvalid), .s0_awready(c_awready), .s0_wdata(c_wdata), .s0_wstrb(c_wstrb), .s0_wlast(c_wlast),
.s0_wvalid(c_wvalid), .s0_wready(c_wready), .s0_bresp(c_bresp), .s0_bvalid(c_bvalid), .s0_bready(c_bready),
.s1_awaddr(30'd0), .s1_awburst(2'b01), .s1_awid(7'd1), .s1_awlen(8'd0), .s1_awsize(3'b101),
.s1_awvalid(1'b0), .s1_awready(), .s1_wdata(256'd0), .s1_wstrb(32'd0), .s1_wlast(1'b0),
.s1_wvalid(1'b0), .s1_wready(), .s1_bresp(), .s1_bvalid(), .s1_bready(1'b0),
.s2_awaddr(z_awaddr[29:0]), .s2_awburst(z_awburst), .s2_awid(7'd2), .s2_awlen(z_awlen), .s2_awsize(z_awsize),
.s2_awvalid(z_awvalid), .s2_awready(z_awready), .s2_wdata(z_wdata), .s2_wstrb(z_wstrb), .s2_wlast(z_wlast),
.s2_wvalid(z_wvalid), .s2_wready(z_wready), .s2_bresp(z_bresp), .s2_bvalid(z_bvalid), .s2_bready(z_bready),
.s3_awaddr(30'd0), .s3_awburst(2'b01), .s3_awid(7'd3), .s3_awlen(8'd0), .s3_awsize(3'b101),
.s3_awvalid(1'b0), .s3_awready(), .s3_wdata(256'd0), .s3_wstrb(32'd0), .s3_wlast(1'b0),
.s3_wvalid(1'b0), .s3_wready(), .s3_bresp(), .s3_bvalid(), .s3_bready(1'b0),
.m_awaddr(m_awaddr), .m_awburst(m_awburst), .m_awid(m_awid), .m_awlen(m_awlen), .m_awsize(m_awsize),
.m_awvalid(m_awvalid), .m_awready(m_awready), .m_wdata(m_wdata), .m_wstrb(m_wstrb), .m_wlast(m_wlast),
.m_wvalid(m_wvalid), .m_wready(m_wready), .m_bvalid(m_bvalid), .m_bready(m_bready), .m_bresp(m_bresp)
);
// ===== REAL de25 read arbiter: s3=Z-read (mirrors de25 reload port) ; s0/s1/s2 idle during render =====
logic [29:0] m_araddr; logic [1:0] m_arburst; logic [6:0] m_arid; logic [7:0] m_arlen; logic [2:0] m_arsize;
logic m_arvalid, m_arready; logic [255:0] m_rdata; logic [1:0] m_rresp; logic m_rlast, m_rvalid, m_rready;
gs_lpddr_rd_arb u_rd_arb (
.clk(emif_clk), .rst_n(rst_n),
.s0_araddr(30'd0), .s0_arburst(2'b01), .s0_arid(7'd0), .s0_arlen(8'd0), .s0_arsize(3'b101),
.s0_arvalid(1'b0), .s0_arready(), .s0_rdata(), .s0_rresp(), .s0_rlast(), .s0_rvalid(), .s0_rready(1'b0),
.s1_araddr(30'd0), .s1_arburst(2'b01), .s1_arid(7'd1), .s1_arlen(8'd0), .s1_arsize(3'b101),
.s1_arvalid(1'b0), .s1_arready(), .s1_rdata(), .s1_rresp(), .s1_rlast(), .s1_rvalid(), .s1_rready(1'b0),
.s2_araddr(30'd0), .s2_arburst(2'b01), .s2_arid(7'd2), .s2_arlen(8'd0), .s2_arsize(3'b101),
.s2_arvalid(1'b0), .s2_arready(), .s2_rdata(), .s2_rresp(), .s2_rlast(), .s2_rvalid(), .s2_rready(1'b0),
.s3_araddr(z_araddr[29:0]), .s3_arburst(z_arburst), .s3_arid(7'd3), .s3_arlen(z_arlen), .s3_arsize(z_arsize),
.s3_arvalid(z_arvalid), .s3_arready(z_arready), .s3_rdata(z_rdata), .s3_rresp(z_rresp), .s3_rlast(z_rlast),
.s3_rvalid(z_rvalid), .s3_rready(z_rready),
.m_araddr(m_araddr), .m_arburst(m_arburst), .m_arid(m_arid), .m_arlen(m_arlen), .m_arsize(m_arsize),
.m_arvalid(m_arvalid), .m_arready(m_arready), .m_rdata(m_rdata), .m_rresp(m_rresp), .m_rlast(m_rlast),
.m_rvalid(m_rvalid), .m_rready(m_rready)
);
logic [15:0] lf=16'hF00D; always_ff @(posedge emif_clk) lf<={lf[14:0], lf[15]^lf[13]^lf[12]^lf[10]};
// ===== ONE shared behavioral LPDDR (write + read), address-decoded, random backpressure =====
logic [255:0] shmem [0:MEMBEATS-1];
logic [29:0] wa; logic [255:0] wd; logic [31:0] ws; logic aw_s, w_s; logic [3:0] bd; logic bp;
logic [29:0] ra; logic rp; logic [3:0] rd_dly;
always_ff @(posedge emif_clk or negedge rst_n) begin
if(!rst_n) begin
m_awready<=0; m_wready<=0; m_bvalid<=0; m_bresp<=0; aw_s<=0; w_s<=0; bp<=0; bd<=0;
m_arready<=0; m_rvalid<=0; m_rlast<=0; m_rresp<=0; m_rdata<=0; rp<=0; rd_dly<=0;
end else begin
m_awready<=(!aw_s)?lf[0]:1'b0; m_wready<=(!w_s)?lf[1]:1'b0;
if(m_awvalid&&m_awready) begin wa<=m_awaddr; aw_s<=1; m_awready<=0; end
if(m_wvalid&&m_wready) begin wd<=m_wdata; ws<=m_wstrb; w_s<=1; m_wready<=0; end
if(aw_s&&w_s&&!bp&&!m_bvalid) begin
for(int bb=0;bb<32;bb++) if(ws[bb]) shmem[wa>>5][bb*8+:8]<=wd[bb*8+:8];
bp<=1; bd<={1'b0,lf[4:2]};
end
if(bp&&!m_bvalid) begin if(bd==0) begin m_bvalid<=1;m_bresp<=0;bp<=0;aw_s<=0;w_s<=0; end else bd<=bd-1; end
if(m_bvalid&&m_bready) m_bvalid<=0;
m_arready<=(!rp&&!m_rvalid)?lf[8]:1'b0;
if(m_arvalid&&m_arready) begin ra<=m_araddr; rp<=1; rd_dly<={1'b0,lf[7:5]}; m_arready<=0; end
if(rp&&!m_rvalid) begin if(rd_dly==0) begin m_rdata<=shmem[ra>>5]; m_rresp<=0; m_rvalid<=1; m_rlast<=1; rp<=0; end else rd_dly<=rd_dly-1; end
if(m_rvalid&&m_rready) begin m_rvalid<=0; m_rlast<=0; end
end
end
// disjoint-map monitors on the MERGED master streams
always_ff @(posedge emif_clk) if(rst_n) begin
if(m_awvalid) begin
if(!((m_awaddr<30'(COLBASE+COL_TOP)) || (m_awaddr>=30'(ZBASE) && m_awaddr<30'(ZBASE+Z_TOP))))
begin $error("[zint] merged AW %h not in color/Z range",m_awaddr); errors++; end
end
if(m_arvalid && (m_araddr<30'(ZBASE) || m_araddr>=30'(ZBASE+Z_TOP))) begin $error("[zint] merged AR %h not in Z range",m_araddr); errors++; end
end
// ===== clamp16 persistent-Z SCOREBOARD over the ACTUAL emitted fragments (design clk) =====
// Mirrors gs_lpddr_z_rmw: GEQUAL vs stored, Z_CLEAR=0. Persists across epochs. Gated active after clear_done.
logic [15:0] sb_z [0:NPX-1];
logic [31:0] sb_col[0:NPX-1];
logic sb_wr [0:NPX-1];
integer sb_frag, sb_pass;
// no-fragment-after-marker: a fragment must not appear in the same scene after fb_flush (before next GO)
logic scene_ended=0; int frag_after_marker;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sb_frag<=0; sb_pass<=0; scene_ended<=0; frag_after_marker<=0;
end else begin
if (feeder_go_tb && feeder_ready_tb) scene_ended<=0; // new scene opens
if (fb_flush) scene_ended<=1; // ordered marker closes the scene
if (frag_v && saw_clear_done) begin
int o; logic [15:0] zq;
if (scene_ended) frag_after_marker<=frag_after_marker+1; // fragment after this scene's marker = ordering bug
o = g_y*W + g_x; zq = cl16(flush_z_w);
sb_frag<=sb_frag+1;
if (o>=0 && o<NPX) begin
if (zq >= sb_z[o]) begin sb_z[o]<=zq; sb_col[o]<=flush_color32_w; sb_wr[o]<=1'b1; sb_pass<=sb_pass+1; end
end
end
end
end
// FRESH-DRAIN observer (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<=frame_drained;
if (frame_drained && !fd_q) fd_rises<=fd_rises+1;
if (!frame_drained && fd_q) fd_falls<=fd_falls+1;
end
end
// clear_start: mirror de25 — pulse once at the writer ARM (after preclear, before first GO)
logic wr_arm=0, arm_e1, arm_e2, arm_e3;
always_ff @(posedge emif_clk or negedge rst_n) begin
if (!rst_n) begin arm_e1<=0; arm_e2<=0; arm_e3<=0; clear_start<=0; end
else begin arm_e1<=wr_arm; arm_e2<=arm_e1; arm_e3<=arm_e2; clear_start<=(arm_e2 && !arm_e3); end
end
// ---- feeder staging stream (write port), per epoch ----
logic [63:0] stg_buf [0:STG_WORDS-1];
task automatic stream_list(input int k);
string fn; fn=$sformatf("../../data/top_psmct32_raster_demo/feeder_sh3_zs640%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);
if (dut.g_feeder.feeder_stg[0][31:0] !== stg_buf[0][31:0]) begin
$error("[zint] epoch %0d: staged word0=%08x exp %08x", k, dut.g_feeder.feeder_stg[0][31:0], stg_buf[0][31:0]); errors++; end
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
d=0; while (!fill_done && d<400000) begin @(posedge clk); d++; end
if (!fill_done) begin $error("[zint] fill %0d: fill_done never rose", k); errors++; end
if (fill_crc_w!==exp_crc) begin $error("[zint] fill %0d: crc=%08x exp %08x", k, fill_crc_w, exp_crc); errors++; end
if (tex_rd_errs!==0) begin $error("[zint] fill %0d: rd_errs=%0d", k, tex_rd_errs); errors++; end
endtask
// one-scene runner: GO, wait render+drain, REQUIRE a FRESH ordered frame_drained
task automatic run_scene(input int k, input int exp_records);
int r0, f0, d=0; logic fd_before; int gd0, fm0;
r0=fd_rises; f0=fd_falls; fd_before=frame_drained; gd0=g_drops; fm0=frag_after_marker;
@(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("[zint] epoch %0d: feeder_ready did not drop after GO", k); errors++; end
if (fd_before) begin
d=0; while (fd_falls==f0 && d<1500000) begin @(posedge emif_clk); d++; end
if (fd_falls==f0) begin $error("[zint] epoch %0d: frame_drained never fell from stale high", k); errors++; end
end
d=0; while (fd_rises<=r0 && d<1500000) begin @(posedge emif_clk); d++; end
if (fd_rises<=r0) begin $error("[zint] epoch %0d: frame_drained never rose", k); errors++; end
repeat(100) @(posedge clk);
if (feeder_records_w!==exp_records) begin $error("[zint] epoch %0d: records=%0d exp %0d", k, feeder_records_w, exp_records); errors++; end
if (col_ovf!==0 || bresp_err!==0) begin $error("[zint] epoch %0d: col_ovf=%0d bresp_err=%0d", k, col_ovf, bresp_err); errors++; end
if (g_drops!==gd0) begin $error("[zint] epoch %0d: %0d FRAGMENT DROPS (request FIFO overflow)", k, g_drops-gd0); errors++; end
if (frag_after_marker!==fm0) begin $error("[zint] epoch %0d: %0d fragment(s) after scene marker (ordering)", k, frag_after_marker-fm0); errors++; end
$display("[zint] epoch %0d: fresh drain (f %0d->%0d, r %0d->%0d) records=%0d drops=%0d fed=%0d pass=%0d",
k, f0, fd_falls, r0, fd_rises, feeder_records_w, g_drops, frags_fed, sb_pass);
endtask
task automatic run_epoch(input int k);
string fn; fn=$sformatf("../../data/top_psmct32_raster_demo/sh3_zs640%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
initial begin
errors=0; feeder_go_tb=1'b0; fill_start=1'b0; wr_arm=0; zc_enable=1'b1; stg_we=0; stg_waddr=0; stg_wdata=0;
for (int i=0;i<NPX;i++) begin sb_z[i]=16'h0000; sb_col[i]=32'd0; sb_wr[i]=1'b0; end
for (int b=0;b<MEMBEATS;b++) shmem[b]=256'd0; // PRECLEAR color+Z EXACTLY ONCE
// skip-guard: probe a fixture (via $fopen — avoids the $readmemh range warning)
begin int pfh; pfh=$fopen("../../data/top_psmct32_raster_demo/sh3_zs6400_tex_lpddr.mem","r");
if (pfh==0) begin $display("[tb_top_psmct32_sh3_zint640] SKIP — sh3_zs640*.mem absent (run gs_make_sh3_scheduler_fixture.py --authz --fb640 --tag zs640 --emit)"); $finish; end
$fclose(pfh);
end
rst_n=1'b0; core_go=1'b0; repeat(6) @(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);
wait (feeder_ready_tb==1'b1); repeat(50) @(posedge clk);
// ARM (-> clear_start) AFTER preclear, BEFORE first GO. z_rmw's clear_start writes Z_CLEAR to EVERY Z beat
// (NBEATS=NPX/16, through wr_arb s2 under random backpressure) then asserts clear_done -> it is the authoritative
// Z preclear. Wait for it (the board host likewise gates the first GO on the clear_done status bit).
wr_arm<=1'b1;
begin int d=0; while (!clear_done && d<600000) begin @(posedge emif_clk); d++; end end
if (!clear_done) begin $error("[zint] clear_done never rose after ARM (Z preclear did not finish)"); errors++; end
else $display("[zint] Z preclear complete (clear_done), z_beats_written(clear)=%0d", z_beats_written);
repeat(40) @(posedge clk);
for (int k=0;k<NEP;k++) run_epoch(k);
if (pre_clear_frags!==0) begin $error("[zint] %0d psm0 fragment(s) before clear_done", pre_clear_frags); errors++; end
if (fd_rises<NEP) begin $error("[zint] expected %0d ordered drains: rises=%0d", NEP, fd_rises); errors++; end
if (fd_falls<NEP-1) begin $error("[zint] epochs after the first never cleared stale (falls=%0d)", fd_falls); errors++; end
if (eof_count!==NEP)begin $error("[zint] scene markers=%0d != accepted GOs=%0d", eof_count, NEP); errors++; end
if (g_drops!==0) begin $error("[zint] TOTAL %0d fragment drops", g_drops); errors++; end
// ===== final memory compare: shared LPDDR color + Z == scoreboard =====
begin
int zmis, cmis, zchk, cchk; zmis=0; cmis=0; zchk=0; cchk=0;
for (int o=0;o<NPX;o++) begin
logic [15:0] zs; logic [31:0] cs; int zb, zl;
zb=(ZBASE + o*2)>>5; zl=o&15; zs=shmem[zb][zl*16+:16];
zchk++;
if (zs !== sb_z[o]) begin if (zmis<10) $error("[zint] Z px%0d got %04x exp %04x", o, zs, sb_z[o]); zmis++; end
if (sb_wr[o]) begin
cs=shmem[o>>3][(o[2:0])*32+:32]; cchk++;
if (cs[23:0] !== sb_col[o][23:0]) begin if (cmis<10) $error("[zint] COL px%0d got %06x exp %06x", o, cs[23:0], sb_col[o][23:0]); cmis++; end
end
end
$display("[zint] final compare: Z %0d/%0d mismatch, COLOR %0d/%0d mismatch (written px=%0d)", zmis, zchk, cmis, cchk, cchk);
end
begin
string fdump;
if ($value$plusargs("FBDUMP=%s", fdump)) begin
int fh;
fh = $fopen(fdump, "w");
if (fh == 0) begin
$error("[zint] could not open FBDUMP '%s'", fdump);
errors++;
end else begin
for (int o=0;o<NPX;o++) begin
logic [31:0] cs;
cs = shmem[o>>3][(o[2:0])*32+:32];
$fdisplay(fh, "%08x", cs);
end
$fclose(fh);
$display("[zint] dumped final COLOR FB (%0dx%0d) -> %s", W, H, fdump);
end
end
end
$display("[tb_top_psmct32_sh3_zint640] fed=%0d pass=%0d drops=%0d markers=%0d drains(r/f)=%0d/%0d col_ovf=%0d bresp_err=%0d errors=%0d",
frags_fed, sb_pass, g_drops, eof_count, fd_rises, fd_falls, col_ovf, bresp_err, errors);
$display("[zint] map: COLOR 0x%0h..0x%0h Z 0x%0h..0x%0h", COLBASE, COLBASE+COL_TOP, ZBASE, ZBASE+Z_TOP);
if (errors==0) $display("[tb_top_psmct32_sh3_zint640] PASS"); else $display("[tb_top_psmct32_sh3_zint640] FAIL");
$finish;
end
initial begin #400000000; $error("[tb_top_psmct32_sh3_zint640] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_zint640
@@ -0,0 +1,673 @@
// retroDE_ps2 — tb_top_psmct32_sh3_zint640_shared (Ch360 — four-epoch real-raster persistent-Z integration)
//
// Codex's pre-fit gate: prove the persistent-Z ROP through the SAME path the de25 top wires (GS_SH3_LPDDR_FB_Z):
// REAL demo (feeder + raster, authz sh3_zs640c12 NATIVE-640x480 strong-reject scene) -> gs_lpddr_zc_emit -> REAL de25 arbiters
// (gs_lpddr_wr_arb s0=color / s2=Z-write ; gs_lpddr_rd_arb s3=Z-read) -> ONE shared behavioral LPDDR
// with randomized backpressure. Unlike tb_top_psmct32_sh3_zarb (which fed a handshake-throttled trace),
// the raster CANNOT honor g_ready, so this is the gate that catches request-FIFO fragment DROPS.
//
// Asserts (Codex list):
// * final LPDDR color + Z == independent clamp16 persistent-Z scoreboard over the ACTUAL emitted fragments;
// * zero fragment drops (g_valid && !g_ready) — the whole reason this sim exists;
// * exactly one ordered scene marker (sh3_fb_flush) per accepted GO; no fragment after a marker within a scene;
// * Z cache invalidated (clear_done) after preclear and before the first GO; Z persists across epochs;
// * a FRESH ordered frame_drained per epoch; scanout never referenced during render (rd_arb s0/s1/s2 idle);
// * merged AW only in color/Z ranges, merged AR only in Z range (disjoint-map monitor).
// LOCAL/gitignored fixtures; skip-guard if absent.
`ifndef SH3_SHARED_PARAMS
`error "Define SH3_SHARED_PARAMS as a quoted generated params header"
`endif
`ifndef SH3_SHARED_TAG
`error "Define SH3_SHARED_TAG as a quoted fixture tag"
`endif
`ifndef SH3_SHARED_EPOCH_TABLE
`error "Define SH3_SHARED_EPOCH_TABLE as a quoted generated descriptor table"
`endif
`ifndef SH3_SHARED_RECIP_BITS
`define SH3_SHARED_RECIP_BITS 11
`endif
`timescale 1ns/1ps
module tb_top_psmct32_sh3_zint640_shared;
`ifdef SH3_FAST_GRAD
// Full-frame fidelity runs need the quotient, not cycle-accurate setup
// latency. The combinational simulation path is signed-/ bit-exact to
// gs_grad_divider and avoids 64 iterations for each of 20 attributes.
localparam bit TB_GRAD_SEQ = 1'b0;
localparam int TB_GRAD_CYCLES = 1;
`elsif SH3_BOARD_GRAD
// Production Ch417 configuration: the same registered-numerator
// combinational divider used by the DE25 top, including its complete
// five-cycle settle contract. This mode is the pre-fit control-path gate;
// SH3_FAST_GRAD remains useful for quicker framebuffer iterations.
localparam bit TB_GRAD_SEQ = 1'b0;
localparam int TB_GRAD_CYCLES = 5;
`else
localparam bit TB_GRAD_SEQ = 1'b1;
localparam int TB_GRAD_CYCLES = 4;
`endif
integer debug_tfx_samples = 0;
always @(posedge clk) begin
if ($test$plusargs("DEBUG_TFX") && dut.u_gs.comb_state_r == 3'd2 && debug_tfx_samples < 8) begin
$display("[zint][tfx] tex0=%016x tfx=%0d mod=%0d tex=%08x interp=%08x cs_prev=%08x",
dut.u_gs.tex0_1_q, dut.u_gs.comb_tfx, dut.u_gs.comb_modulate,
dut.u_gs.s1_tex_color, dut.u_gs.comb_interp_color, dut.u_gs.comb_cs_r);
debug_tfx_samples = debug_tfx_samples + 1;
end
end
`include `SH3_SHARED_PARAMS
localparam string FIXTURE_TAG = `SH3_SHARED_TAG;
localparam int MAX_EPOCHS = 1024;
localparam int W = FBPXW, H = FBH;
localparam int NPX = W*H;
localparam int NEP = N_EPOCHS;
localparam [31:0] COLBASE = 32'h0000_0000, ZBASE = 32'h0014_0000;
localparam int COL_TOP = NPX*4; // color region byte-size (disjoint below ZBASE)
localparam int Z_TOP = NPX*2; // Z region byte-size
localparam int MEMBEATS = 65536; // shared LPDDR: covers ZBASE + Z_TOP
// per-epoch expected CRC / records
// Ch360 — four epochs share one texture: EPk_REUSE=1 means the epoch runs on the resident cache without a fill.
logic [31:0] EP_CRC [0:MAX_EPOCHS-1]; int EP_REC [0:MAX_EPOCHS-1]; bit EP_REUSE [0:MAX_EPOCHS-1]; int fills;
initial begin
`include `SH3_SHARED_EPOCH_TABLE
fills=0;
end
`ifdef SH3_SHARED_REQ_DEPTH
localparam int TB_REQ_DEPTH = `SH3_SHARED_REQ_DEPTH;
`else
localparam int TB_REQ_DEPTH = 1024; // Ch359 production depth (Codex: cold-Z burst peaks at 478)
`endif
logic clk=0; always #5 clk=~clk;
logic emif_clk=0; always #2 emif_clk=~emif_clk;
logic rst_n; logic fb_commit=0; always #13 fb_commit=~fb_commit;
int errors; initial errors=0;
// clamp16 (vendored PCSX2 PSMZ16S source-Z saturation)
function automatic logic [15:0] cl16(input logic [31:0] z); cl16=(|z[31:16])?16'hFFFF:z[15:0]; endfunction
// ===== bram-top (FB_LPDDR_ONLY, AUTOSTART off), 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 [3:0] flush_be_w; logic [5:0] flush_psm_w;
logic [11:0] flush_x_w, flush_y_w; logic [31:0] flush_z_w;
logic stg_we; logic [11:0] stg_waddr; logic [63:0] stg_wdata;
// Optional HPS-resident palette path. Normal shared-fixture runs hold it idle;
// the runtime-CLUT target drives a complete palette before each epoch.
logic runtime_clut_wr_en, runtime_clut_busy;
logic [7:0] runtime_clut_wr_idx;
logic [31:0] runtime_clut_wr_data;
logic [31:0] runtime_pal [0:255];
logic flush_abe_w;
logic [16:0] flush_alpha_w;
logic flush_ztest_w, flush_zmsk_w; logic [1:0] flush_ztst_w;
integer frag_fh;
integer frag_epoch;
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(`SH3_SHARED_RECIP_BITS),
`ifdef SH3_SUBPIXEL_XY
.SUBPIXEL_XY(1'b1),
`endif
`ifdef SH3_SHARED_NO_BILINEAR
.BILINEAR_ENABLE(1'b0), .PALETTE_BILINEAR(1'b0),
`else
.BILINEAR_ENABLE(1'b1), .PALETTE_BILINEAR(1'b1),
`endif
.GRAD_SEQ_DIVIDER(TB_GRAD_SEQ), .GRAD_DIV_CYCLES(TB_GRAD_CYCLES),
.GS_LPDDR_TEX(1'b1), .TEX_VRAM_BASE(TEX_VRAM_BASE), .TEX_CACHE_BYTES(TEX_BYTES),
.CLUT_CSM1_ENABLE(1'b1), .RAM_SIZE_BYTES(32 * 1024), .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_be_o(flush_be_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),
.flush_abe_o(flush_abe_w), .flush_alpha_o(flush_alpha_w),
.flush_ztest_o(flush_ztest_w), .flush_ztst_o(flush_ztst_w), .flush_zmsk_o(flush_zmsk_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),
.runtime_clut_wr_en_i(runtime_clut_wr_en), .runtime_clut_wr_idx_i(runtime_clut_wr_idx),
.runtime_clut_wr_data_i(runtime_clut_wr_data), .runtime_clut_busy_i(runtime_clut_busy)
);
// texture cache + behavioral texture LPDDR (reloaded per epoch)
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];
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: one ordered scene marker (sh3_fb_flush) per accepted GO =====
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
// ===== gs_lpddr_zc_emit — Z-then-color emit, fed by the REAL raster (NO handshake) =====
logic zc_enable, clear_start, clear_done, frame_drained;
wire [15:0] g_zq = cl16(flush_z_w);
wire frag_v = flush_emit_w && (flush_psm_w==6'h00);
wire g_valid = frag_v || fb_flush;
wire g_scene = fb_flush;
wire [11:0] g_x = flush_x_w, g_y = flush_y_w;
wire [31:0] g_color = flush_color32_w;
wire [16:0] g_alpha = flush_alpha_w;
wire [3:0] g_be = flush_be_w;
wire g_ready;
// Z AXI
logic [31:0] z_araddr; logic [7:0] z_arlen; logic [2:0] z_arsize; logic [1:0] z_arburst; logic z_arvalid, z_arready;
logic [255:0] z_rdata; logic [1:0] z_rresp; logic z_rlast, z_rvalid, z_rready;
logic [31:0] z_awaddr; logic [7:0] z_awlen; logic [2:0] z_awsize; logic [1:0] z_awburst; logic z_awvalid, z_awready;
logic [255:0] z_wdata; logic [31:0] z_wstrb; logic z_wlast, z_wvalid, z_wready; logic z_bvalid, z_bready; logic [1:0] z_bresp;
logic [31:0] d_araddr; logic [7:0] d_arlen; logic [2:0] d_arsize; logic [1:0] d_arburst; logic d_arvalid, d_arready;
logic [255:0] d_rdata; logic [1:0] d_rresp; logic d_rlast, d_rvalid, d_rready;
// Color AXI
logic [31:0] c_awaddr; logic [7:0] c_awlen; logic [2:0] c_awsize; logic [1:0] c_awburst; logic c_awvalid, c_awready;
logic [255:0] c_wdata; logic [31:0] c_wstrb; logic c_wlast, c_wvalid, c_wready; logic c_bvalid, c_bready; logic [1:0] c_bresp;
logic [31:0] z_beats_read, z_beats_written, c_beats_written, col_ovf, bresp_err; logic zc_idle;
gs_lpddr_zc_emit #(.COLBASE(COLBASE), .ZBASE(ZBASE), .FB_PXW(W), .FB_H(H), .REQ_DEPTH(TB_REQ_DEPTH), .COL_DEPTH(128)) u_zc (
.gs_clk(clk), .gs_rst_n(rst_n), .enable(zc_enable),
.g_valid(g_valid), .g_ready(g_ready), .g_x(g_x), .g_y(g_y), .g_zq(g_zq), .g_zmsk(flush_zmsk_w),
.g_ztest(flush_ztest_w), .g_ztst(flush_ztst_w), .g_color(g_color), .g_alpha(g_alpha), .g_be(g_be), .g_scene(g_scene),
.axi_clk(emif_clk), .axi_rst_n(rst_n), .clear_start(clear_start), .clear_done(clear_done), .frame_drained(frame_drained),
.z_araddr(z_araddr), .z_arlen(z_arlen), .z_arsize(z_arsize), .z_arburst(z_arburst), .z_arvalid(z_arvalid), .z_arready(z_arready),
.z_rdata(z_rdata), .z_rresp(z_rresp), .z_rlast(z_rlast), .z_rvalid(z_rvalid), .z_rready(z_rready),
.d_araddr(d_araddr), .d_arlen(d_arlen), .d_arsize(d_arsize), .d_arburst(d_arburst), .d_arvalid(d_arvalid), .d_arready(d_arready),
.d_rdata(d_rdata), .d_rresp(d_rresp), .d_rlast(d_rlast), .d_rvalid(d_rvalid), .d_rready(d_rready),
.z_awaddr(z_awaddr), .z_awlen(z_awlen), .z_awsize(z_awsize), .z_awburst(z_awburst), .z_awvalid(z_awvalid), .z_awready(z_awready),
.z_wdata(z_wdata), .z_wstrb(z_wstrb), .z_wlast(z_wlast), .z_wvalid(z_wvalid), .z_wready(z_wready),
.z_bvalid(z_bvalid), .z_bready(z_bready), .z_bresp(z_bresp),
.c_awaddr(c_awaddr), .c_awlen(c_awlen), .c_awsize(c_awsize), .c_awburst(c_awburst), .c_awvalid(c_awvalid), .c_awready(c_awready),
.c_wdata(c_wdata), .c_wstrb(c_wstrb), .c_wlast(c_wlast), .c_wvalid(c_wvalid), .c_wready(c_wready),
.c_bvalid(c_bvalid), .c_bready(c_bready), .c_bresp(c_bresp),
.z_beats_read(z_beats_read), .z_beats_written(z_beats_written), .c_beats_written(c_beats_written),
.col_ovf(col_ovf), .bresp_err(bresp_err), .idle(zc_idle)
);
// Ch359 DIAGNOSTIC — request-FIFO occupancy high-water (write-domain binary pointers, hierarchical):
// sizes the burst that overflows REQ_DEPTH=256 (would a deeper FIFO absorb it, or is the ep0 dirty-miss
// streak service-rate-bound?). Sampled on the write clock; wbin-rbin_sampled is a safe over-estimate.
// (Codex correction: occupancy must be computed ENTIRELY in the write clock domain — wbin vs the
// write-domain-SYNCHRONIZED read pointer rgray_s2 converted gray->binary — with the modulus derived from the
// selected depth, not a hard-coded 2*256.)
int req_hiwater; logic hw_clear;
function automatic int g2b(input int g);
int b; b=g; b^=b>>1; b^=b>>2; b^=b>>4; b^=b>>8; b^=b>>16; return b;
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) req_hiwater <= 0;
else if (hw_clear) req_hiwater <= 0;
else begin
int occ;
occ = (int'(u_zc.u_req.wbin) - g2b(int'(u_zc.u_req.rgray_s2))) % (2*TB_REQ_DEPTH);
if (occ < 0) occ += 2*TB_REQ_DEPTH;
if (occ > req_hiwater) req_hiwater <= occ;
end
end
// fragment-drop guard (design clk): the request FIFO silently drops when g_ready is low
int g_drops; int frags_fed;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin g_drops<=0; frags_fed<=0; end
else begin
if (frag_v && !g_ready) g_drops<=g_drops+1;
if (frag_v && g_ready) frags_fed<=frags_fed+1;
end
end
// clear ordering: clear_done must rise before the first GO. no psm0 fragment before clear_done.
logic saw_clear_done=0, first_go_done=0; int pre_clear_frags;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin saw_clear_done<=0; pre_clear_frags<=0; end
else begin
if (clear_done) saw_clear_done<=1;
if (frag_v && !saw_clear_done) pre_clear_frags<=pre_clear_frags+1;
end
end
// ===== REAL de25 write arbiter: s0=color, s2=Z-write ; s1/s3 inert =====
logic [29:0] m_awaddr; logic [1:0] m_awburst; logic [6:0] m_awid; logic [7:0] m_awlen; logic [2:0] m_awsize;
logic m_awvalid, m_awready; logic [255:0] m_wdata; logic [31:0] m_wstrb; logic m_wlast, m_wvalid, m_wready;
logic m_bvalid, m_bready; logic [1:0] m_bresp;
gs_lpddr_wr_arb u_wr_arb (
.clk(emif_clk), .rst_n(rst_n),
.s0_awaddr(c_awaddr[29:0]), .s0_awburst(c_awburst), .s0_awid(7'd0), .s0_awlen(c_awlen), .s0_awsize(c_awsize),
.s0_awvalid(c_awvalid), .s0_awready(c_awready), .s0_wdata(c_wdata), .s0_wstrb(c_wstrb), .s0_wlast(c_wlast),
.s0_wvalid(c_wvalid), .s0_wready(c_wready), .s0_bresp(c_bresp), .s0_bvalid(c_bvalid), .s0_bready(c_bready),
.s1_awaddr(30'd0), .s1_awburst(2'b01), .s1_awid(7'd1), .s1_awlen(8'd0), .s1_awsize(3'b101),
.s1_awvalid(1'b0), .s1_awready(), .s1_wdata(256'd0), .s1_wstrb(32'd0), .s1_wlast(1'b0),
.s1_wvalid(1'b0), .s1_wready(), .s1_bresp(), .s1_bvalid(), .s1_bready(1'b0),
.s2_awaddr(z_awaddr[29:0]), .s2_awburst(z_awburst), .s2_awid(7'd2), .s2_awlen(z_awlen), .s2_awsize(z_awsize),
.s2_awvalid(z_awvalid), .s2_awready(z_awready), .s2_wdata(z_wdata), .s2_wstrb(z_wstrb), .s2_wlast(z_wlast),
.s2_wvalid(z_wvalid), .s2_wready(z_wready), .s2_bresp(z_bresp), .s2_bvalid(z_bvalid), .s2_bready(z_bready),
.s3_awaddr(30'd0), .s3_awburst(2'b01), .s3_awid(7'd3), .s3_awlen(8'd0), .s3_awsize(3'b101),
.s3_awvalid(1'b0), .s3_awready(), .s3_wdata(256'd0), .s3_wstrb(32'd0), .s3_wlast(1'b0),
.s3_wvalid(1'b0), .s3_wready(), .s3_bresp(), .s3_bvalid(), .s3_bready(1'b0),
.m_awaddr(m_awaddr), .m_awburst(m_awburst), .m_awid(m_awid), .m_awlen(m_awlen), .m_awsize(m_awsize),
.m_awvalid(m_awvalid), .m_awready(m_awready), .m_wdata(m_wdata), .m_wstrb(m_wstrb), .m_wlast(m_wlast),
.m_wvalid(m_wvalid), .m_wready(m_wready), .m_bvalid(m_bvalid), .m_bready(m_bready), .m_bresp(m_bresp)
);
// ===== REAL de25 read arbiter: s3=Z-read (mirrors de25 reload port) ; s0/s1/s2 idle during render =====
logic [29:0] m_araddr; logic [1:0] m_arburst; logic [6:0] m_arid; logic [7:0] m_arlen; logic [2:0] m_arsize;
logic m_arvalid, m_arready; logic [255:0] m_rdata; logic [1:0] m_rresp; logic m_rlast, m_rvalid, m_rready;
gs_lpddr_rd_arb u_rd_arb (
.clk(emif_clk), .rst_n(rst_n),
.s0_araddr(30'd0), .s0_arburst(2'b01), .s0_arid(7'd0), .s0_arlen(8'd0), .s0_arsize(3'b101),
.s0_arvalid(1'b0), .s0_arready(), .s0_rdata(), .s0_rresp(), .s0_rlast(), .s0_rvalid(), .s0_rready(1'b0),
.s1_araddr(30'd0), .s1_arburst(2'b01), .s1_arid(7'd1), .s1_arlen(8'd0), .s1_arsize(3'b101),
.s1_arvalid(1'b0), .s1_arready(), .s1_rdata(), .s1_rresp(), .s1_rlast(), .s1_rvalid(), .s1_rready(1'b0),
.s2_araddr(30'd0), .s2_arburst(2'b01), .s2_arid(7'd2), .s2_arlen(8'd0), .s2_arsize(3'b101),
.s2_arvalid(1'b0), .s2_arready(), .s2_rdata(), .s2_rresp(), .s2_rlast(), .s2_rvalid(), .s2_rready(1'b0),
.s3_araddr(z_araddr[29:0]), .s3_arburst(z_arburst), .s3_arid(7'd3), .s3_arlen(z_arlen), .s3_arsize(z_arsize),
.s3_arvalid(z_arvalid), .s3_arready(z_arready), .s3_rdata(z_rdata), .s3_rresp(z_rresp), .s3_rlast(z_rlast),
.s3_rvalid(z_rvalid), .s3_rready(z_rready),
.s4_araddr(d_araddr[29:0]), .s4_arburst(d_arburst), .s4_arid(7'd4), .s4_arlen(d_arlen), .s4_arsize(d_arsize),
.s4_arvalid(d_arvalid), .s4_arready(d_arready), .s4_rdata(d_rdata), .s4_rresp(d_rresp), .s4_rlast(d_rlast),
.s4_rvalid(d_rvalid), .s4_rready(d_rready),
.m_araddr(m_araddr), .m_arburst(m_arburst), .m_arid(m_arid), .m_arlen(m_arlen), .m_arsize(m_arsize),
.m_arvalid(m_arvalid), .m_arready(m_arready), .m_rdata(m_rdata), .m_rresp(m_rresp), .m_rlast(m_rlast),
.m_rvalid(m_rvalid), .m_rready(m_rready)
);
logic [15:0] lf=16'hF00D; always_ff @(posedge emif_clk) lf<={lf[14:0], lf[15]^lf[13]^lf[12]^lf[10]};
// ===== ONE shared behavioral LPDDR (write + read), address-decoded, random backpressure =====
logic [255:0] shmem [0:MEMBEATS-1];
logic [29:0] wa; logic [255:0] wd; logic [31:0] ws; logic aw_s, w_s; logic [3:0] bd; logic bp;
logic [29:0] ra; logic rp; logic [3:0] rd_dly;
always_ff @(posedge emif_clk or negedge rst_n) begin
if(!rst_n) begin
m_awready<=0; m_wready<=0; m_bvalid<=0; m_bresp<=0; aw_s<=0; w_s<=0; bp<=0; bd<=0;
m_arready<=0; m_rvalid<=0; m_rlast<=0; m_rresp<=0; m_rdata<=0; rp<=0; rd_dly<=0;
end else begin
m_awready<=(!aw_s)?lf[0]:1'b0; m_wready<=(!w_s)?lf[1]:1'b0;
if(m_awvalid&&m_awready) begin wa<=m_awaddr; aw_s<=1; m_awready<=0; end
if(m_wvalid&&m_wready) begin wd<=m_wdata; ws<=m_wstrb; w_s<=1; m_wready<=0; end
if(aw_s&&w_s&&!bp&&!m_bvalid) begin
for(int bb=0;bb<32;bb++) if(ws[bb]) shmem[wa>>5][bb*8+:8]<=wd[bb*8+:8];
bp<=1; bd<={1'b0,lf[4:2]};
end
if(bp&&!m_bvalid) begin if(bd==0) begin m_bvalid<=1;m_bresp<=0;bp<=0;aw_s<=0;w_s<=0; end else bd<=bd-1; end
if(m_bvalid&&m_bready) m_bvalid<=0;
m_arready<=(!rp&&!m_rvalid)?lf[8]:1'b0;
if(m_arvalid&&m_arready) begin ra<=m_araddr; rp<=1; rd_dly<={1'b0,lf[7:5]}; m_arready<=0; end
if(rp&&!m_rvalid) begin if(rd_dly==0) begin m_rdata<=shmem[ra>>5]; m_rresp<=0; m_rvalid<=1; m_rlast<=1; rp<=0; end else rd_dly<=rd_dly-1; end
if(m_rvalid&&m_rready) begin m_rvalid<=0; m_rlast<=0; end
end
end
// disjoint-map monitors on the MERGED master streams
always_ff @(posedge emif_clk) if(rst_n) begin
if(m_awvalid) begin
if(!((m_awaddr<30'(COLBASE+COL_TOP)) || (m_awaddr>=30'(ZBASE) && m_awaddr<30'(ZBASE+Z_TOP))))
begin $error("[zint] merged AW %h not in color/Z range",m_awaddr); errors++; end
end
if(m_arvalid && !((m_araddr<COL_TOP) || (m_araddr>=30'(ZBASE) && m_araddr<30'(ZBASE+Z_TOP)))) begin $error("[zint] merged AR %h not in color/Z range",m_araddr); errors++; end
end
// ===== clamp16 persistent-Z SCOREBOARD over the ACTUAL emitted fragments (design clk) =====
// Mirrors gs_lpddr_z_rmw: GEQUAL vs stored, Z_CLEAR=0. Persists across epochs. Gated active after clear_done.
logic [15:0] sb_z [0:NPX-1];
logic [31:0] sb_col[0:NPX-1];
logic sb_wr [0:NPX-1];
integer sb_frag, sb_pass;
function automatic [7:0] sel_rgb(input [7:0] cs,input [7:0] cd,input [1:0] sel);
sel_rgb=(sel==0)?cs:(sel==1)?cd:8'd0;
endfunction
function automatic [7:0] blend_rgb(input [7:0] cs,input [7:0] cd,input [1:0] aa,input [1:0] bb,input [1:0] cc,input [1:0] dd,input [7:0] as,input [7:0] ad,input [7:0] fix);
integer coef, val, op_a, op_b, op_d;
begin
coef=(cc==0)?((as>128)?128:as):(cc==1)?ad:fix;
op_a=sel_rgb(cs,cd,aa); op_b=sel_rgb(cs,cd,bb); op_d=sel_rgb(cs,cd,dd);
val=(((op_a-op_b)*coef) >>> 7)+op_d;
blend_rgb=(val<0)?0:(val>255)?255:val;
end
endfunction
function automatic [31:0] expected_color(input [31:0] cs,input [31:0] cd,input [16:0] alpha,input [3:0] be);
logic [31:0] blended;
begin
if (!alpha[16]) blended=cs;
else blended={cs[31:24],
blend_rgb(cs[23:16],cd[23:16],alpha[15:14],alpha[13:12],alpha[11:10],alpha[9:8],cs[31:24],cd[31:24],alpha[7:0]),
blend_rgb(cs[15:8], cd[15:8], alpha[15:14],alpha[13:12],alpha[11:10],alpha[9:8],cs[31:24],cd[31:24],alpha[7:0]),
blend_rgb(cs[7:0], cd[7:0], alpha[15:14],alpha[13:12],alpha[11:10],alpha[9:8],cs[31:24],cd[31:24],alpha[7:0])};
for (int i=0;i<4;i++)
expected_color[i*8 +: 8]=be[i]?blended[i*8 +: 8]:cd[i*8 +: 8];
end
endfunction
// no-fragment-after-marker: a fragment must not appear in the same scene after fb_flush (before next GO)
logic scene_ended=0; int frag_after_marker;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sb_frag<=0; sb_pass<=0; scene_ended<=0; frag_after_marker<=0;
end else begin
if (feeder_go_tb && feeder_ready_tb) scene_ended<=0; // new scene opens
if (fb_flush) scene_ended<=1; // ordered marker closes the scene
if (frag_v && saw_clear_done) begin
int o; logic [15:0] zq;
`ifdef SH3_SHARED_TRACE
if (frag_fh != 0)
$fwrite(frag_fh, "%0d %0d %0d %0d %08x\n", frag_epoch, g_x, g_y, flush_z_w, flush_color32_w);
`endif
if (scene_ended) frag_after_marker<=frag_after_marker+1; // fragment after this scene's marker = ordering bug
o = g_y*W + g_x; zq = cl16(flush_z_w);
sb_frag<=sb_frag+1;
if (o>=0 && o<NPX) begin
if (!flush_ztest_w || (flush_ztst_w==2'd1)
|| ((flush_ztst_w==2'd2) && zq >= sb_z[o])
|| ((flush_ztst_w==2'd3) && zq > sb_z[o])) begin
if (!flush_zmsk_w) sb_z[o]<=zq;
sb_col[o]<=expected_color(flush_color32_w,sb_col[o],flush_alpha_w,flush_be_w);
sb_wr[o]<=1'b1; sb_pass<=sb_pass+1;
end
end
end
end
end
// FRESH-DRAIN observer (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<=frame_drained;
if (frame_drained && !fd_q) fd_rises<=fd_rises+1;
if (!frame_drained && fd_q) fd_falls<=fd_falls+1;
end
end
// clear_start: mirror de25 — pulse once at the writer ARM (after preclear, before first GO)
logic wr_arm=0, arm_e1, arm_e2, arm_e3;
always_ff @(posedge emif_clk or negedge rst_n) begin
if (!rst_n) begin arm_e1<=0; arm_e2<=0; arm_e3<=0; clear_start<=0; end
else begin arm_e1<=wr_arm; arm_e2<=arm_e1; arm_e3<=arm_e2; clear_start<=(arm_e2 && !arm_e3); end
end
// ---- feeder staging stream (write port), per epoch ----
logic [63:0] stg_buf [0:STG_WORDS-1];
task automatic stream_list(input int k);
string fn; fn=$sformatf("../../data/top_psmct32_raster_demo/feeder_sh3_%s%0d.mem", FIXTURE_TAG, 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);
if (dut.g_feeder.feeder_stg[0] !== stg_buf[0]) begin
$error("[zint] epoch %0d: staged word0=%016x exp %016x", k, dut.g_feeder.feeder_stg[0], stg_buf[0]); errors++; end
endtask
task automatic fill_cache(input int k, input logic [31:0] exp_crc);
int d=0;
fills=fills+1;
@(posedge clk) fill_start<=1'b1; @(posedge clk) fill_start<=1'b0;
d=0; while (fill_done && d<1000) begin @(posedge clk); d++; end
d=0; while (!fill_done && d<400000) begin @(posedge clk); d++; end
if (!fill_done) begin $error("[zint] fill %0d: fill_done never rose", k); errors++; end
if (fill_crc_w!==exp_crc) begin $error("[zint] fill %0d: crc=%08x exp %08x", k, fill_crc_w, exp_crc); errors++; end
if (tex_rd_errs!==0) begin $error("[zint] fill %0d: rd_errs=%0d", k, tex_rd_errs); errors++; end
endtask
`ifdef SH3_SHARED_RUNTIME_CLUT
task automatic load_runtime_palette(input int k);
string fn;
fn = $sformatf("../../data/top_psmct32_raster_demo/sh3_%s%0d_pal.mem", FIXTURE_TAG, k);
$readmemh(fn, runtime_pal);
@(negedge clk);
runtime_clut_busy <= 1'b1;
for (int i=0; i<256; i++) begin
runtime_clut_wr_en <= 1'b1;
runtime_clut_wr_idx <= i[7:0];
runtime_clut_wr_data <= runtime_pal[i];
@(negedge clk);
end
runtime_clut_wr_en <= 1'b0;
runtime_clut_wr_idx <= '0;
runtime_clut_wr_data <= '0;
@(negedge clk);
runtime_clut_busy <= 1'b0;
@(negedge clk);
endtask
`endif
// one-scene runner: GO, wait render+drain, REQUIRE a FRESH ordered frame_drained
task automatic run_scene(input int k, input int exp_records);
int r0, f0, d=0; logic fd_before; int gd0, fm0;
r0=fd_rises; f0=fd_falls; fd_before=frame_drained; gd0=g_drops; fm0=frag_after_marker;
@(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("[zint] epoch %0d: feeder_ready did not drop after GO", k); errors++; end
if (fd_before) begin
d=0; while (fd_falls==f0 && d<6000000) begin @(posedge emif_clk); d++; end
if (fd_falls==f0) begin $error("[zint] epoch %0d: frame_drained never fell from stale high", k); errors++; end
end
d=0; while (fd_rises<=r0 && d<6000000) begin @(posedge emif_clk); d++; end
if (fd_rises<=r0) begin $error("[zint] epoch %0d: frame_drained never rose", k); errors++; end
repeat(100) @(posedge clk);
// The extended Ch402 header keeps TEX0 at word 5. Check the complete
// 64-bit commit so high-half state such as TFX cannot disappear silently.
if (dut.u_gs.tex0_1_q !== stg_buf[5]) begin
$error("[zint] epoch %0d: TEX0 commit=%016x exp=%016x", k, dut.u_gs.tex0_1_q, stg_buf[5]); errors++; end
if (feeder_records_w!==exp_records) begin $error("[zint] epoch %0d: records=%0d exp %0d", k, feeder_records_w, exp_records); errors++; end
if (col_ovf!==0 || bresp_err!==0) begin $error("[zint] epoch %0d: col_ovf=%0d bresp_err=%0d", k, col_ovf, bresp_err); errors++; end
if (g_drops!==gd0) begin $error("[zint] epoch %0d: %0d FRAGMENT DROPS (request FIFO overflow)", k, g_drops-gd0); errors++; end
if (frag_after_marker!==fm0) begin $error("[zint] epoch %0d: %0d fragment(s) after scene marker (ordering)", k, frag_after_marker-fm0); errors++; end
$display("[zint] epoch %0d: fresh drain (f %0d->%0d, r %0d->%0d) records=%0d drops=%0d fed=%0d pass=%0d",
k, f0, fd_falls, r0, fd_rises, feeder_records_w, g_drops, frags_fed, sb_pass);
$fflush();
endtask
int cold_start_epoch, cold_asset_epoch, resident_asset_epoch;
task automatic run_epoch(input int k);
int asset_k;
string fn;
if (k == cold_start_epoch) asset_k = cold_asset_epoch;
else if (!EP_REUSE[k]) asset_k = k;
else asset_k = resident_asset_epoch;
fn=$sformatf("../../data/top_psmct32_raster_demo/sh3_%s%0d_tex_lpddr.mem", FIXTURE_TAG, asset_k);
if ((k == cold_start_epoch) || !EP_REUSE[k]) begin
$readmemh(fn, lpddr_mem);
fill_cache(k, EP_CRC[k]);
resident_asset_epoch = asset_k;
end else begin
// Ch359 — EXPLICIT REUSE: no reload, no fill pulse. Fail-closed residency: the fill CRC register is
// only rewritten by a fill, so it must still hold the prior epoch's verified CRC (same shared texture).
if (fill_crc_w!==EP_CRC[k]) begin $error("%s epoch %0d: REUSE but resident crc=%08x exp %08x — residency broken", "[zint]", k, fill_crc_w, EP_CRC[k]); errors++; end
else $display("%s epoch %0d: REUSE resident texture (crc=0x%08x, no fill)", "[zint]", k, fill_crc_w);
end
`ifdef SH3_SHARED_RUNTIME_CLUT
load_runtime_palette(asset_k);
`endif
stream_list(k);
frag_epoch = k;
run_scene(k, EP_REC[k]);
endtask
initial begin
int first_epoch, last_epoch, run_epochs, expected_fed;
bit check_expected_fed;
errors=0; feeder_go_tb=1'b0; fill_start=1'b0; wr_arm=0; zc_enable=1'b1; stg_we=0; stg_waddr=0; stg_wdata=0; hw_clear=1'b0;
runtime_clut_wr_en=0; runtime_clut_wr_idx='0; runtime_clut_wr_data='0; runtime_clut_busy=0; frag_epoch=0; frag_fh=0;
if (!$value$plusargs("START_EPOCH=%d", first_epoch)) first_epoch=0;
if (first_epoch < 0 || first_epoch >= NEP) begin
$error("[zint] START_EPOCH=%0d outside 0..%0d", first_epoch, NEP-1); $finish;
end
if (!$value$plusargs("END_EPOCH=%d", last_epoch)) last_epoch=NEP-1;
if (last_epoch < first_epoch || last_epoch >= NEP) begin
$error("[zint] END_EPOCH=%0d outside START_EPOCH=%0d..%0d", last_epoch, first_epoch, NEP-1); $finish;
end
run_epochs = last_epoch-first_epoch+1;
check_expected_fed = $value$plusargs("EXPECT_FED=%d", expected_fed);
cold_start_epoch = first_epoch;
if (!$value$plusargs("COLD_ASSET_EPOCH=%d", cold_asset_epoch)) cold_asset_epoch=first_epoch;
if (cold_asset_epoch < 0 || cold_asset_epoch > first_epoch) begin
$error("[zint] COLD_ASSET_EPOCH=%0d outside 0..START_EPOCH=%0d", cold_asset_epoch, first_epoch); $finish;
end
resident_asset_epoch = cold_asset_epoch;
`ifdef SH3_SHARED_TRACE
frag_fh=$fopen($sformatf("%s_frags.txt", FIXTURE_TAG), "w");
if (frag_fh==0) begin $error("[zint] could not open %s fragment trace", FIXTURE_TAG); $finish; end
`endif
for (int i=0;i<NPX;i++) begin sb_z[i]=16'h0000; sb_col[i]=32'd0; sb_wr[i]=1'b0; end
for (int b=0;b<MEMBEATS;b++) shmem[b]=256'd0; // PRECLEAR color+Z EXACTLY ONCE
// skip-guard: probe a fixture (via $fopen — avoids the $readmemh range warning)
begin int pfh; pfh=$fopen($sformatf("../../data/top_psmct32_raster_demo/sh3_%s0_tex_lpddr.mem", FIXTURE_TAG),"r");
if (pfh==0) begin $display("[tb_top_psmct32_sh3_zint640_shared] SKIP — sh3_%s*.mem absent", FIXTURE_TAG); $finish; end
$fclose(pfh);
end
// A late-epoch-only run begins with cold Z and an explicit texture fill.
// Reuse epochs name only their feeder list, so COLD_ASSET_EPOCH points
// at the earlier descriptor that owns the shared texture and palette.
rst_n=1'b0; core_go=1'b0; repeat(6) @(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);
`ifdef SH3_SHARED_RUNTIME_CLUT
// Runtime fixtures begin after the HPS has completed boot handoff. This
// standalone raster top has no HPS bootlet agent, so model that settled
// state while explicitly exercising all palette/list/render transactions.
force dut.dma_done_seen = 1'b1;
force dut.xfer_busy = 1'b0;
`else
wait (dma_done_seen==1'b1); repeat(10) @(posedge clk);
if (dut.xfer_busy==1'b1) wait (dut.xfer_busy==1'b0);
`endif
wait (feeder_ready_tb==1'b1); repeat(50) @(posedge clk);
// ARM (-> clear_start) AFTER preclear, BEFORE first GO. z_rmw's clear_start writes Z_CLEAR to EVERY Z beat
// (NBEATS=NPX/16, through wr_arb s2 under random backpressure) then asserts clear_done -> it is the authoritative
// Z preclear. Wait for it (the board host likewise gates the first GO on the clear_done status bit).
wr_arm<=1'b1;
begin int d=0; while (!clear_done && d<600000) begin @(posedge emif_clk); d++; end end
if (!clear_done) begin $error("[zint] clear_done never rose after ARM (Z preclear did not finish)"); errors++; end
else $display("[zint] Z preclear complete (clear_done), z_beats_written(clear)=%0d", z_beats_written);
repeat(40) @(posedge clk);
// Ch405 focused read-only-Z proof. After the real clear engine has
// completed, optionally seed alternating pixels with max depth in both
// the shared LPDDR model and independent scoreboard. Authentic fan
// fragments (ZMSK=1) must reject on max lanes, blend on zero lanes,
// and leave every stored Z lane unchanged.
if ($test$plusargs("CHECKER_Z")) begin
for (int o=0;o<NPX;o++) begin
logic [15:0] seed; int zb, zl;
seed = o[0] ? 16'hffff : 16'h0000;
sb_z[o] = seed;
zb=(ZBASE + o*2)>>5; zl=o&15;
shmem[zb][zl*16+:16] = seed;
end
$display("[zint] CHECKER_Z seeded alternating 0000/ffff after clear");
end
for (int k=first_epoch;k<=last_epoch;k++) begin
int fed0, dr0;
fed0=frags_fed; dr0=g_drops;
run_epoch(k);
$display("[zint][sweep] depth=%0d epoch %0d: accepted=%0d drops=%0d req_hiwater=%0d",
TB_REQ_DEPTH, k, frags_fed-fed0, g_drops-dr0, req_hiwater);
$fflush();
@(negedge clk); hw_clear<=1'b1; @(negedge clk); hw_clear<=1'b0; // per-epoch high-water
end
if (pre_clear_frags!==0) begin $error("[zint] %0d psm0 fragment(s) before clear_done", pre_clear_frags); errors++; end
if (fd_rises<run_epochs) begin $error("[zint] expected %0d ordered drains: rises=%0d", run_epochs, fd_rises); errors++; end
if (fd_falls<(run_epochs-1)) begin $error("[zint] epochs after the first never cleared stale (falls=%0d)", fd_falls); errors++; end
if (eof_count!==run_epochs)begin $error("[zint] scene markers=%0d != accepted GOs=%0d", eof_count, run_epochs); errors++; end
if (g_drops!==0) begin $error("[zint] TOTAL %0d fragment drops", g_drops); errors++; end
if (check_expected_fed && (frags_fed !== expected_fed)) begin
$error("[zint] fed=%0d, expected exact regression count %0d", frags_fed, expected_fed);
errors++;
end
// Descriptor-driven residency: shared-texture epochs reuse a verified fill; multi-texture descriptors refill.
begin
int exp_fills; exp_fills=0;
for (int k=first_epoch;k<=last_epoch;k++)
if ((k==first_epoch) || !EP_REUSE[k]) exp_fills++;
if (fills!==exp_fills) begin $error("[zint] fills=%0d exp=%0d from descriptor reuse flags", fills, exp_fills); errors++; end
end
// ===== final memory compare: shared LPDDR color + Z == scoreboard =====
begin
int zmis, cmis, zchk, cchk; zmis=0; cmis=0; zchk=0; cchk=0;
for (int o=0;o<NPX;o++) begin
logic [15:0] zs; logic [31:0] cs; int zb, zl;
zb=(ZBASE + o*2)>>5; zl=o&15; zs=shmem[zb][zl*16+:16];
zchk++;
if (zs !== sb_z[o]) begin if (zmis<10) $error("[zint] Z px%0d got %04x exp %04x", o, zs, sb_z[o]); zmis++; end
if (sb_wr[o]) begin
cs=shmem[o>>3][(o[2:0])*32+:32]; cchk++;
if (cs[23:0] !== sb_col[o][23:0]) begin if (cmis<10) $error("[zint] COL px%0d got %06x exp %06x", o, cs[23:0], sb_col[o][23:0]); cmis++; end
end
end
$display("[zint] final compare: Z %0d/%0d mismatch, COLOR %0d/%0d mismatch (written px=%0d)", zmis, zchk, cmis, cchk, cchk);
if (zmis != 0 || cmis != 0) errors += zmis + cmis;
end
begin
string fdump;
if ($value$plusargs("FBDUMP=%s", fdump)) begin
int fh;
fh = $fopen(fdump, "w");
if (fh == 0) begin
$error("[zint] could not open FBDUMP '%s'", fdump);
errors++;
end else begin
for (int o=0;o<NPX;o++) begin
logic [31:0] cs;
cs = shmem[o>>3][(o[2:0])*32+:32];
$fdisplay(fh, "%08x", cs);
end
$fclose(fh);
$display("[zint] dumped final COLOR FB (%0dx%0d) -> %s", W, H, fdump);
end
end
end
$display("[tb_top_psmct32_sh3_zint640_shared] fed=%0d pass=%0d drops=%0d markers=%0d drains(r/f)=%0d/%0d col_ovf=%0d bresp_err=%0d errors=%0d",
frags_fed, sb_pass, g_drops, eof_count, fd_rises, fd_falls, col_ovf, bresp_err, errors);
$display("[zint] req-FIFO depth=%0d (diagnostic sweep variant when != 1024)", TB_REQ_DEPTH);
$display("[zint] map: COLOR 0x%0h..0x%0h Z 0x%0h..0x%0h", COLBASE, COLBASE+COL_TOP, ZBASE, ZBASE+Z_TOP);
if (errors==0) $display("[tb_top_psmct32_sh3_zint640_shared] PASS"); else $display("[tb_top_psmct32_sh3_zint640_shared] FAIL");
if (frag_fh != 0) $fclose(frag_fh);
$finish;
end
initial begin #800000000; $error("[tb_top_psmct32_sh3_zint640_shared] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_zint640_shared
+469
View File
@@ -0,0 +1,469 @@
// retroDE_ps2 — tb_top_psmct32_sh3_zint640c12 (Ch360 — four-epoch real-raster persistent-Z integration)
//
// Codex's pre-fit gate: prove the persistent-Z ROP through the SAME path the de25 top wires (GS_SH3_LPDDR_FB_Z):
// REAL demo (feeder + raster, authz sh3_zs640c12 NATIVE-640x480 strong-reject scene) -> gs_lpddr_zc_emit -> REAL de25 arbiters
// (gs_lpddr_wr_arb s0=color / s2=Z-write ; gs_lpddr_rd_arb s3=Z-read) -> ONE shared behavioral LPDDR
// with randomized backpressure. Unlike tb_top_psmct32_sh3_zarb (which fed a handshake-throttled trace),
// the raster CANNOT honor g_ready, so this is the gate that catches request-FIFO fragment DROPS.
//
// Asserts (Codex list):
// * final LPDDR color + Z == independent clamp16 persistent-Z scoreboard over the ACTUAL emitted fragments;
// * zero fragment drops (g_valid && !g_ready) — the whole reason this sim exists;
// * exactly one ordered scene marker (sh3_fb_flush) per accepted GO; no fragment after a marker within a scene;
// * Z cache invalidated (clear_done) after preclear and before the first GO; Z persists across epochs;
// * a FRESH ordered frame_drained per epoch; scanout never referenced during render (rd_arb s0/s1/s2 idle);
// * merged AW only in color/Z ranges, merged AR only in Z range (disjoint-map monitor).
// LOCAL/gitignored fixtures; skip-guard if absent.
`timescale 1ns/1ps
module tb_top_psmct32_sh3_zint640c12;
`include "sh3_zs640c12_params.vh" // FBPXW=640, FBH=480, N_EPOCHS=4, EPk_CRC/EPk_NTRIS, TEX_*, ...
localparam int W = FBPXW, H = FBH;
localparam int NPX = W*H;
localparam int NEP = N_EPOCHS;
localparam [31:0] COLBASE = 32'h0000_0000, ZBASE = 32'h0014_0000;
localparam int COL_TOP = NPX*4; // color region byte-size (disjoint below ZBASE)
localparam int Z_TOP = NPX*2; // Z region byte-size
localparam int MEMBEATS = 65536; // shared LPDDR: covers ZBASE + Z_TOP
// per-epoch expected CRC / records
// Ch360 — four epochs share one texture: EPk_REUSE=1 means the epoch runs on the resident cache without a fill.
logic [31:0] EP_CRC [0:3]; int EP_REC [0:3]; bit EP_REUSE [0:3]; int fills;
initial begin
EP_CRC[0]=EP0_CRC; EP_CRC[1]=EP1_CRC; EP_CRC[2]=EP2_CRC; EP_CRC[3]=EP3_CRC;
EP_REC[0]=EP0_NTRIS; EP_REC[1]=EP1_NTRIS; EP_REC[2]=EP2_NTRIS; EP_REC[3]=EP3_NTRIS;
EP_REUSE[0]=EP0_REUSE; EP_REUSE[1]=EP1_REUSE; EP_REUSE[2]=EP2_REUSE; EP_REUSE[3]=EP3_REUSE;
fills=0;
end
`ifdef C12_REQ_DEPTH
localparam int TB_REQ_DEPTH = `C12_REQ_DEPTH; // Ch359 sim-only depth-sweep variant (Codex-authorized)
`else
localparam int TB_REQ_DEPTH = 1024; // Ch359 production depth (Codex: cold-Z burst peaks at 478)
`endif
logic clk=0; always #5 clk=~clk;
logic emif_clk=0; always #2 emif_clk=~emif_clk;
logic rst_n; logic fb_commit=0; always #13 fb_commit=~fb_commit;
int errors; initial errors=0;
// clamp16 (vendored PCSX2 PSMZ16S source-Z saturation)
function automatic logic [15:0] cl16(input logic [31:0] z); cl16=(|z[31:16])?16'hFFFF:z[15:0]; endfunction
// ===== bram-top (FB_LPDDR_ONLY, AUTOSTART off), 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;
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)
);
// texture cache + behavioral texture LPDDR (reloaded per epoch)
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];
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: one ordered scene marker (sh3_fb_flush) per accepted GO =====
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
// ===== gs_lpddr_zc_emit — Z-then-color emit, fed by the REAL raster (NO handshake) =====
logic zc_enable, clear_start, clear_done, frame_drained;
wire [15:0] g_zq = cl16(flush_z_w);
wire frag_v = flush_emit_w && (flush_psm_w==6'h00);
wire g_valid = frag_v || fb_flush;
wire g_scene = fb_flush;
wire [11:0] g_x = flush_x_w, g_y = flush_y_w;
wire [31:0] g_color = flush_color32_w;
wire g_ready;
// Z AXI
logic [31:0] z_araddr; logic [7:0] z_arlen; logic [2:0] z_arsize; logic [1:0] z_arburst; logic z_arvalid, z_arready;
logic [255:0] z_rdata; logic [1:0] z_rresp; logic z_rlast, z_rvalid, z_rready;
logic [31:0] z_awaddr; logic [7:0] z_awlen; logic [2:0] z_awsize; logic [1:0] z_awburst; logic z_awvalid, z_awready;
logic [255:0] z_wdata; logic [31:0] z_wstrb; logic z_wlast, z_wvalid, z_wready; logic z_bvalid, z_bready; logic [1:0] z_bresp;
// Color AXI
logic [31:0] c_awaddr; logic [7:0] c_awlen; logic [2:0] c_awsize; logic [1:0] c_awburst; logic c_awvalid, c_awready;
logic [255:0] c_wdata; logic [31:0] c_wstrb; logic c_wlast, c_wvalid, c_wready; logic c_bvalid, c_bready; logic [1:0] c_bresp;
logic [31:0] z_beats_read, z_beats_written, c_beats_written, col_ovf, bresp_err; logic zc_idle;
gs_lpddr_zc_emit #(.COLBASE(COLBASE), .ZBASE(ZBASE), .FB_PXW(W), .FB_H(H), .REQ_DEPTH(TB_REQ_DEPTH), .COL_DEPTH(128)) u_zc (
.gs_clk(clk), .gs_rst_n(rst_n), .enable(zc_enable),
.g_valid(g_valid), .g_ready(g_ready), .g_x(g_x), .g_y(g_y), .g_zq(g_zq), .g_zmsk(1'b0),
.g_ztest(1'b1), .g_ztst(2'd2), .g_color(g_color), .g_be(4'hF), .g_scene(g_scene),
.axi_clk(emif_clk), .axi_rst_n(rst_n), .clear_start(clear_start), .clear_done(clear_done), .frame_drained(frame_drained),
.z_araddr(z_araddr), .z_arlen(z_arlen), .z_arsize(z_arsize), .z_arburst(z_arburst), .z_arvalid(z_arvalid), .z_arready(z_arready),
.z_rdata(z_rdata), .z_rresp(z_rresp), .z_rlast(z_rlast), .z_rvalid(z_rvalid), .z_rready(z_rready),
.z_awaddr(z_awaddr), .z_awlen(z_awlen), .z_awsize(z_awsize), .z_awburst(z_awburst), .z_awvalid(z_awvalid), .z_awready(z_awready),
.z_wdata(z_wdata), .z_wstrb(z_wstrb), .z_wlast(z_wlast), .z_wvalid(z_wvalid), .z_wready(z_wready),
.z_bvalid(z_bvalid), .z_bready(z_bready), .z_bresp(z_bresp),
.c_awaddr(c_awaddr), .c_awlen(c_awlen), .c_awsize(c_awsize), .c_awburst(c_awburst), .c_awvalid(c_awvalid), .c_awready(c_awready),
.c_wdata(c_wdata), .c_wstrb(c_wstrb), .c_wlast(c_wlast), .c_wvalid(c_wvalid), .c_wready(c_wready),
.c_bvalid(c_bvalid), .c_bready(c_bready), .c_bresp(c_bresp),
.z_beats_read(z_beats_read), .z_beats_written(z_beats_written), .c_beats_written(c_beats_written),
.col_ovf(col_ovf), .bresp_err(bresp_err), .idle(zc_idle)
);
// Ch359 DIAGNOSTIC — request-FIFO occupancy high-water (write-domain binary pointers, hierarchical):
// sizes the burst that overflows REQ_DEPTH=256 (would a deeper FIFO absorb it, or is the ep0 dirty-miss
// streak service-rate-bound?). Sampled on the write clock; wbin-rbin_sampled is a safe over-estimate.
// (Codex correction: occupancy must be computed ENTIRELY in the write clock domain — wbin vs the
// write-domain-SYNCHRONIZED read pointer rgray_s2 converted gray->binary — with the modulus derived from the
// selected depth, not a hard-coded 2*256.)
int req_hiwater; logic hw_clear;
function automatic int g2b(input int g);
int b; b=g; b^=b>>1; b^=b>>2; b^=b>>4; b^=b>>8; b^=b>>16; return b;
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) req_hiwater <= 0;
else if (hw_clear) req_hiwater <= 0;
else begin
int occ;
occ = (int'(u_zc.u_req.wbin) - g2b(int'(u_zc.u_req.rgray_s2))) % (2*TB_REQ_DEPTH);
if (occ < 0) occ += 2*TB_REQ_DEPTH;
if (occ > req_hiwater) req_hiwater <= occ;
end
end
// fragment-drop guard (design clk): the request FIFO silently drops when g_ready is low
int g_drops; int frags_fed;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin g_drops<=0; frags_fed<=0; end
else begin
if (frag_v && !g_ready) g_drops<=g_drops+1;
if (frag_v && g_ready) frags_fed<=frags_fed+1;
end
end
// clear ordering: clear_done must rise before the first GO. no psm0 fragment before clear_done.
logic saw_clear_done=0, first_go_done=0; int pre_clear_frags;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin saw_clear_done<=0; pre_clear_frags<=0; end
else begin
if (clear_done) saw_clear_done<=1;
if (frag_v && !saw_clear_done) pre_clear_frags<=pre_clear_frags+1;
end
end
// ===== REAL de25 write arbiter: s0=color, s2=Z-write ; s1/s3 inert =====
logic [29:0] m_awaddr; logic [1:0] m_awburst; logic [6:0] m_awid; logic [7:0] m_awlen; logic [2:0] m_awsize;
logic m_awvalid, m_awready; logic [255:0] m_wdata; logic [31:0] m_wstrb; logic m_wlast, m_wvalid, m_wready;
logic m_bvalid, m_bready; logic [1:0] m_bresp;
gs_lpddr_wr_arb u_wr_arb (
.clk(emif_clk), .rst_n(rst_n),
.s0_awaddr(c_awaddr[29:0]), .s0_awburst(c_awburst), .s0_awid(7'd0), .s0_awlen(c_awlen), .s0_awsize(c_awsize),
.s0_awvalid(c_awvalid), .s0_awready(c_awready), .s0_wdata(c_wdata), .s0_wstrb(c_wstrb), .s0_wlast(c_wlast),
.s0_wvalid(c_wvalid), .s0_wready(c_wready), .s0_bresp(c_bresp), .s0_bvalid(c_bvalid), .s0_bready(c_bready),
.s1_awaddr(30'd0), .s1_awburst(2'b01), .s1_awid(7'd1), .s1_awlen(8'd0), .s1_awsize(3'b101),
.s1_awvalid(1'b0), .s1_awready(), .s1_wdata(256'd0), .s1_wstrb(32'd0), .s1_wlast(1'b0),
.s1_wvalid(1'b0), .s1_wready(), .s1_bresp(), .s1_bvalid(), .s1_bready(1'b0),
.s2_awaddr(z_awaddr[29:0]), .s2_awburst(z_awburst), .s2_awid(7'd2), .s2_awlen(z_awlen), .s2_awsize(z_awsize),
.s2_awvalid(z_awvalid), .s2_awready(z_awready), .s2_wdata(z_wdata), .s2_wstrb(z_wstrb), .s2_wlast(z_wlast),
.s2_wvalid(z_wvalid), .s2_wready(z_wready), .s2_bresp(z_bresp), .s2_bvalid(z_bvalid), .s2_bready(z_bready),
.s3_awaddr(30'd0), .s3_awburst(2'b01), .s3_awid(7'd3), .s3_awlen(8'd0), .s3_awsize(3'b101),
.s3_awvalid(1'b0), .s3_awready(), .s3_wdata(256'd0), .s3_wstrb(32'd0), .s3_wlast(1'b0),
.s3_wvalid(1'b0), .s3_wready(), .s3_bresp(), .s3_bvalid(), .s3_bready(1'b0),
.m_awaddr(m_awaddr), .m_awburst(m_awburst), .m_awid(m_awid), .m_awlen(m_awlen), .m_awsize(m_awsize),
.m_awvalid(m_awvalid), .m_awready(m_awready), .m_wdata(m_wdata), .m_wstrb(m_wstrb), .m_wlast(m_wlast),
.m_wvalid(m_wvalid), .m_wready(m_wready), .m_bvalid(m_bvalid), .m_bready(m_bready), .m_bresp(m_bresp)
);
// ===== REAL de25 read arbiter: s3=Z-read (mirrors de25 reload port) ; s0/s1/s2 idle during render =====
logic [29:0] m_araddr; logic [1:0] m_arburst; logic [6:0] m_arid; logic [7:0] m_arlen; logic [2:0] m_arsize;
logic m_arvalid, m_arready; logic [255:0] m_rdata; logic [1:0] m_rresp; logic m_rlast, m_rvalid, m_rready;
gs_lpddr_rd_arb u_rd_arb (
.clk(emif_clk), .rst_n(rst_n),
.s0_araddr(30'd0), .s0_arburst(2'b01), .s0_arid(7'd0), .s0_arlen(8'd0), .s0_arsize(3'b101),
.s0_arvalid(1'b0), .s0_arready(), .s0_rdata(), .s0_rresp(), .s0_rlast(), .s0_rvalid(), .s0_rready(1'b0),
.s1_araddr(30'd0), .s1_arburst(2'b01), .s1_arid(7'd1), .s1_arlen(8'd0), .s1_arsize(3'b101),
.s1_arvalid(1'b0), .s1_arready(), .s1_rdata(), .s1_rresp(), .s1_rlast(), .s1_rvalid(), .s1_rready(1'b0),
.s2_araddr(30'd0), .s2_arburst(2'b01), .s2_arid(7'd2), .s2_arlen(8'd0), .s2_arsize(3'b101),
.s2_arvalid(1'b0), .s2_arready(), .s2_rdata(), .s2_rresp(), .s2_rlast(), .s2_rvalid(), .s2_rready(1'b0),
.s3_araddr(z_araddr[29:0]), .s3_arburst(z_arburst), .s3_arid(7'd3), .s3_arlen(z_arlen), .s3_arsize(z_arsize),
.s3_arvalid(z_arvalid), .s3_arready(z_arready), .s3_rdata(z_rdata), .s3_rresp(z_rresp), .s3_rlast(z_rlast),
.s3_rvalid(z_rvalid), .s3_rready(z_rready),
.m_araddr(m_araddr), .m_arburst(m_arburst), .m_arid(m_arid), .m_arlen(m_arlen), .m_arsize(m_arsize),
.m_arvalid(m_arvalid), .m_arready(m_arready), .m_rdata(m_rdata), .m_rresp(m_rresp), .m_rlast(m_rlast),
.m_rvalid(m_rvalid), .m_rready(m_rready)
);
logic [15:0] lf=16'hF00D; always_ff @(posedge emif_clk) lf<={lf[14:0], lf[15]^lf[13]^lf[12]^lf[10]};
// ===== ONE shared behavioral LPDDR (write + read), address-decoded, random backpressure =====
logic [255:0] shmem [0:MEMBEATS-1];
logic [29:0] wa; logic [255:0] wd; logic [31:0] ws; logic aw_s, w_s; logic [3:0] bd; logic bp;
logic [29:0] ra; logic rp; logic [3:0] rd_dly;
always_ff @(posedge emif_clk or negedge rst_n) begin
if(!rst_n) begin
m_awready<=0; m_wready<=0; m_bvalid<=0; m_bresp<=0; aw_s<=0; w_s<=0; bp<=0; bd<=0;
m_arready<=0; m_rvalid<=0; m_rlast<=0; m_rresp<=0; m_rdata<=0; rp<=0; rd_dly<=0;
end else begin
m_awready<=(!aw_s)?lf[0]:1'b0; m_wready<=(!w_s)?lf[1]:1'b0;
if(m_awvalid&&m_awready) begin wa<=m_awaddr; aw_s<=1; m_awready<=0; end
if(m_wvalid&&m_wready) begin wd<=m_wdata; ws<=m_wstrb; w_s<=1; m_wready<=0; end
if(aw_s&&w_s&&!bp&&!m_bvalid) begin
for(int bb=0;bb<32;bb++) if(ws[bb]) shmem[wa>>5][bb*8+:8]<=wd[bb*8+:8];
bp<=1; bd<={1'b0,lf[4:2]};
end
if(bp&&!m_bvalid) begin if(bd==0) begin m_bvalid<=1;m_bresp<=0;bp<=0;aw_s<=0;w_s<=0; end else bd<=bd-1; end
if(m_bvalid&&m_bready) m_bvalid<=0;
m_arready<=(!rp&&!m_rvalid)?lf[8]:1'b0;
if(m_arvalid&&m_arready) begin ra<=m_araddr; rp<=1; rd_dly<={1'b0,lf[7:5]}; m_arready<=0; end
if(rp&&!m_rvalid) begin if(rd_dly==0) begin m_rdata<=shmem[ra>>5]; m_rresp<=0; m_rvalid<=1; m_rlast<=1; rp<=0; end else rd_dly<=rd_dly-1; end
if(m_rvalid&&m_rready) begin m_rvalid<=0; m_rlast<=0; end
end
end
// disjoint-map monitors on the MERGED master streams
always_ff @(posedge emif_clk) if(rst_n) begin
if(m_awvalid) begin
if(!((m_awaddr<30'(COLBASE+COL_TOP)) || (m_awaddr>=30'(ZBASE) && m_awaddr<30'(ZBASE+Z_TOP))))
begin $error("[zint] merged AW %h not in color/Z range",m_awaddr); errors++; end
end
if(m_arvalid && (m_araddr<30'(ZBASE) || m_araddr>=30'(ZBASE+Z_TOP))) begin $error("[zint] merged AR %h not in Z range",m_araddr); errors++; end
end
// ===== clamp16 persistent-Z SCOREBOARD over the ACTUAL emitted fragments (design clk) =====
// Mirrors gs_lpddr_z_rmw: GEQUAL vs stored, Z_CLEAR=0. Persists across epochs. Gated active after clear_done.
logic [15:0] sb_z [0:NPX-1];
logic [31:0] sb_col[0:NPX-1];
logic sb_wr [0:NPX-1];
integer sb_frag, sb_pass;
// no-fragment-after-marker: a fragment must not appear in the same scene after fb_flush (before next GO)
logic scene_ended=0; int frag_after_marker;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sb_frag<=0; sb_pass<=0; scene_ended<=0; frag_after_marker<=0;
end else begin
if (feeder_go_tb && feeder_ready_tb) scene_ended<=0; // new scene opens
if (fb_flush) scene_ended<=1; // ordered marker closes the scene
if (frag_v && saw_clear_done) begin
int o; logic [15:0] zq;
if (scene_ended) frag_after_marker<=frag_after_marker+1; // fragment after this scene's marker = ordering bug
o = g_y*W + g_x; zq = cl16(flush_z_w);
sb_frag<=sb_frag+1;
if (o>=0 && o<NPX) begin
if (zq >= sb_z[o]) begin sb_z[o]<=zq; sb_col[o]<=flush_color32_w; sb_wr[o]<=1'b1; sb_pass<=sb_pass+1; end
end
end
end
end
// FRESH-DRAIN observer (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<=frame_drained;
if (frame_drained && !fd_q) fd_rises<=fd_rises+1;
if (!frame_drained && fd_q) fd_falls<=fd_falls+1;
end
end
// clear_start: mirror de25 — pulse once at the writer ARM (after preclear, before first GO)
logic wr_arm=0, arm_e1, arm_e2, arm_e3;
always_ff @(posedge emif_clk or negedge rst_n) begin
if (!rst_n) begin arm_e1<=0; arm_e2<=0; arm_e3<=0; clear_start<=0; end
else begin arm_e1<=wr_arm; arm_e2<=arm_e1; arm_e3<=arm_e2; clear_start<=(arm_e2 && !arm_e3); end
end
// ---- feeder staging stream (write port), per epoch ----
logic [63:0] stg_buf [0:STG_WORDS-1];
task automatic stream_list(input int k);
string fn; fn=$sformatf("../../data/top_psmct32_raster_demo/feeder_sh3_zs640c12%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);
if (dut.g_feeder.feeder_stg[0][31:0] !== stg_buf[0][31:0]) begin
$error("[zint] epoch %0d: staged word0=%08x exp %08x", k, dut.g_feeder.feeder_stg[0][31:0], stg_buf[0][31:0]); errors++; end
endtask
task automatic fill_cache(input int k, input logic [31:0] exp_crc);
int d=0;
fills=fills+1;
@(posedge clk) fill_start<=1'b1; @(posedge clk) fill_start<=1'b0;
d=0; while (fill_done && d<1000) begin @(posedge clk); d++; end
d=0; while (!fill_done && d<400000) begin @(posedge clk); d++; end
if (!fill_done) begin $error("[zint] fill %0d: fill_done never rose", k); errors++; end
if (fill_crc_w!==exp_crc) begin $error("[zint] fill %0d: crc=%08x exp %08x", k, fill_crc_w, exp_crc); errors++; end
if (tex_rd_errs!==0) begin $error("[zint] fill %0d: rd_errs=%0d", k, tex_rd_errs); errors++; end
endtask
// one-scene runner: GO, wait render+drain, REQUIRE a FRESH ordered frame_drained
task automatic run_scene(input int k, input int exp_records);
int r0, f0, d=0; logic fd_before; int gd0, fm0;
r0=fd_rises; f0=fd_falls; fd_before=frame_drained; gd0=g_drops; fm0=frag_after_marker;
@(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("[zint] epoch %0d: feeder_ready did not drop after GO", k); errors++; end
if (fd_before) begin
d=0; while (fd_falls==f0 && d<6000000) begin @(posedge emif_clk); d++; end
if (fd_falls==f0) begin $error("[zint] epoch %0d: frame_drained never fell from stale high", k); errors++; end
end
d=0; while (fd_rises<=r0 && d<6000000) begin @(posedge emif_clk); d++; end
if (fd_rises<=r0) begin $error("[zint] epoch %0d: frame_drained never rose", k); errors++; end
repeat(100) @(posedge clk);
if (feeder_records_w!==exp_records) begin $error("[zint] epoch %0d: records=%0d exp %0d", k, feeder_records_w, exp_records); errors++; end
if (col_ovf!==0 || bresp_err!==0) begin $error("[zint] epoch %0d: col_ovf=%0d bresp_err=%0d", k, col_ovf, bresp_err); errors++; end
if (g_drops!==gd0) begin $error("[zint] epoch %0d: %0d FRAGMENT DROPS (request FIFO overflow)", k, g_drops-gd0); errors++; end
if (frag_after_marker!==fm0) begin $error("[zint] epoch %0d: %0d fragment(s) after scene marker (ordering)", k, frag_after_marker-fm0); errors++; end
$display("[zint] epoch %0d: fresh drain (f %0d->%0d, r %0d->%0d) records=%0d drops=%0d fed=%0d pass=%0d",
k, f0, fd_falls, r0, fd_rises, feeder_records_w, g_drops, frags_fed, sb_pass);
endtask
task automatic run_epoch(input int k);
string fn; fn=$sformatf("../../data/top_psmct32_raster_demo/sh3_zs640c12%0d_tex_lpddr.mem", k);
if (!EP_REUSE[k]) begin
$readmemh(fn, lpddr_mem);
fill_cache(k, EP_CRC[k]);
end else begin
// Ch359 — EXPLICIT REUSE: no reload, no fill pulse. Fail-closed residency: the fill CRC register is
// only rewritten by a fill, so it must still hold the prior epoch's verified CRC (same shared texture).
if (fill_crc_w!==EP_CRC[k]) begin $error("%s epoch %0d: REUSE but resident crc=%08x exp %08x — residency broken", "[zint]", k, fill_crc_w, EP_CRC[k]); errors++; end
else $display("%s epoch %0d: REUSE resident texture (crc=0x%08x, no fill)", "[zint]", k, fill_crc_w);
end
stream_list(k);
run_scene(k, EP_REC[k]);
endtask
initial begin
errors=0; feeder_go_tb=1'b0; fill_start=1'b0; wr_arm=0; zc_enable=1'b1; stg_we=0; stg_waddr=0; stg_wdata=0; hw_clear=1'b0;
for (int i=0;i<NPX;i++) begin sb_z[i]=16'h0000; sb_col[i]=32'd0; sb_wr[i]=1'b0; end
for (int b=0;b<MEMBEATS;b++) shmem[b]=256'd0; // PRECLEAR color+Z EXACTLY ONCE
// skip-guard: probe a fixture (via $fopen — avoids the $readmemh range warning)
begin int pfh; pfh=$fopen("../../data/top_psmct32_raster_demo/sh3_zs640c120_tex_lpddr.mem","r");
if (pfh==0) begin $display("[tb_top_psmct32_sh3_zint640c12] SKIP — sh3_zs640c12*.mem absent (run gs_make_sh3_scheduler_fixture.py --authz --fb640 --tag zs640c12 --emit)"); $finish; end
$fclose(pfh);
end
rst_n=1'b0; core_go=1'b0; repeat(6) @(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);
wait (feeder_ready_tb==1'b1); repeat(50) @(posedge clk);
// ARM (-> clear_start) AFTER preclear, BEFORE first GO. z_rmw's clear_start writes Z_CLEAR to EVERY Z beat
// (NBEATS=NPX/16, through wr_arb s2 under random backpressure) then asserts clear_done -> it is the authoritative
// Z preclear. Wait for it (the board host likewise gates the first GO on the clear_done status bit).
wr_arm<=1'b1;
begin int d=0; while (!clear_done && d<600000) begin @(posedge emif_clk); d++; end end
if (!clear_done) begin $error("[zint] clear_done never rose after ARM (Z preclear did not finish)"); errors++; end
else $display("[zint] Z preclear complete (clear_done), z_beats_written(clear)=%0d", z_beats_written);
repeat(40) @(posedge clk);
for (int k=0;k<NEP;k++) begin
int fed0, dr0;
fed0=frags_fed; dr0=g_drops;
run_epoch(k);
$display("[zint][sweep] depth=%0d epoch %0d: accepted=%0d drops=%0d req_hiwater=%0d",
TB_REQ_DEPTH, k, frags_fed-fed0, g_drops-dr0, req_hiwater);
@(negedge clk); hw_clear<=1'b1; @(negedge clk); hw_clear<=1'b0; // per-epoch high-water
end
if (pre_clear_frags!==0) begin $error("[zint] %0d psm0 fragment(s) before clear_done", pre_clear_frags); errors++; end
if (fd_rises<NEP) begin $error("[zint] expected %0d ordered drains: rises=%0d", NEP, fd_rises); errors++; end
if (fd_falls<NEP-1) begin $error("[zint] epochs after the first never cleared stale (falls=%0d)", fd_falls); errors++; end
if (eof_count!==NEP)begin $error("[zint] scene markers=%0d != accepted GOs=%0d", eof_count, NEP); errors++; end
if (g_drops!==0) begin $error("[zint] TOTAL %0d fragment drops", g_drops); errors++; end
// Ch359 — the WHOLE run must perform exactly ONE verified texture fill (epoch 1 reuses the resident cache)
if (fills!==1) begin $error("[zint] expected exactly ONE texture fill, got %0d (residency contract)", fills); errors++; end
// ===== final memory compare: shared LPDDR color + Z == scoreboard =====
begin
int zmis, cmis, zchk, cchk; zmis=0; cmis=0; zchk=0; cchk=0;
for (int o=0;o<NPX;o++) begin
logic [15:0] zs; logic [31:0] cs; int zb, zl;
zb=(ZBASE + o*2)>>5; zl=o&15; zs=shmem[zb][zl*16+:16];
zchk++;
if (zs !== sb_z[o]) begin if (zmis<10) $error("[zint] Z px%0d got %04x exp %04x", o, zs, sb_z[o]); zmis++; end
if (sb_wr[o]) begin
cs=shmem[o>>3][(o[2:0])*32+:32]; cchk++;
if (cs[23:0] !== sb_col[o][23:0]) begin if (cmis<10) $error("[zint] COL px%0d got %06x exp %06x", o, cs[23:0], sb_col[o][23:0]); cmis++; end
end
end
$display("[zint] final compare: Z %0d/%0d mismatch, COLOR %0d/%0d mismatch (written px=%0d)", zmis, zchk, cmis, cchk, cchk);
end
begin
string fdump;
if ($value$plusargs("FBDUMP=%s", fdump)) begin
int fh;
fh = $fopen(fdump, "w");
if (fh == 0) begin
$error("[zint] could not open FBDUMP '%s'", fdump);
errors++;
end else begin
for (int o=0;o<NPX;o++) begin
logic [31:0] cs;
cs = shmem[o>>3][(o[2:0])*32+:32];
$fdisplay(fh, "%08x", cs);
end
$fclose(fh);
$display("[zint] dumped final COLOR FB (%0dx%0d) -> %s", W, H, fdump);
end
end
end
$display("[tb_top_psmct32_sh3_zint640c12] fed=%0d pass=%0d drops=%0d markers=%0d drains(r/f)=%0d/%0d col_ovf=%0d bresp_err=%0d errors=%0d",
frags_fed, sb_pass, g_drops, eof_count, fd_rises, fd_falls, col_ovf, bresp_err, errors);
$display("[zint] req-FIFO depth=%0d (diagnostic sweep variant when != 1024)", TB_REQ_DEPTH);
$display("[zint] map: COLOR 0x%0h..0x%0h Z 0x%0h..0x%0h", COLBASE, COLBASE+COL_TOP, ZBASE, ZBASE+Z_TOP);
if (errors==0) $display("[tb_top_psmct32_sh3_zint640c12] PASS"); else $display("[tb_top_psmct32_sh3_zint640c12] FAIL");
$finish;
end
initial begin #400000000; $error("[tb_top_psmct32_sh3_zint640c12] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_zint640c12
+469
View File
@@ -0,0 +1,469 @@
// retroDE_ps2 — tb_top_psmct32_sh3_zint640c6 (Ch358 — REAL-RASTER persistent-Z board integration sim at NATIVE 640x480)
//
// Codex's pre-fit gate: prove the persistent-Z ROP through the SAME path the de25 top wires (GS_SH3_LPDDR_FB_Z):
// REAL demo (feeder + raster, authz sh3_zs640c6 NATIVE-640x480 strong-reject scene) -> gs_lpddr_zc_emit -> REAL de25 arbiters
// (gs_lpddr_wr_arb s0=color / s2=Z-write ; gs_lpddr_rd_arb s3=Z-read) -> ONE shared behavioral LPDDR
// with randomized backpressure. Unlike tb_top_psmct32_sh3_zarb (which fed a handshake-throttled trace),
// the raster CANNOT honor g_ready, so this is the gate that catches request-FIFO fragment DROPS.
//
// Asserts (Codex list):
// * final LPDDR color + Z == independent clamp16 persistent-Z scoreboard over the ACTUAL emitted fragments;
// * zero fragment drops (g_valid && !g_ready) — the whole reason this sim exists;
// * exactly one ordered scene marker (sh3_fb_flush) per accepted GO; no fragment after a marker within a scene;
// * Z cache invalidated (clear_done) after preclear and before the first GO; Z persists across epochs;
// * a FRESH ordered frame_drained per epoch; scanout never referenced during render (rd_arb s0/s1/s2 idle);
// * merged AW only in color/Z ranges, merged AR only in Z range (disjoint-map monitor).
// LOCAL/gitignored fixtures; skip-guard if absent.
`timescale 1ns/1ps
module tb_top_psmct32_sh3_zint640c6;
`include "sh3_zs640c6_params.vh" // FBPXW=640, FBH=480, N_EPOCHS=3, EPk_CRC/EPk_NTRIS, TEX_*, ...
localparam int W = FBPXW, H = FBH;
localparam int NPX = W*H;
localparam int NEP = N_EPOCHS;
localparam [31:0] COLBASE = 32'h0000_0000, ZBASE = 32'h0014_0000;
localparam int COL_TOP = NPX*4; // color region byte-size (disjoint below ZBASE)
localparam int Z_TOP = NPX*2; // Z region byte-size
localparam int MEMBEATS = 65536; // shared LPDDR: covers ZBASE + Z_TOP
// per-epoch expected CRC / records
// Ch359 — TWO epochs sharing ONE texture: EPk_REUSE=1 means the epoch runs on the RESIDENT cache (no fill).
logic [31:0] EP_CRC [0:1]; int EP_REC [0:1]; bit EP_REUSE [0:1]; int fills;
initial begin
EP_CRC[0]=EP0_CRC; EP_CRC[1]=EP1_CRC;
EP_REC[0]=EP0_NTRIS; EP_REC[1]=EP1_NTRIS;
EP_REUSE[0]=EP0_REUSE; EP_REUSE[1]=EP1_REUSE;
fills=0;
end
`ifdef C6_REQ_DEPTH
localparam int TB_REQ_DEPTH = `C6_REQ_DEPTH; // Ch359 sim-only depth-sweep variant (Codex-authorized)
`else
localparam int TB_REQ_DEPTH = 1024; // Ch359 production depth (Codex: cold-Z burst peaks at 478)
`endif
logic clk=0; always #5 clk=~clk;
logic emif_clk=0; always #2 emif_clk=~emif_clk;
logic rst_n; logic fb_commit=0; always #13 fb_commit=~fb_commit;
int errors; initial errors=0;
// clamp16 (vendored PCSX2 PSMZ16S source-Z saturation)
function automatic logic [15:0] cl16(input logic [31:0] z); cl16=(|z[31:16])?16'hFFFF:z[15:0]; endfunction
// ===== bram-top (FB_LPDDR_ONLY, AUTOSTART off), 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;
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)
);
// texture cache + behavioral texture LPDDR (reloaded per epoch)
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];
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: one ordered scene marker (sh3_fb_flush) per accepted GO =====
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
// ===== gs_lpddr_zc_emit — Z-then-color emit, fed by the REAL raster (NO handshake) =====
logic zc_enable, clear_start, clear_done, frame_drained;
wire [15:0] g_zq = cl16(flush_z_w);
wire frag_v = flush_emit_w && (flush_psm_w==6'h00);
wire g_valid = frag_v || fb_flush;
wire g_scene = fb_flush;
wire [11:0] g_x = flush_x_w, g_y = flush_y_w;
wire [31:0] g_color = flush_color32_w;
wire g_ready;
// Z AXI
logic [31:0] z_araddr; logic [7:0] z_arlen; logic [2:0] z_arsize; logic [1:0] z_arburst; logic z_arvalid, z_arready;
logic [255:0] z_rdata; logic [1:0] z_rresp; logic z_rlast, z_rvalid, z_rready;
logic [31:0] z_awaddr; logic [7:0] z_awlen; logic [2:0] z_awsize; logic [1:0] z_awburst; logic z_awvalid, z_awready;
logic [255:0] z_wdata; logic [31:0] z_wstrb; logic z_wlast, z_wvalid, z_wready; logic z_bvalid, z_bready; logic [1:0] z_bresp;
// Color AXI
logic [31:0] c_awaddr; logic [7:0] c_awlen; logic [2:0] c_awsize; logic [1:0] c_awburst; logic c_awvalid, c_awready;
logic [255:0] c_wdata; logic [31:0] c_wstrb; logic c_wlast, c_wvalid, c_wready; logic c_bvalid, c_bready; logic [1:0] c_bresp;
logic [31:0] z_beats_read, z_beats_written, c_beats_written, col_ovf, bresp_err; logic zc_idle;
gs_lpddr_zc_emit #(.COLBASE(COLBASE), .ZBASE(ZBASE), .FB_PXW(W), .FB_H(H), .REQ_DEPTH(TB_REQ_DEPTH), .COL_DEPTH(128)) u_zc (
.gs_clk(clk), .gs_rst_n(rst_n), .enable(zc_enable),
.g_valid(g_valid), .g_ready(g_ready), .g_x(g_x), .g_y(g_y), .g_zq(g_zq), .g_zmsk(1'b0),
.g_ztest(1'b1), .g_ztst(2'd2), .g_color(g_color), .g_be(4'hF), .g_scene(g_scene),
.axi_clk(emif_clk), .axi_rst_n(rst_n), .clear_start(clear_start), .clear_done(clear_done), .frame_drained(frame_drained),
.z_araddr(z_araddr), .z_arlen(z_arlen), .z_arsize(z_arsize), .z_arburst(z_arburst), .z_arvalid(z_arvalid), .z_arready(z_arready),
.z_rdata(z_rdata), .z_rresp(z_rresp), .z_rlast(z_rlast), .z_rvalid(z_rvalid), .z_rready(z_rready),
.z_awaddr(z_awaddr), .z_awlen(z_awlen), .z_awsize(z_awsize), .z_awburst(z_awburst), .z_awvalid(z_awvalid), .z_awready(z_awready),
.z_wdata(z_wdata), .z_wstrb(z_wstrb), .z_wlast(z_wlast), .z_wvalid(z_wvalid), .z_wready(z_wready),
.z_bvalid(z_bvalid), .z_bready(z_bready), .z_bresp(z_bresp),
.c_awaddr(c_awaddr), .c_awlen(c_awlen), .c_awsize(c_awsize), .c_awburst(c_awburst), .c_awvalid(c_awvalid), .c_awready(c_awready),
.c_wdata(c_wdata), .c_wstrb(c_wstrb), .c_wlast(c_wlast), .c_wvalid(c_wvalid), .c_wready(c_wready),
.c_bvalid(c_bvalid), .c_bready(c_bready), .c_bresp(c_bresp),
.z_beats_read(z_beats_read), .z_beats_written(z_beats_written), .c_beats_written(c_beats_written),
.col_ovf(col_ovf), .bresp_err(bresp_err), .idle(zc_idle)
);
// Ch359 DIAGNOSTIC — request-FIFO occupancy high-water (write-domain binary pointers, hierarchical):
// sizes the burst that overflows REQ_DEPTH=256 (would a deeper FIFO absorb it, or is the ep0 dirty-miss
// streak service-rate-bound?). Sampled on the write clock; wbin-rbin_sampled is a safe over-estimate.
// (Codex correction: occupancy must be computed ENTIRELY in the write clock domain — wbin vs the
// write-domain-SYNCHRONIZED read pointer rgray_s2 converted gray->binary — with the modulus derived from the
// selected depth, not a hard-coded 2*256.)
int req_hiwater; logic hw_clear;
function automatic int g2b(input int g);
int b; b=g; b^=b>>1; b^=b>>2; b^=b>>4; b^=b>>8; b^=b>>16; return b;
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) req_hiwater <= 0;
else if (hw_clear) req_hiwater <= 0;
else begin
int occ;
occ = (int'(u_zc.u_req.wbin) - g2b(int'(u_zc.u_req.rgray_s2))) % (2*TB_REQ_DEPTH);
if (occ < 0) occ += 2*TB_REQ_DEPTH;
if (occ > req_hiwater) req_hiwater <= occ;
end
end
// fragment-drop guard (design clk): the request FIFO silently drops when g_ready is low
int g_drops; int frags_fed;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin g_drops<=0; frags_fed<=0; end
else begin
if (frag_v && !g_ready) g_drops<=g_drops+1;
if (frag_v && g_ready) frags_fed<=frags_fed+1;
end
end
// clear ordering: clear_done must rise before the first GO. no psm0 fragment before clear_done.
logic saw_clear_done=0, first_go_done=0; int pre_clear_frags;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin saw_clear_done<=0; pre_clear_frags<=0; end
else begin
if (clear_done) saw_clear_done<=1;
if (frag_v && !saw_clear_done) pre_clear_frags<=pre_clear_frags+1;
end
end
// ===== REAL de25 write arbiter: s0=color, s2=Z-write ; s1/s3 inert =====
logic [29:0] m_awaddr; logic [1:0] m_awburst; logic [6:0] m_awid; logic [7:0] m_awlen; logic [2:0] m_awsize;
logic m_awvalid, m_awready; logic [255:0] m_wdata; logic [31:0] m_wstrb; logic m_wlast, m_wvalid, m_wready;
logic m_bvalid, m_bready; logic [1:0] m_bresp;
gs_lpddr_wr_arb u_wr_arb (
.clk(emif_clk), .rst_n(rst_n),
.s0_awaddr(c_awaddr[29:0]), .s0_awburst(c_awburst), .s0_awid(7'd0), .s0_awlen(c_awlen), .s0_awsize(c_awsize),
.s0_awvalid(c_awvalid), .s0_awready(c_awready), .s0_wdata(c_wdata), .s0_wstrb(c_wstrb), .s0_wlast(c_wlast),
.s0_wvalid(c_wvalid), .s0_wready(c_wready), .s0_bresp(c_bresp), .s0_bvalid(c_bvalid), .s0_bready(c_bready),
.s1_awaddr(30'd0), .s1_awburst(2'b01), .s1_awid(7'd1), .s1_awlen(8'd0), .s1_awsize(3'b101),
.s1_awvalid(1'b0), .s1_awready(), .s1_wdata(256'd0), .s1_wstrb(32'd0), .s1_wlast(1'b0),
.s1_wvalid(1'b0), .s1_wready(), .s1_bresp(), .s1_bvalid(), .s1_bready(1'b0),
.s2_awaddr(z_awaddr[29:0]), .s2_awburst(z_awburst), .s2_awid(7'd2), .s2_awlen(z_awlen), .s2_awsize(z_awsize),
.s2_awvalid(z_awvalid), .s2_awready(z_awready), .s2_wdata(z_wdata), .s2_wstrb(z_wstrb), .s2_wlast(z_wlast),
.s2_wvalid(z_wvalid), .s2_wready(z_wready), .s2_bresp(z_bresp), .s2_bvalid(z_bvalid), .s2_bready(z_bready),
.s3_awaddr(30'd0), .s3_awburst(2'b01), .s3_awid(7'd3), .s3_awlen(8'd0), .s3_awsize(3'b101),
.s3_awvalid(1'b0), .s3_awready(), .s3_wdata(256'd0), .s3_wstrb(32'd0), .s3_wlast(1'b0),
.s3_wvalid(1'b0), .s3_wready(), .s3_bresp(), .s3_bvalid(), .s3_bready(1'b0),
.m_awaddr(m_awaddr), .m_awburst(m_awburst), .m_awid(m_awid), .m_awlen(m_awlen), .m_awsize(m_awsize),
.m_awvalid(m_awvalid), .m_awready(m_awready), .m_wdata(m_wdata), .m_wstrb(m_wstrb), .m_wlast(m_wlast),
.m_wvalid(m_wvalid), .m_wready(m_wready), .m_bvalid(m_bvalid), .m_bready(m_bready), .m_bresp(m_bresp)
);
// ===== REAL de25 read arbiter: s3=Z-read (mirrors de25 reload port) ; s0/s1/s2 idle during render =====
logic [29:0] m_araddr; logic [1:0] m_arburst; logic [6:0] m_arid; logic [7:0] m_arlen; logic [2:0] m_arsize;
logic m_arvalid, m_arready; logic [255:0] m_rdata; logic [1:0] m_rresp; logic m_rlast, m_rvalid, m_rready;
gs_lpddr_rd_arb u_rd_arb (
.clk(emif_clk), .rst_n(rst_n),
.s0_araddr(30'd0), .s0_arburst(2'b01), .s0_arid(7'd0), .s0_arlen(8'd0), .s0_arsize(3'b101),
.s0_arvalid(1'b0), .s0_arready(), .s0_rdata(), .s0_rresp(), .s0_rlast(), .s0_rvalid(), .s0_rready(1'b0),
.s1_araddr(30'd0), .s1_arburst(2'b01), .s1_arid(7'd1), .s1_arlen(8'd0), .s1_arsize(3'b101),
.s1_arvalid(1'b0), .s1_arready(), .s1_rdata(), .s1_rresp(), .s1_rlast(), .s1_rvalid(), .s1_rready(1'b0),
.s2_araddr(30'd0), .s2_arburst(2'b01), .s2_arid(7'd2), .s2_arlen(8'd0), .s2_arsize(3'b101),
.s2_arvalid(1'b0), .s2_arready(), .s2_rdata(), .s2_rresp(), .s2_rlast(), .s2_rvalid(), .s2_rready(1'b0),
.s3_araddr(z_araddr[29:0]), .s3_arburst(z_arburst), .s3_arid(7'd3), .s3_arlen(z_arlen), .s3_arsize(z_arsize),
.s3_arvalid(z_arvalid), .s3_arready(z_arready), .s3_rdata(z_rdata), .s3_rresp(z_rresp), .s3_rlast(z_rlast),
.s3_rvalid(z_rvalid), .s3_rready(z_rready),
.m_araddr(m_araddr), .m_arburst(m_arburst), .m_arid(m_arid), .m_arlen(m_arlen), .m_arsize(m_arsize),
.m_arvalid(m_arvalid), .m_arready(m_arready), .m_rdata(m_rdata), .m_rresp(m_rresp), .m_rlast(m_rlast),
.m_rvalid(m_rvalid), .m_rready(m_rready)
);
logic [15:0] lf=16'hF00D; always_ff @(posedge emif_clk) lf<={lf[14:0], lf[15]^lf[13]^lf[12]^lf[10]};
// ===== ONE shared behavioral LPDDR (write + read), address-decoded, random backpressure =====
logic [255:0] shmem [0:MEMBEATS-1];
logic [29:0] wa; logic [255:0] wd; logic [31:0] ws; logic aw_s, w_s; logic [3:0] bd; logic bp;
logic [29:0] ra; logic rp; logic [3:0] rd_dly;
always_ff @(posedge emif_clk or negedge rst_n) begin
if(!rst_n) begin
m_awready<=0; m_wready<=0; m_bvalid<=0; m_bresp<=0; aw_s<=0; w_s<=0; bp<=0; bd<=0;
m_arready<=0; m_rvalid<=0; m_rlast<=0; m_rresp<=0; m_rdata<=0; rp<=0; rd_dly<=0;
end else begin
m_awready<=(!aw_s)?lf[0]:1'b0; m_wready<=(!w_s)?lf[1]:1'b0;
if(m_awvalid&&m_awready) begin wa<=m_awaddr; aw_s<=1; m_awready<=0; end
if(m_wvalid&&m_wready) begin wd<=m_wdata; ws<=m_wstrb; w_s<=1; m_wready<=0; end
if(aw_s&&w_s&&!bp&&!m_bvalid) begin
for(int bb=0;bb<32;bb++) if(ws[bb]) shmem[wa>>5][bb*8+:8]<=wd[bb*8+:8];
bp<=1; bd<={1'b0,lf[4:2]};
end
if(bp&&!m_bvalid) begin if(bd==0) begin m_bvalid<=1;m_bresp<=0;bp<=0;aw_s<=0;w_s<=0; end else bd<=bd-1; end
if(m_bvalid&&m_bready) m_bvalid<=0;
m_arready<=(!rp&&!m_rvalid)?lf[8]:1'b0;
if(m_arvalid&&m_arready) begin ra<=m_araddr; rp<=1; rd_dly<={1'b0,lf[7:5]}; m_arready<=0; end
if(rp&&!m_rvalid) begin if(rd_dly==0) begin m_rdata<=shmem[ra>>5]; m_rresp<=0; m_rvalid<=1; m_rlast<=1; rp<=0; end else rd_dly<=rd_dly-1; end
if(m_rvalid&&m_rready) begin m_rvalid<=0; m_rlast<=0; end
end
end
// disjoint-map monitors on the MERGED master streams
always_ff @(posedge emif_clk) if(rst_n) begin
if(m_awvalid) begin
if(!((m_awaddr<30'(COLBASE+COL_TOP)) || (m_awaddr>=30'(ZBASE) && m_awaddr<30'(ZBASE+Z_TOP))))
begin $error("[zint] merged AW %h not in color/Z range",m_awaddr); errors++; end
end
if(m_arvalid && (m_araddr<30'(ZBASE) || m_araddr>=30'(ZBASE+Z_TOP))) begin $error("[zint] merged AR %h not in Z range",m_araddr); errors++; end
end
// ===== clamp16 persistent-Z SCOREBOARD over the ACTUAL emitted fragments (design clk) =====
// Mirrors gs_lpddr_z_rmw: GEQUAL vs stored, Z_CLEAR=0. Persists across epochs. Gated active after clear_done.
logic [15:0] sb_z [0:NPX-1];
logic [31:0] sb_col[0:NPX-1];
logic sb_wr [0:NPX-1];
integer sb_frag, sb_pass;
// no-fragment-after-marker: a fragment must not appear in the same scene after fb_flush (before next GO)
logic scene_ended=0; int frag_after_marker;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sb_frag<=0; sb_pass<=0; scene_ended<=0; frag_after_marker<=0;
end else begin
if (feeder_go_tb && feeder_ready_tb) scene_ended<=0; // new scene opens
if (fb_flush) scene_ended<=1; // ordered marker closes the scene
if (frag_v && saw_clear_done) begin
int o; logic [15:0] zq;
if (scene_ended) frag_after_marker<=frag_after_marker+1; // fragment after this scene's marker = ordering bug
o = g_y*W + g_x; zq = cl16(flush_z_w);
sb_frag<=sb_frag+1;
if (o>=0 && o<NPX) begin
if (zq >= sb_z[o]) begin sb_z[o]<=zq; sb_col[o]<=flush_color32_w; sb_wr[o]<=1'b1; sb_pass<=sb_pass+1; end
end
end
end
end
// FRESH-DRAIN observer (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<=frame_drained;
if (frame_drained && !fd_q) fd_rises<=fd_rises+1;
if (!frame_drained && fd_q) fd_falls<=fd_falls+1;
end
end
// clear_start: mirror de25 — pulse once at the writer ARM (after preclear, before first GO)
logic wr_arm=0, arm_e1, arm_e2, arm_e3;
always_ff @(posedge emif_clk or negedge rst_n) begin
if (!rst_n) begin arm_e1<=0; arm_e2<=0; arm_e3<=0; clear_start<=0; end
else begin arm_e1<=wr_arm; arm_e2<=arm_e1; arm_e3<=arm_e2; clear_start<=(arm_e2 && !arm_e3); end
end
// ---- feeder staging stream (write port), per epoch ----
logic [63:0] stg_buf [0:STG_WORDS-1];
task automatic stream_list(input int k);
string fn; fn=$sformatf("../../data/top_psmct32_raster_demo/feeder_sh3_zs640c6%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);
if (dut.g_feeder.feeder_stg[0][31:0] !== stg_buf[0][31:0]) begin
$error("[zint] epoch %0d: staged word0=%08x exp %08x", k, dut.g_feeder.feeder_stg[0][31:0], stg_buf[0][31:0]); errors++; end
endtask
task automatic fill_cache(input int k, input logic [31:0] exp_crc);
int d=0;
fills=fills+1;
@(posedge clk) fill_start<=1'b1; @(posedge clk) fill_start<=1'b0;
d=0; while (fill_done && d<1000) begin @(posedge clk); d++; end
d=0; while (!fill_done && d<400000) begin @(posedge clk); d++; end
if (!fill_done) begin $error("[zint] fill %0d: fill_done never rose", k); errors++; end
if (fill_crc_w!==exp_crc) begin $error("[zint] fill %0d: crc=%08x exp %08x", k, fill_crc_w, exp_crc); errors++; end
if (tex_rd_errs!==0) begin $error("[zint] fill %0d: rd_errs=%0d", k, tex_rd_errs); errors++; end
endtask
// one-scene runner: GO, wait render+drain, REQUIRE a FRESH ordered frame_drained
task automatic run_scene(input int k, input int exp_records);
int r0, f0, d=0; logic fd_before; int gd0, fm0;
r0=fd_rises; f0=fd_falls; fd_before=frame_drained; gd0=g_drops; fm0=frag_after_marker;
@(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("[zint] epoch %0d: feeder_ready did not drop after GO", k); errors++; end
if (fd_before) begin
d=0; while (fd_falls==f0 && d<6000000) begin @(posedge emif_clk); d++; end
if (fd_falls==f0) begin $error("[zint] epoch %0d: frame_drained never fell from stale high", k); errors++; end
end
d=0; while (fd_rises<=r0 && d<6000000) begin @(posedge emif_clk); d++; end
if (fd_rises<=r0) begin $error("[zint] epoch %0d: frame_drained never rose", k); errors++; end
repeat(100) @(posedge clk);
if (feeder_records_w!==exp_records) begin $error("[zint] epoch %0d: records=%0d exp %0d", k, feeder_records_w, exp_records); errors++; end
if (col_ovf!==0 || bresp_err!==0) begin $error("[zint] epoch %0d: col_ovf=%0d bresp_err=%0d", k, col_ovf, bresp_err); errors++; end
if (g_drops!==gd0) begin $error("[zint] epoch %0d: %0d FRAGMENT DROPS (request FIFO overflow)", k, g_drops-gd0); errors++; end
if (frag_after_marker!==fm0) begin $error("[zint] epoch %0d: %0d fragment(s) after scene marker (ordering)", k, frag_after_marker-fm0); errors++; end
$display("[zint] epoch %0d: fresh drain (f %0d->%0d, r %0d->%0d) records=%0d drops=%0d fed=%0d pass=%0d",
k, f0, fd_falls, r0, fd_rises, feeder_records_w, g_drops, frags_fed, sb_pass);
endtask
task automatic run_epoch(input int k);
string fn; fn=$sformatf("../../data/top_psmct32_raster_demo/sh3_zs640c6%0d_tex_lpddr.mem", k);
if (!EP_REUSE[k]) begin
$readmemh(fn, lpddr_mem);
fill_cache(k, EP_CRC[k]);
end else begin
// Ch359 — EXPLICIT REUSE: no reload, no fill pulse. Fail-closed residency: the fill CRC register is
// only rewritten by a fill, so it must still hold the prior epoch's verified CRC (same shared texture).
if (fill_crc_w!==EP_CRC[k]) begin $error("%s epoch %0d: REUSE but resident crc=%08x exp %08x — residency broken", "[zint]", k, fill_crc_w, EP_CRC[k]); errors++; end
else $display("%s epoch %0d: REUSE resident texture (crc=0x%08x, no fill)", "[zint]", k, fill_crc_w);
end
stream_list(k);
run_scene(k, EP_REC[k]);
endtask
initial begin
errors=0; feeder_go_tb=1'b0; fill_start=1'b0; wr_arm=0; zc_enable=1'b1; stg_we=0; stg_waddr=0; stg_wdata=0; hw_clear=1'b0;
for (int i=0;i<NPX;i++) begin sb_z[i]=16'h0000; sb_col[i]=32'd0; sb_wr[i]=1'b0; end
for (int b=0;b<MEMBEATS;b++) shmem[b]=256'd0; // PRECLEAR color+Z EXACTLY ONCE
// skip-guard: probe a fixture (via $fopen — avoids the $readmemh range warning)
begin int pfh; pfh=$fopen("../../data/top_psmct32_raster_demo/sh3_zs640c60_tex_lpddr.mem","r");
if (pfh==0) begin $display("[tb_top_psmct32_sh3_zint640c6] SKIP — sh3_zs640c6*.mem absent (run gs_make_sh3_scheduler_fixture.py --authz --fb640 --tag zs640c6 --emit)"); $finish; end
$fclose(pfh);
end
rst_n=1'b0; core_go=1'b0; repeat(6) @(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);
wait (feeder_ready_tb==1'b1); repeat(50) @(posedge clk);
// ARM (-> clear_start) AFTER preclear, BEFORE first GO. z_rmw's clear_start writes Z_CLEAR to EVERY Z beat
// (NBEATS=NPX/16, through wr_arb s2 under random backpressure) then asserts clear_done -> it is the authoritative
// Z preclear. Wait for it (the board host likewise gates the first GO on the clear_done status bit).
wr_arm<=1'b1;
begin int d=0; while (!clear_done && d<600000) begin @(posedge emif_clk); d++; end end
if (!clear_done) begin $error("[zint] clear_done never rose after ARM (Z preclear did not finish)"); errors++; end
else $display("[zint] Z preclear complete (clear_done), z_beats_written(clear)=%0d", z_beats_written);
repeat(40) @(posedge clk);
for (int k=0;k<NEP;k++) begin
int fed0, dr0;
fed0=frags_fed; dr0=g_drops;
run_epoch(k);
$display("[zint][sweep] depth=%0d epoch %0d: accepted=%0d drops=%0d req_hiwater=%0d",
TB_REQ_DEPTH, k, frags_fed-fed0, g_drops-dr0, req_hiwater);
@(negedge clk); hw_clear<=1'b1; @(negedge clk); hw_clear<=1'b0; // per-epoch high-water
end
if (pre_clear_frags!==0) begin $error("[zint] %0d psm0 fragment(s) before clear_done", pre_clear_frags); errors++; end
if (fd_rises<NEP) begin $error("[zint] expected %0d ordered drains: rises=%0d", NEP, fd_rises); errors++; end
if (fd_falls<NEP-1) begin $error("[zint] epochs after the first never cleared stale (falls=%0d)", fd_falls); errors++; end
if (eof_count!==NEP)begin $error("[zint] scene markers=%0d != accepted GOs=%0d", eof_count, NEP); errors++; end
if (g_drops!==0) begin $error("[zint] TOTAL %0d fragment drops", g_drops); errors++; end
// Ch359 — the WHOLE run must perform exactly ONE verified texture fill (epoch 1 reuses the resident cache)
if (fills!==1) begin $error("[zint] expected exactly ONE texture fill, got %0d (residency contract)", fills); errors++; end
// ===== final memory compare: shared LPDDR color + Z == scoreboard =====
begin
int zmis, cmis, zchk, cchk; zmis=0; cmis=0; zchk=0; cchk=0;
for (int o=0;o<NPX;o++) begin
logic [15:0] zs; logic [31:0] cs; int zb, zl;
zb=(ZBASE + o*2)>>5; zl=o&15; zs=shmem[zb][zl*16+:16];
zchk++;
if (zs !== sb_z[o]) begin if (zmis<10) $error("[zint] Z px%0d got %04x exp %04x", o, zs, sb_z[o]); zmis++; end
if (sb_wr[o]) begin
cs=shmem[o>>3][(o[2:0])*32+:32]; cchk++;
if (cs[23:0] !== sb_col[o][23:0]) begin if (cmis<10) $error("[zint] COL px%0d got %06x exp %06x", o, cs[23:0], sb_col[o][23:0]); cmis++; end
end
end
$display("[zint] final compare: Z %0d/%0d mismatch, COLOR %0d/%0d mismatch (written px=%0d)", zmis, zchk, cmis, cchk, cchk);
end
begin
string fdump;
if ($value$plusargs("FBDUMP=%s", fdump)) begin
int fh;
fh = $fopen(fdump, "w");
if (fh == 0) begin
$error("[zint] could not open FBDUMP '%s'", fdump);
errors++;
end else begin
for (int o=0;o<NPX;o++) begin
logic [31:0] cs;
cs = shmem[o>>3][(o[2:0])*32+:32];
$fdisplay(fh, "%08x", cs);
end
$fclose(fh);
$display("[zint] dumped final COLOR FB (%0dx%0d) -> %s", W, H, fdump);
end
end
end
$display("[tb_top_psmct32_sh3_zint640c6] fed=%0d pass=%0d drops=%0d markers=%0d drains(r/f)=%0d/%0d col_ovf=%0d bresp_err=%0d errors=%0d",
frags_fed, sb_pass, g_drops, eof_count, fd_rises, fd_falls, col_ovf, bresp_err, errors);
$display("[zint] req-FIFO depth=%0d (diagnostic sweep variant when != 1024)", TB_REQ_DEPTH);
$display("[zint] map: COLOR 0x%0h..0x%0h Z 0x%0h..0x%0h", COLBASE, COLBASE+COL_TOP, ZBASE, ZBASE+Z_TOP);
if (errors==0) $display("[tb_top_psmct32_sh3_zint640c6] PASS"); else $display("[tb_top_psmct32_sh3_zint640c6] FAIL");
$finish;
end
initial begin #400000000; $error("[tb_top_psmct32_sh3_zint640c6] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_zint640c6
+477
View File
@@ -0,0 +1,477 @@
// retroDE_ps2 — tb_top_psmct32_sh3_zint640rt3 (runtime-CLUT persistent-Z board integration at NATIVE 640x480)
//
// Codex's pre-fit gate: prove the persistent-Z ROP through the SAME path the de25 top wires (GS_SH3_LPDDR_FB_Z):
// REAL demo (feeder + raster, authz sh3_zsrt3 NATIVE-640x480 strong-reject scene) -> gs_lpddr_zc_emit -> REAL de25 arbiters
// (gs_lpddr_wr_arb s0=color / s2=Z-write ; gs_lpddr_rd_arb s3=Z-read) -> ONE shared behavioral LPDDR
// with randomized backpressure. Unlike tb_top_psmct32_sh3_zarb (which fed a handshake-throttled trace),
// the raster CANNOT honor g_ready, so this is the gate that catches request-FIFO fragment DROPS.
//
// Asserts (Codex list):
// * final LPDDR color + Z == independent clamp16 persistent-Z scoreboard over the ACTUAL emitted fragments;
// * zero fragment drops (g_valid && !g_ready) — the whole reason this sim exists;
// * exactly one ordered scene marker (sh3_fb_flush) per accepted GO; no fragment after a marker within a scene;
// * Z cache invalidated (clear_done) after preclear and before the first GO; Z persists across epochs;
// * a FRESH ordered frame_drained per epoch; scanout never referenced during render (rd_arb s0/s1/s2 idle);
// * merged AW only in color/Z ranges, merged AR only in Z range (disjoint-map monitor).
// LOCAL/gitignored fixtures; skip-guard if absent.
`timescale 1ns/1ps
module tb_top_psmct32_sh3_zint640rt3;
`include "sh3_zsrt3_params.vh" // FBPXW=640, FBH=480, N_EPOCHS=3, EPk_CRC/EPk_NTRIS, TEX_*, ...
localparam int W = FBPXW, H = FBH;
localparam int NPX = W*H;
localparam int NEP = N_EPOCHS;
localparam [31:0] COLBASE = 32'h0000_0000, ZBASE = 32'h0014_0000;
localparam int COL_TOP = NPX*4; // color region byte-size (disjoint below ZBASE)
localparam int Z_TOP = NPX*2; // Z region byte-size
localparam int MEMBEATS = 65536; // shared LPDDR: covers ZBASE + Z_TOP
// per-epoch expected CRC / records
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 rst_n; logic fb_commit=0; always #13 fb_commit=~fb_commit;
int errors; initial errors=0;
// clamp16 (vendored PCSX2 PSMZ16S source-Z saturation)
function automatic logic [15:0] cl16(input logic [31:0] z); cl16=(|z[31:16])?16'hFFFF:z[15:0]; endfunction
// ===== bram-top (FB_LPDDR_ONLY, AUTOSTART off), 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;
logic stg_we; logic [11:0] stg_waddr; logic [63:0] stg_wdata;
// Mirrors the core-side output of the runtime-CLUT CDC: the bridge and
// CDC have focused gates; this integration gate proves the rendered scene.
logic runtime_clut_wr_en, runtime_clut_busy;
logic [7:0] runtime_clut_wr_idx;
logic [31:0] runtime_clut_wr_data;
logic [31:0] runtime_pal [0:255];
integer frag_fh;
integer frag_epoch;
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),
.runtime_clut_wr_en_i(runtime_clut_wr_en), .runtime_clut_wr_idx_i(runtime_clut_wr_idx),
.runtime_clut_wr_data_i(runtime_clut_wr_data), .runtime_clut_busy_i(runtime_clut_busy)
);
// texture cache + behavioral texture LPDDR (reloaded per epoch)
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];
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: one ordered scene marker (sh3_fb_flush) per accepted GO =====
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
// ===== gs_lpddr_zc_emit — Z-then-color emit, fed by the REAL raster (NO handshake) =====
logic zc_enable, clear_start, clear_done, frame_drained;
wire [15:0] g_zq = cl16(flush_z_w);
wire frag_v = flush_emit_w && (flush_psm_w==6'h00);
wire g_valid = frag_v || fb_flush;
wire g_scene = fb_flush;
wire [11:0] g_x = flush_x_w, g_y = flush_y_w;
wire [31:0] g_color = flush_color32_w;
wire g_ready;
// Z AXI
logic [31:0] z_araddr; logic [7:0] z_arlen; logic [2:0] z_arsize; logic [1:0] z_arburst; logic z_arvalid, z_arready;
logic [255:0] z_rdata; logic [1:0] z_rresp; logic z_rlast, z_rvalid, z_rready;
logic [31:0] z_awaddr; logic [7:0] z_awlen; logic [2:0] z_awsize; logic [1:0] z_awburst; logic z_awvalid, z_awready;
logic [255:0] z_wdata; logic [31:0] z_wstrb; logic z_wlast, z_wvalid, z_wready; logic z_bvalid, z_bready; logic [1:0] z_bresp;
// Color AXI
logic [31:0] c_awaddr; logic [7:0] c_awlen; logic [2:0] c_awsize; logic [1:0] c_awburst; logic c_awvalid, c_awready;
logic [255:0] c_wdata; logic [31:0] c_wstrb; logic c_wlast, c_wvalid, c_wready; logic c_bvalid, c_bready; logic [1:0] c_bresp;
logic [31:0] z_beats_read, z_beats_written, c_beats_written, col_ovf, bresp_err; logic zc_idle;
gs_lpddr_zc_emit #(.COLBASE(COLBASE), .ZBASE(ZBASE), .FB_PXW(W), .FB_H(H), .REQ_DEPTH(1024), .COL_DEPTH(128)) u_zc (
.gs_clk(clk), .gs_rst_n(rst_n), .enable(zc_enable),
.g_valid(g_valid), .g_ready(g_ready), .g_x(g_x), .g_y(g_y), .g_zq(g_zq), .g_zmsk(1'b0),
.g_ztest(1'b1), .g_ztst(2'd2), .g_color(g_color), .g_be(4'hF), .g_scene(g_scene),
.axi_clk(emif_clk), .axi_rst_n(rst_n), .clear_start(clear_start), .clear_done(clear_done), .frame_drained(frame_drained),
.z_araddr(z_araddr), .z_arlen(z_arlen), .z_arsize(z_arsize), .z_arburst(z_arburst), .z_arvalid(z_arvalid), .z_arready(z_arready),
.z_rdata(z_rdata), .z_rresp(z_rresp), .z_rlast(z_rlast), .z_rvalid(z_rvalid), .z_rready(z_rready),
.z_awaddr(z_awaddr), .z_awlen(z_awlen), .z_awsize(z_awsize), .z_awburst(z_awburst), .z_awvalid(z_awvalid), .z_awready(z_awready),
.z_wdata(z_wdata), .z_wstrb(z_wstrb), .z_wlast(z_wlast), .z_wvalid(z_wvalid), .z_wready(z_wready),
.z_bvalid(z_bvalid), .z_bready(z_bready), .z_bresp(z_bresp),
.c_awaddr(c_awaddr), .c_awlen(c_awlen), .c_awsize(c_awsize), .c_awburst(c_awburst), .c_awvalid(c_awvalid), .c_awready(c_awready),
.c_wdata(c_wdata), .c_wstrb(c_wstrb), .c_wlast(c_wlast), .c_wvalid(c_wvalid), .c_wready(c_wready),
.c_bvalid(c_bvalid), .c_bready(c_bready), .c_bresp(c_bresp),
.z_beats_read(z_beats_read), .z_beats_written(z_beats_written), .c_beats_written(c_beats_written),
.col_ovf(col_ovf), .bresp_err(bresp_err), .idle(zc_idle)
);
// fragment-drop guard (design clk): the request FIFO silently drops when g_ready is low
int g_drops; int frags_fed;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin g_drops<=0; frags_fed<=0; end
else begin
if (frag_v && !g_ready) g_drops<=g_drops+1;
if (frag_v && g_ready) frags_fed<=frags_fed+1;
end
end
// clear ordering: clear_done must rise before the first GO. no psm0 fragment before clear_done.
logic saw_clear_done=0, first_go_done=0; int pre_clear_frags;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin saw_clear_done<=0; pre_clear_frags<=0; end
else begin
if (clear_done) saw_clear_done<=1;
if (frag_v && !saw_clear_done) pre_clear_frags<=pre_clear_frags+1;
end
end
// ===== REAL de25 write arbiter: s0=color, s2=Z-write ; s1/s3 inert =====
logic [29:0] m_awaddr; logic [1:0] m_awburst; logic [6:0] m_awid; logic [7:0] m_awlen; logic [2:0] m_awsize;
logic m_awvalid, m_awready; logic [255:0] m_wdata; logic [31:0] m_wstrb; logic m_wlast, m_wvalid, m_wready;
logic m_bvalid, m_bready; logic [1:0] m_bresp;
gs_lpddr_wr_arb u_wr_arb (
.clk(emif_clk), .rst_n(rst_n),
.s0_awaddr(c_awaddr[29:0]), .s0_awburst(c_awburst), .s0_awid(7'd0), .s0_awlen(c_awlen), .s0_awsize(c_awsize),
.s0_awvalid(c_awvalid), .s0_awready(c_awready), .s0_wdata(c_wdata), .s0_wstrb(c_wstrb), .s0_wlast(c_wlast),
.s0_wvalid(c_wvalid), .s0_wready(c_wready), .s0_bresp(c_bresp), .s0_bvalid(c_bvalid), .s0_bready(c_bready),
.s1_awaddr(30'd0), .s1_awburst(2'b01), .s1_awid(7'd1), .s1_awlen(8'd0), .s1_awsize(3'b101),
.s1_awvalid(1'b0), .s1_awready(), .s1_wdata(256'd0), .s1_wstrb(32'd0), .s1_wlast(1'b0),
.s1_wvalid(1'b0), .s1_wready(), .s1_bresp(), .s1_bvalid(), .s1_bready(1'b0),
.s2_awaddr(z_awaddr[29:0]), .s2_awburst(z_awburst), .s2_awid(7'd2), .s2_awlen(z_awlen), .s2_awsize(z_awsize),
.s2_awvalid(z_awvalid), .s2_awready(z_awready), .s2_wdata(z_wdata), .s2_wstrb(z_wstrb), .s2_wlast(z_wlast),
.s2_wvalid(z_wvalid), .s2_wready(z_wready), .s2_bresp(z_bresp), .s2_bvalid(z_bvalid), .s2_bready(z_bready),
.s3_awaddr(30'd0), .s3_awburst(2'b01), .s3_awid(7'd3), .s3_awlen(8'd0), .s3_awsize(3'b101),
.s3_awvalid(1'b0), .s3_awready(), .s3_wdata(256'd0), .s3_wstrb(32'd0), .s3_wlast(1'b0),
.s3_wvalid(1'b0), .s3_wready(), .s3_bresp(), .s3_bvalid(), .s3_bready(1'b0),
.m_awaddr(m_awaddr), .m_awburst(m_awburst), .m_awid(m_awid), .m_awlen(m_awlen), .m_awsize(m_awsize),
.m_awvalid(m_awvalid), .m_awready(m_awready), .m_wdata(m_wdata), .m_wstrb(m_wstrb), .m_wlast(m_wlast),
.m_wvalid(m_wvalid), .m_wready(m_wready), .m_bvalid(m_bvalid), .m_bready(m_bready), .m_bresp(m_bresp)
);
// ===== REAL de25 read arbiter: s3=Z-read (mirrors de25 reload port) ; s0/s1/s2 idle during render =====
logic [29:0] m_araddr; logic [1:0] m_arburst; logic [6:0] m_arid; logic [7:0] m_arlen; logic [2:0] m_arsize;
logic m_arvalid, m_arready; logic [255:0] m_rdata; logic [1:0] m_rresp; logic m_rlast, m_rvalid, m_rready;
gs_lpddr_rd_arb u_rd_arb (
.clk(emif_clk), .rst_n(rst_n),
.s0_araddr(30'd0), .s0_arburst(2'b01), .s0_arid(7'd0), .s0_arlen(8'd0), .s0_arsize(3'b101),
.s0_arvalid(1'b0), .s0_arready(), .s0_rdata(), .s0_rresp(), .s0_rlast(), .s0_rvalid(), .s0_rready(1'b0),
.s1_araddr(30'd0), .s1_arburst(2'b01), .s1_arid(7'd1), .s1_arlen(8'd0), .s1_arsize(3'b101),
.s1_arvalid(1'b0), .s1_arready(), .s1_rdata(), .s1_rresp(), .s1_rlast(), .s1_rvalid(), .s1_rready(1'b0),
.s2_araddr(30'd0), .s2_arburst(2'b01), .s2_arid(7'd2), .s2_arlen(8'd0), .s2_arsize(3'b101),
.s2_arvalid(1'b0), .s2_arready(), .s2_rdata(), .s2_rresp(), .s2_rlast(), .s2_rvalid(), .s2_rready(1'b0),
.s3_araddr(z_araddr[29:0]), .s3_arburst(z_arburst), .s3_arid(7'd3), .s3_arlen(z_arlen), .s3_arsize(z_arsize),
.s3_arvalid(z_arvalid), .s3_arready(z_arready), .s3_rdata(z_rdata), .s3_rresp(z_rresp), .s3_rlast(z_rlast),
.s3_rvalid(z_rvalid), .s3_rready(z_rready),
.m_araddr(m_araddr), .m_arburst(m_arburst), .m_arid(m_arid), .m_arlen(m_arlen), .m_arsize(m_arsize),
.m_arvalid(m_arvalid), .m_arready(m_arready), .m_rdata(m_rdata), .m_rresp(m_rresp), .m_rlast(m_rlast),
.m_rvalid(m_rvalid), .m_rready(m_rready)
);
logic [15:0] lf=16'hF00D; always_ff @(posedge emif_clk) lf<={lf[14:0], lf[15]^lf[13]^lf[12]^lf[10]};
// ===== ONE shared behavioral LPDDR (write + read), address-decoded, random backpressure =====
logic [255:0] shmem [0:MEMBEATS-1];
logic [29:0] wa; logic [255:0] wd; logic [31:0] ws; logic aw_s, w_s; logic [3:0] bd; logic bp;
logic [29:0] ra; logic rp; logic [3:0] rd_dly;
always_ff @(posedge emif_clk or negedge rst_n) begin
if(!rst_n) begin
m_awready<=0; m_wready<=0; m_bvalid<=0; m_bresp<=0; aw_s<=0; w_s<=0; bp<=0; bd<=0;
m_arready<=0; m_rvalid<=0; m_rlast<=0; m_rresp<=0; m_rdata<=0; rp<=0; rd_dly<=0;
end else begin
m_awready<=(!aw_s)?lf[0]:1'b0; m_wready<=(!w_s)?lf[1]:1'b0;
if(m_awvalid&&m_awready) begin wa<=m_awaddr; aw_s<=1; m_awready<=0; end
if(m_wvalid&&m_wready) begin wd<=m_wdata; ws<=m_wstrb; w_s<=1; m_wready<=0; end
if(aw_s&&w_s&&!bp&&!m_bvalid) begin
for(int bb=0;bb<32;bb++) if(ws[bb]) shmem[wa>>5][bb*8+:8]<=wd[bb*8+:8];
bp<=1; bd<={1'b0,lf[4:2]};
end
if(bp&&!m_bvalid) begin if(bd==0) begin m_bvalid<=1;m_bresp<=0;bp<=0;aw_s<=0;w_s<=0; end else bd<=bd-1; end
if(m_bvalid&&m_bready) m_bvalid<=0;
m_arready<=(!rp&&!m_rvalid)?lf[8]:1'b0;
if(m_arvalid&&m_arready) begin ra<=m_araddr; rp<=1; rd_dly<={1'b0,lf[7:5]}; m_arready<=0; end
if(rp&&!m_rvalid) begin if(rd_dly==0) begin m_rdata<=shmem[ra>>5]; m_rresp<=0; m_rvalid<=1; m_rlast<=1; rp<=0; end else rd_dly<=rd_dly-1; end
if(m_rvalid&&m_rready) begin m_rvalid<=0; m_rlast<=0; end
end
end
// disjoint-map monitors on the MERGED master streams
always_ff @(posedge emif_clk) if(rst_n) begin
if(m_awvalid) begin
if(!((m_awaddr<30'(COLBASE+COL_TOP)) || (m_awaddr>=30'(ZBASE) && m_awaddr<30'(ZBASE+Z_TOP))))
begin $error("[zint] merged AW %h not in color/Z range",m_awaddr); errors++; end
end
if(m_arvalid && (m_araddr<30'(ZBASE) || m_araddr>=30'(ZBASE+Z_TOP))) begin $error("[zint] merged AR %h not in Z range",m_araddr); errors++; end
end
// ===== clamp16 persistent-Z SCOREBOARD over the ACTUAL emitted fragments (design clk) =====
// Mirrors gs_lpddr_z_rmw: GEQUAL vs stored, Z_CLEAR=0. Persists across epochs. Gated active after clear_done.
logic [15:0] sb_z [0:NPX-1];
logic [31:0] sb_col[0:NPX-1];
logic sb_wr [0:NPX-1];
integer sb_frag, sb_pass;
// no-fragment-after-marker: a fragment must not appear in the same scene after fb_flush (before next GO)
logic scene_ended=0; int frag_after_marker;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sb_frag<=0; sb_pass<=0; scene_ended<=0; frag_after_marker<=0;
end else begin
if (feeder_go_tb && feeder_ready_tb) scene_ended<=0; // new scene opens
if (fb_flush) scene_ended<=1; // ordered marker closes the scene
if (frag_v && saw_clear_done) begin
int o; logic [15:0] zq;
if (frag_fh != 0)
$fwrite(frag_fh, "%0d %0d %0d %0d %08x\n", frag_epoch, g_x, g_y, flush_z_w, flush_color32_w);
if (scene_ended) frag_after_marker<=frag_after_marker+1; // fragment after this scene's marker = ordering bug
o = g_y*W + g_x; zq = cl16(flush_z_w);
sb_frag<=sb_frag+1;
if (o>=0 && o<NPX) begin
if (zq >= sb_z[o]) begin sb_z[o]<=zq; sb_col[o]<=flush_color32_w; sb_wr[o]<=1'b1; sb_pass<=sb_pass+1; end
end
end
end
end
// FRESH-DRAIN observer (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<=frame_drained;
if (frame_drained && !fd_q) fd_rises<=fd_rises+1;
if (!frame_drained && fd_q) fd_falls<=fd_falls+1;
end
end
// clear_start: mirror de25 — pulse once at the writer ARM (after preclear, before first GO)
logic wr_arm=0, arm_e1, arm_e2, arm_e3;
always_ff @(posedge emif_clk or negedge rst_n) begin
if (!rst_n) begin arm_e1<=0; arm_e2<=0; arm_e3<=0; clear_start<=0; end
else begin arm_e1<=wr_arm; arm_e2<=arm_e1; arm_e3<=arm_e2; clear_start<=(arm_e2 && !arm_e3); end
end
// ---- feeder staging stream (write port), per epoch ----
logic [63:0] stg_buf [0:STG_WORDS-1];
task automatic stream_list(input int k);
string fn; fn=$sformatf("../../data/top_psmct32_raster_demo/feeder_sh3_zsrt3%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);
if (dut.g_feeder.feeder_stg[0][31:0] !== stg_buf[0][31:0]) begin
$error("[zint] epoch %0d: staged word0=%08x exp %08x", k, dut.g_feeder.feeder_stg[0][31:0], stg_buf[0][31:0]); errors++; end
endtask
task automatic fill_cache(input int k, input logic [31:0] exp_crc);
int d=0;
$display("[zint-rt3] epoch %0d: texture fill start", k); $fflush();
@(posedge clk) fill_start<=1'b1; @(posedge clk) fill_start<=1'b0;
d=0; while (fill_done && d<1000) begin @(posedge clk); d++; end
d=0; while (!fill_done && d<400000) begin @(posedge clk); d++; end
if (!fill_done) begin $error("[zint] fill %0d: fill_done never rose", k); errors++; end
if (fill_crc_w!==exp_crc) begin $error("[zint] fill %0d: crc=%08x exp %08x", k, fill_crc_w, exp_crc); errors++; end
if (tex_rd_errs!==0) begin $error("[zint] fill %0d: rd_errs=%0d", k, tex_rd_errs); errors++; end
$display("[zint-rt3] epoch %0d: texture fill done", k); $fflush();
endtask
task automatic load_runtime_palette(input int k);
string fn;
fn = $sformatf("../../data/top_psmct32_raster_demo/sh3_zsrt3%0d_pal.mem", k);
$display("[zint-rt3] epoch %0d: runtime palette start", k); $fflush();
$readmemh(fn, runtime_pal);
@(negedge clk);
runtime_clut_busy <= 1'b1;
for (int i=0; i<256; i++) begin
runtime_clut_wr_en <= 1'b1;
runtime_clut_wr_idx <= i[7:0];
runtime_clut_wr_data <= runtime_pal[i];
@(negedge clk);
end
runtime_clut_wr_en <= 1'b0;
runtime_clut_wr_idx <= '0;
runtime_clut_wr_data <= '0;
@(negedge clk);
runtime_clut_busy <= 1'b0;
@(negedge clk);
$display("[zint-rt3] epoch %0d: runtime palette done", k); $fflush();
endtask
// one-scene runner: GO, wait render+drain, REQUIRE a FRESH ordered frame_drained
task automatic run_scene(input int k, input int exp_records);
int r0, f0, d=0; logic fd_before; int gd0, fm0;
r0=fd_rises; f0=fd_falls; fd_before=frame_drained; gd0=g_drops; fm0=frag_after_marker;
@(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("[zint] epoch %0d: feeder_ready did not drop after GO", k); errors++; end
if (fd_before) begin
d=0; while (fd_falls==f0 && d<1500000) begin @(posedge emif_clk); d++; end
if (fd_falls==f0) begin $error("[zint] epoch %0d: frame_drained never fell from stale high", k); errors++; end
end
d=0; while (fd_rises<=r0 && d<1500000) begin @(posedge emif_clk); d++; end
if (fd_rises<=r0) begin $error("[zint] epoch %0d: frame_drained never rose", k); errors++; end
repeat(100) @(posedge clk);
if (feeder_records_w!==exp_records) begin $error("[zint] epoch %0d: records=%0d exp %0d", k, feeder_records_w, exp_records); errors++; end
if (col_ovf!==0 || bresp_err!==0) begin $error("[zint] epoch %0d: col_ovf=%0d bresp_err=%0d", k, col_ovf, bresp_err); errors++; end
if (g_drops!==gd0) begin $error("[zint] epoch %0d: %0d FRAGMENT DROPS (request FIFO overflow)", k, g_drops-gd0); errors++; end
if (frag_after_marker!==fm0) begin $error("[zint] epoch %0d: %0d fragment(s) after scene marker (ordering)", k, frag_after_marker-fm0); errors++; end
$display("[zint] epoch %0d: fresh drain (f %0d->%0d, r %0d->%0d) records=%0d drops=%0d fed=%0d pass=%0d",
k, f0, fd_falls, r0, fd_rises, feeder_records_w, g_drops, frags_fed, sb_pass);
endtask
task automatic run_epoch(input int k);
string fn; fn=$sformatf("../../data/top_psmct32_raster_demo/sh3_zsrt3%0d_tex_lpddr.mem", k);
$display("[zint-rt3] epoch %0d: begin", k); $fflush();
$readmemh(fn, lpddr_mem);
fill_cache(k, EP_CRC[k]);
load_runtime_palette(k);
stream_list(k);
frag_epoch = k;
run_scene(k, EP_REC[k]);
$display("[zint-rt3] epoch %0d: done", k); $fflush();
endtask
initial begin
errors=0; feeder_go_tb=1'b0; fill_start=1'b0; wr_arm=0; zc_enable=1'b1; stg_we=0; stg_waddr=0; stg_wdata=0;
runtime_clut_wr_en=0; runtime_clut_wr_idx='0; runtime_clut_wr_data='0; runtime_clut_busy=0; frag_epoch=0;
frag_fh=$fopen("zsrt3_frags.txt", "w");
if (frag_fh==0) begin $error("[zint] could not open zsrt3_frags.txt"); $finish; end
for (int i=0;i<NPX;i++) begin sb_z[i]=16'h0000; sb_col[i]=32'd0; sb_wr[i]=1'b0; end
for (int b=0;b<MEMBEATS;b++) shmem[b]=256'd0; // PRECLEAR color+Z EXACTLY ONCE
// skip-guard: probe a fixture (via $fopen — avoids the $readmemh range warning)
begin int pfh; pfh=$fopen("../../data/top_psmct32_raster_demo/sh3_zsrt30_tex_lpddr.mem","r");
if (pfh==0) begin $display("[tb_top_psmct32_sh3_zint640rt3] SKIP — sh3_zsrt3*.mem absent (run gs_make_sh3_scheduler_fixture.py --authz --fb640 --tag zsrt3 --runtime-clut --emit)"); $finish; end
$fclose(pfh);
end
rst_n=1'b0; core_go=1'b0; repeat(6) @(posedge clk); rst_n=1'b1; repeat(8) @(posedge clk);
@(negedge clk); core_go=1'b1; @(negedge clk); core_go=1'b0;
begin int d=0; while(core_halt!==1'b1 && d<2000000) begin @(posedge clk); d++; end
if(core_halt!==1'b1) begin $error("[zint-rt3] boot timeout waiting for core_halt"); $finish; end end
repeat(4) @(posedge clk);
// zsrt3 is an HPS-resident runtime-CLUT fixture. Its board flow begins after
// the boot handoff has completed, but this standalone raster top has no HPS
// bootlet agent to produce that one-shot DMA-complete event. Model the already
// initialized board state; the test still drives every staged list, palette,
// texture fill, render, drain, and LPDDR transaction explicitly.
force dut.dma_done_seen = 1'b1;
force dut.xfer_busy = 1'b0;
begin int d=0; while(feeder_ready_tb!==1'b1 && d<2000000) begin @(posedge clk); d++; end
if(feeder_ready_tb!==1'b1) begin $error("[zint-rt3] boot timeout waiting for feeder_ready"); $finish; end end
repeat(50) @(posedge clk);
$display("[zint-rt3] boot and feeder ready"); $fflush();
// ARM (-> clear_start) AFTER preclear, BEFORE first GO. z_rmw's clear_start writes Z_CLEAR to EVERY Z beat
// (NBEATS=NPX/16, through wr_arb s2 under random backpressure) then asserts clear_done -> it is the authoritative
// Z preclear. Wait for it (the board host likewise gates the first GO on the clear_done status bit).
wr_arm<=1'b1;
begin int d=0; while (!clear_done && d<600000) begin @(posedge emif_clk); d++; end end
if (!clear_done) begin $error("[zint] clear_done never rose after ARM (Z preclear did not finish)"); errors++; end
else $display("[zint] Z preclear complete (clear_done), z_beats_written(clear)=%0d", z_beats_written);
repeat(40) @(posedge clk);
for (int k=0;k<NEP;k++) run_epoch(k);
if (pre_clear_frags!==0) begin $error("[zint] %0d psm0 fragment(s) before clear_done", pre_clear_frags); errors++; end
if (fd_rises<NEP) begin $error("[zint] expected %0d ordered drains: rises=%0d", NEP, fd_rises); errors++; end
if (fd_falls<NEP-1) begin $error("[zint] epochs after the first never cleared stale (falls=%0d)", fd_falls); errors++; end
if (eof_count!==NEP)begin $error("[zint] scene markers=%0d != accepted GOs=%0d", eof_count, NEP); errors++; end
if (g_drops!==0) begin $error("[zint] TOTAL %0d fragment drops", g_drops); errors++; end
// ===== final memory compare: shared LPDDR color + Z == scoreboard =====
begin
int zmis, cmis, zchk, cchk; zmis=0; cmis=0; zchk=0; cchk=0;
for (int o=0;o<NPX;o++) begin
logic [15:0] zs; logic [31:0] cs; int zb, zl;
zb=(ZBASE + o*2)>>5; zl=o&15; zs=shmem[zb][zl*16+:16];
zchk++;
if (zs !== sb_z[o]) begin if (zmis<10) $error("[zint] Z px%0d got %04x exp %04x", o, zs, sb_z[o]); zmis++; end
if (sb_wr[o]) begin
cs=shmem[o>>3][(o[2:0])*32+:32]; cchk++;
if (cs[23:0] !== sb_col[o][23:0]) begin if (cmis<10) $error("[zint] COL px%0d got %06x exp %06x", o, cs[23:0], sb_col[o][23:0]); cmis++; end
end
end
$display("[zint] final compare: Z %0d/%0d mismatch, COLOR %0d/%0d mismatch (written px=%0d)", zmis, zchk, cmis, cchk, cchk);
end
begin
string fdump;
if ($value$plusargs("FBDUMP=%s", fdump)) begin
int fh;
fh = $fopen(fdump, "w");
if (fh == 0) begin
$error("[zint] could not open FBDUMP '%s'", fdump);
errors++;
end else begin
for (int o=0;o<NPX;o++) begin
logic [31:0] cs;
cs = shmem[o>>3][(o[2:0])*32+:32];
$fdisplay(fh, "%08x", cs);
end
$fclose(fh);
$display("[zint] dumped final COLOR FB (%0dx%0d) -> %s", W, H, fdump);
end
end
end
$display("[tb_top_psmct32_sh3_zint640rt3] fed=%0d pass=%0d drops=%0d markers=%0d drains(r/f)=%0d/%0d col_ovf=%0d bresp_err=%0d errors=%0d",
frags_fed, sb_pass, g_drops, eof_count, fd_rises, fd_falls, col_ovf, bresp_err, errors);
$display("[zint] map: COLOR 0x%0h..0x%0h Z 0x%0h..0x%0h", COLBASE, COLBASE+COL_TOP, ZBASE, ZBASE+Z_TOP);
$fclose(frag_fh);
if (errors==0) $display("[tb_top_psmct32_sh3_zint640rt3] PASS"); else $display("[tb_top_psmct32_sh3_zint640rt3] FAIL");
$finish;
end
initial begin #400000000; $error("[tb_top_psmct32_sh3_zint640rt3] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_zint640rt3
+184
View File
@@ -0,0 +1,184 @@
// retroDE_ps2 — tb_top_psmct32_sh3_zrop (Ch357 — persistent-Z ROP acceptance gate, TRACE-based scoreboard)
//
// Codex option A: verify the LPDDR-Z ROP (gs_lpddr_zc_emit) against the raster's ACTUAL emitted fragments. This TB replays
// the captured fragment trace (tb_top_psmct32_sh3_zsched -> zsched_frags.txt: "epoch x y persp_z color" per line) through
// zc_emit across two clock domains, with a per-epoch scene marker, and scores it with an INDEPENDENT software scoreboard
// (clamp16 / GEQUAL / persistence). Proves Codex's Ch357 gates:
// * every replayed fragment enters the ROP EXACTLY once (we feed each once and wait g_ready — no drop, no double).
// * the scoreboard predicts every pass/reject and the final packed 16-bit Z; failed depth tests emit NO color.
// * final color + Z LPDDR memories match the scoreboard EXACTLY.
// * the ordered end-of-scene marker drain waits BOTH pipelines (color + Z BRESPs) before frame_drained.
// This is the ROP-correctness gate; the raster's Z-interp fidelity (float-oracle 3971 / 87.6% owner agreement) is a SEPARATE
// recorded limitation, not tested here. Primary fixture = sh3_zsched [8634,12757,145742], FB 256x210, Z base 0x140000.
`timescale 1ns/1ps
module tb_top_psmct32_sh3_zrop;
localparam int FB_PXW=256, FB_H=210;
localparam int NPX=FB_PXW*FB_H; // 53760
localparam int ZBEATS=(NPX+15)/16; // 3360
localparam int CBEATS=(NPX+7)/8; // 6720
localparam [31:0] COLBASE=32'h0000_0000, ZBASE=32'h0014_0000;
localparam int ZBW=$clog2(ZBEATS), CBW=$clog2(CBEATS);
logic gs_clk=0; always #5 gs_clk=~gs_clk;
logic axi_clk=0; always #2 axi_clk=~axi_clk; // emif faster than design -> ROP drains faster than the trace feeds
logic gs_rst_n, axi_rst_n; int errors; initial errors=0;
logic enable, clear_start, clear_done, frame_drained;
logic g_valid, g_ready; logic [11:0] g_x, g_y; logic [15:0] g_zq; logic g_zmsk, g_ztest, g_scene; logic [31:0] g_color;
logic [31:0] z_araddr; logic [7:0] z_arlen; logic [2:0] z_arsize; logic [1:0] z_arburst; logic z_arvalid, z_arready;
logic [255:0] z_rdata; logic [1:0] z_rresp; logic z_rlast, z_rvalid, z_rready;
logic [31:0] z_awaddr; logic [7:0] z_awlen; logic [2:0] z_awsize; logic [1:0] z_awburst; logic z_awvalid, z_awready;
logic [255:0] z_wdata; logic [31:0] z_wstrb; logic z_wlast, z_wvalid, z_wready; logic z_bvalid, z_bready; logic [1:0] z_bresp;
logic [31:0] c_awaddr; logic [7:0] c_awlen; logic [2:0] c_awsize; logic [1:0] c_awburst; logic c_awvalid, c_awready;
logic [255:0] c_wdata; logic [31:0] c_wstrb; logic c_wlast, c_wvalid, c_wready; logic c_bvalid, c_bready; logic [1:0] c_bresp;
logic [31:0] z_beats_read, z_beats_written, c_beats_written, col_ovf, bresp_err; logic idle;
gs_lpddr_zc_emit #(.COLBASE(COLBASE), .ZBASE(ZBASE), .FB_PXW(FB_PXW), .FB_H(FB_H), .REQ_DEPTH(32), .COL_DEPTH(64)) dut (
.gs_clk(gs_clk), .gs_rst_n(gs_rst_n), .enable(enable),
.g_valid(g_valid), .g_ready(g_ready), .g_x(g_x), .g_y(g_y), .g_zq(g_zq), .g_zmsk(g_zmsk),
.g_ztest(g_ztest), .g_ztst(2'd2), .g_color(g_color), .g_be(4'hF), .g_scene(g_scene),
.axi_clk(axi_clk), .axi_rst_n(axi_rst_n), .clear_start(clear_start), .clear_done(clear_done), .frame_drained(frame_drained),
.z_araddr(z_araddr), .z_arlen(z_arlen), .z_arsize(z_arsize), .z_arburst(z_arburst), .z_arvalid(z_arvalid), .z_arready(z_arready),
.z_rdata(z_rdata), .z_rresp(z_rresp), .z_rlast(z_rlast), .z_rvalid(z_rvalid), .z_rready(z_rready),
.z_awaddr(z_awaddr), .z_awlen(z_awlen), .z_awsize(z_awsize), .z_awburst(z_awburst), .z_awvalid(z_awvalid), .z_awready(z_awready),
.z_wdata(z_wdata), .z_wstrb(z_wstrb), .z_wlast(z_wlast), .z_wvalid(z_wvalid), .z_wready(z_wready),
.z_bvalid(z_bvalid), .z_bready(z_bready), .z_bresp(z_bresp),
.c_awaddr(c_awaddr), .c_awlen(c_awlen), .c_awsize(c_awsize), .c_awburst(c_awburst), .c_awvalid(c_awvalid), .c_awready(c_awready),
.c_wdata(c_wdata), .c_wstrb(c_wstrb), .c_wlast(c_wlast), .c_wvalid(c_wvalid), .c_wready(c_wready),
.c_bvalid(c_bvalid), .c_bready(c_bready), .c_bresp(c_bresp),
.z_beats_read(z_beats_read), .z_beats_written(z_beats_written), .c_beats_written(c_beats_written),
.col_ovf(col_ovf), .bresp_err(bresp_err), .idle(idle)
);
logic [15:0] lf=16'hCAFE; always_ff @(posedge axi_clk) lf<={lf[14:0], lf[15]^lf[13]^lf[12]^lf[10]};
// ---- Z LPDDR slave (256b, random backpressure) ----
logic [255:0] zmem [0:ZBEATS-1];
logic [31:0] zra; logic zrp; logic [3:0] zrd; logic [31:0] zwa; logic [255:0] zwd; logic zaw,zw; logic [3:0] zbd; logic zbp;
always_ff @(posedge axi_clk or negedge axi_rst_n) begin
if(!axi_rst_n) begin z_arready<=0;z_rvalid<=0;z_rlast<=0;z_rresp<=0;z_rdata<=0;zrp<=0;zrd<=0;
z_awready<=0;z_wready<=0;z_bvalid<=0;z_bresp<=0;zaw<=0;zw<=0;zbp<=0;zbd<=0; end
else begin
z_arready<=(!zrp&&!z_rvalid)?lf[0]:1'b0;
if(z_arvalid&&z_arready) begin zra<=z_araddr;zrp<=1;zrd<={1'b0,lf[3:1]};z_arready<=0; end
if(zrp&&!z_rvalid) begin if(zrd==0) begin z_rdata<=zmem[(zra-ZBASE)>>5];z_rresp<=0;z_rvalid<=1;z_rlast<=1;zrp<=0; end else zrd<=zrd-1; end
if(z_rvalid&&z_rready) begin z_rvalid<=0;z_rlast<=0; end
z_awready<=(!zaw)?lf[4]:1'b0; z_wready<=(!zw)?lf[5]:1'b0;
if(z_awvalid&&z_awready) begin zwa<=z_awaddr;zaw<=1;z_awready<=0; end
if(z_wvalid&&z_wready) begin zwd<=z_wdata;zw<=1;z_wready<=0; end
if(zaw&&zw&&!zbp&&!z_bvalid) begin zmem[(zwa-ZBASE)>>5]<=zwd;zbp<=1;zbd<={1'b0,lf[8:6]}; end
if(zbp&&!z_bvalid) begin if(zbd==0) begin z_bvalid<=1;z_bresp<=0;zbp<=0;zaw<=0;zw<=0; end else zbd<=zbd-1; end
if(z_bvalid&&z_bready) z_bvalid<=0;
end
end
// ---- Color LPDDR slave (256b, per-byte wstrb, random backpressure) ----
logic [255:0] cmem [0:CBEATS-1];
logic [31:0] cwa; logic [255:0] cwd; logic [31:0] cws; logic caw,cw; logic [3:0] cbd; logic cbp;
always_ff @(posedge axi_clk or negedge axi_rst_n) begin
if(!axi_rst_n) begin c_awready<=0;c_wready<=0;c_bvalid<=0;c_bresp<=0;caw<=0;cw<=0;cbp<=0;cbd<=0; end
else begin
c_awready<=(!caw)?lf[9]:1'b0; c_wready<=(!cw)?lf[11]:1'b0;
if(c_awvalid&&c_awready) begin cwa<=c_awaddr;caw<=1;c_awready<=0; end
if(c_wvalid&&c_wready) begin cwd<=c_wdata;cws<=c_wstrb;cw<=1;c_wready<=0; end
if(caw&&cw&&!cbp&&!c_bvalid) begin
for(int b=0;b<32;b++) if(cws[b]) cmem[(cwa-COLBASE)>>5][b*8+:8]<=cwd[b*8+:8];
cbp<=1;cbd<={1'b0,lf[14:12]};
end
if(cbp&&!c_bvalid) begin if(cbd==0) begin c_bvalid<=1;c_bresp<=0;cbp<=0;caw<=0;cw<=0; end else cbd<=cbd-1; end
if(c_bvalid&&c_bready) c_bvalid<=0;
end
end
// disjoint-range monitors
always_ff @(posedge axi_clk) if(axi_rst_n) begin
if(z_arvalid && (z_araddr<ZBASE || z_araddr>=ZBASE+ZBEATS*32)) begin $error("Z AR %h OOR",z_araddr);errors++; end
if(z_awvalid && (z_awaddr<ZBASE || z_awaddr>=ZBASE+ZBEATS*32)) begin $error("Z AW %h OOR",z_awaddr);errors++; end
if(c_awvalid && (c_awaddr<COLBASE || c_awaddr>=COLBASE+CBEATS*32)) begin $error("C AW %h OOR",c_awaddr);errors++; end
end
// ---- independent scoreboard (clamp16 persistent-Z over the fed fragments) ----
logic [15:0] sb_z [0:NPX-1]; logic [31:0] sb_col [0:NPX-1]; logic sb_seen [0:NPX-1];
function automatic logic [15:0] cl16(input logic [31:0] z); cl16=(|z[31:16])?16'hFFFF:z[15:0]; endfunction
// ---- feed one fragment: drive g_*, wait acceptance (exactly once), update the scoreboard ----
int nfed;
task automatic feed_frag(input int ep, input int x, input int y, input int z, input logic [31:0] col);
int o; logic [15:0] zq; logic pass;
@(negedge gs_clk);
g_valid<=1; g_scene<=0; g_x<=x[11:0]; g_y<=y[11:0]; g_zq<=cl16(z); g_zmsk<=0; g_ztest<=1; g_color<=col;
@(posedge gs_clk);
while(!(g_valid && g_ready)) @(posedge gs_clk); // wait acceptance — NEVER drop a fragment
@(negedge gs_clk); g_valid<=0;
// scoreboard (in feed order == ROP processing order)
o=y*FB_PXW+x; zq=cl16(z); pass=(zq>=sb_z[o]);
if(pass) begin sb_col[o]=col; sb_seen[o]=1; sb_z[o]=zq; end
nfed++;
if(lf[7]) repeat(1+lf[1:0]) @(posedge gs_clk); // random inter-fragment gap
endtask
task automatic send_marker();
@(negedge gs_clk); g_valid<=1; g_scene<=1; @(posedge gs_clk);
while(!(g_valid && g_ready)) @(posedge gs_clk);
@(negedge gs_clk); g_valid<=0; g_scene<=0;
endtask
int fh, r, ep, x, y, z, cep, drains; logic [31:0] col; logic [1023:0] line;
initial begin
errors=0; enable=0; clear_start=0; g_valid=0; g_scene=0; g_x=0; g_y=0; g_zq=0; g_zmsk=0; g_ztest=1; g_color=0; nfed=0; drains=0;
for(int i=0;i<NPX;i++) begin sb_z[i]=16'h0000; sb_col[i]=32'd0; sb_seen[i]=1'b0; end
for(int b=0;b<CBEATS;b++) cmem[b]=256'd0; // host preclears the color FB
gs_rst_n=0; axi_rst_n=0; repeat(6) @(posedge axi_clk); gs_rst_n=1; axi_rst_n=1; repeat(4) @(posedge axi_clk);
enable=1;
@(negedge axi_clk) clear_start=1; @(negedge axi_clk) clear_start=0; // preclear Z once
begin int g=0; while(!clear_done && g<400000) begin @(posedge axi_clk); g++; end end
if(!clear_done) begin $error("[zrop] Z preclear timeout"); errors++; end
fh=$fopen("zsched_frags.txt","r");
if(fh==0) begin $display("[tb_top_psmct32_sh3_zrop] SKIP — zsched_frags.txt absent (run make tb_top_psmct32_sh3_zsched)"); $finish; end
cep=0;
while(!$feof(fh)) begin
// zsched_frags.txt currently carries "epoch x y z color u v". Read one full line so the optional
// trailing texture-debug fields cannot be re-parsed as a bogus next fragment.
if(!$fgets(line, fh)) begin end
r=$sscanf(line, "%d %d %d %d %h", ep, x, y, z, col);
if(r==5) begin
if(x<0 || x>=FB_PXW || y<0 || y>=FB_H) begin
$error("[zrop] trace coord OOR ep=%0d x=%0d y=%0d z=%0d col=%08x", ep, x, y, z, col);
errors++;
end else begin
if(ep!=cep) begin // epoch boundary -> ordered scene drain
send_marker();
begin int g=0; while(!frame_drained && g<800000) begin @(posedge axi_clk); g++; end end
if(!frame_drained) begin $error("[zrop] epoch %0d: frame_drained never rose",cep); errors++; end
else drains++;
cep=ep;
end
feed_frag(ep, x, y, z, col);
end
end
end
$fclose(fh);
// final scene marker (last epoch)
send_marker();
begin int g=0; while(!frame_drained && g<800000) begin @(posedge axi_clk); g++; end end
if(!frame_drained) begin $error("[zrop] final: frame_drained never rose"); errors++; end else drains++;
repeat(60) @(posedge axi_clk);
// ---- gates: final Z + color LPDDR == scoreboard ; suppression ; counts ----
for(int o=0;o<NPX;o++) begin
logic [15:0] zs; zs=zmem[o>>4][(o[3:0])*16+:16];
if(zs!==sb_z[o]) begin if(errors<12)$error("[zrop] Z px%0d=%04x exp %04x",o,zs,sb_z[o]);errors++; end
end
for(int o=0;o<NPX;o++) begin
logic [31:0] cs; cs=cmem[o>>3][(o[2:0])*32+:32];
if(sb_seen[o]) begin if(cs!==sb_col[o]) begin if(errors<12)$error("[zrop] COLOR px%0d=%08x exp %08x",o,cs,sb_col[o]);errors++; end end
else begin if(cs!==32'd0) begin if(errors<12)$error("[zrop] px%0d never-passed but color=%08x (suppression fail)",o,cs);errors++; end end
end
if(col_ovf!==0) begin $error("[zrop] col_ovf=%0d",col_ovf);errors++; end
if(bresp_err!==0)begin $error("[zrop] bresp_err=%0d",bresp_err);errors++; end
$display("[zrop] fed=%0d fragments, scene-drains=%0d, z_wr=%0d c_wr=%0d col_ovf=%0d bresp_err=%0d errors=%0d",
nfed, drains, z_beats_written, c_beats_written, col_ovf, bresp_err, errors);
if(errors==0) $display("[tb_top_psmct32_sh3_zrop] PASS"); else $display("[tb_top_psmct32_sh3_zrop] FAIL");
$finish;
end
initial begin #400000000; $error("[tb_top_psmct32_sh3_zrop] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_zrop
+448
View File
@@ -0,0 +1,448 @@
// retroDE_ps2 — tb_top_psmct32_sh3_zs640_cap (Ch358 — authentic-Z capture at NATIVE 640x480, strong-reject scene)
//
// 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_zs640_params.vh). Default
// N_EPOCHS=3 strong-reject trio: E0=idx8634 E1=idx12757 E2=idx145742 at AUTHENTIC native coords x[407..619] y[97..306].
// Also captures the fragment stream {epoch,x,y,persp_z,color} -> zs640_frags.txt. Proves the 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_zs640_cap;
`include "sh3_zs640_params.vh" // FBPXW=640, FBH=480, 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_zs640%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_zs640%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_zs640%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_zs640%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_zs640%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_zs640_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_zs640_cap] SKIP — sh3_zs640*.mem absent (run gs_make_sh3_scheduler_fixture.py --authz --fb640 --tag zs640 --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("zs640_frags.txt","w"); // Ch357 fragment capture for external persp_z5 verification
tex_fh = $fopen("zs640_textrace.txt","w"); // sim-only texture lookup trace for perspective-color diagnosis
issue_fh = $fopen("zs640_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_zs640_cap] 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_zs640_cap] PASS"); else $display("[tb_top_psmct32_sh3_zs640_cap] FAIL");
$finish;
end
initial begin #320000000; $error("[tb_top_psmct32_sh3_zs640_cap] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_zs640_cap
@@ -0,0 +1,474 @@
// retroDE_ps2 — tb_top_psmct32_sh3_zs640_shared_cap (Ch360 — four-epoch shared-texture capture at native 640x480)
//
// Four authentic SH3 draw groups share one resident TEX0/CLUT and accumulate into one LPDDR framebuffer via the
// C12 epoch descriptor table. Each group keeps the proven 204-triangle feeder-list capacity.
// Also captures the fragment stream {epoch,x,y,persp_z,color} -> zs640c12_frags.txt. Proves the 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.
`ifndef SH3_SHARED_PARAMS
`error "Define SH3_SHARED_PARAMS as a quoted generated params header"
`endif
`ifndef SH3_SHARED_TAG
`error "Define SH3_SHARED_TAG as a quoted fixture tag"
`endif
`ifndef SH3_SHARED_EPOCH_TABLE
`error "Define SH3_SHARED_EPOCH_TABLE as a quoted generated descriptor table"
`endif
`timescale 1ns/1ps
module tb_top_psmct32_sh3_zs640_shared_cap;
`include `SH3_SHARED_PARAMS
localparam string FIXTURE_TAG = `SH3_SHARED_TAG;
localparam int MAX_EPOCHS = 16;
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; // 4
// per-epoch expected CRC / records (from the descriptor params)
// Ch360 — four epochs share one texture: EPk_REUSE=1 means the epoch runs on the resident cache without a fill.
logic [31:0] EP_CRC [0:MAX_EPOCHS-1]; int EP_REC [0:MAX_EPOCHS-1]; bit EP_REUSE [0:MAX_EPOCHS-1]; int fills;
initial begin
`include `SH3_SHARED_EPOCH_TABLE
fills=0;
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), .RAM_SIZE_BYTES(32 * 1024), .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:MAX_EPOCHS-1][0:(512*512/4)-1]; logic [31:0] pal [0:MAX_EPOCHS-1][0:255];
logic [31:0] refmap [0:FB_WORDS-1]; logic [31:0] refmap_ep [0:MAX_EPOCHS-1][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_%s%0d.mem", FIXTURE_TAG, 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<4000000) 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<4000000) 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;
fills=fills+1;
@(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_%s%0d_tex_lpddr.mem", FIXTURE_TAG, k);
if (!EP_REUSE[k] || only_k>=0) begin // isolation (+ONLY=k) always fills — nothing resident
$readmemh(fn, lpddr_mem);
fill_cache(k, EP_CRC[k]);
end else begin
// Ch359 — EXPLICIT REUSE: no reload, no fill pulse. Fail-closed residency: the fill CRC register is
// only rewritten by a fill, so it must still hold the prior epoch's verified CRC (same shared texture).
if (fill_crc_w!==EP_CRC[k]) begin $error("%s epoch %0d: REUSE but resident crc=%08x exp %08x — residency broken", "[sched]", k, fill_crc_w, EP_CRC[k]); errors++; end
else $display("%s epoch %0d: REUSE resident texture (crc=0x%08x, no fill)", "[sched]", k, fill_crc_w);
end
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_%s%0d_idx.mem", FIXTURE_TAG, 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_%s%0d_pal.mem", FIXTURE_TAG, e), t_pal);
for (int i=0;i<256;i++) pal[e][i]=t_pal[i];
$readmemh($sformatf("../../data/top_psmct32_raster_demo/sh3_%s%0d_refmap.mem", FIXTURE_TAG, e), t_rm);
for (int i=0;i<FB_WORDS;i++) refmap_ep[e][i]=t_rm[i];
end
$readmemh($sformatf("../../data/top_psmct32_raster_demo/sh3_%s_refmap.mem", FIXTURE_TAG), 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_zs640_shared_cap] SKIP — sh3_%s*.mem absent", FIXTURE_TAG); $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($sformatf("%s_frags.txt", FIXTURE_TAG),"w");
tex_fh = $fopen($sformatf("%s_textrace.txt", FIXTURE_TAG),"w");
issue_fh = $fopen($sformatf("%s_issue.txt", FIXTURE_TAG),"w");
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
// A descriptor marked resident reuses the prior cache image; every non-resident descriptor must refill it.
begin
int exp_fills; exp_fills=0;
if (only_k>=0) exp_fills=1;
else for (int k=0;k<NEP;k++) if (!EP_REUSE[k]) exp_fills++;
if (fills!==exp_fills) begin $error("[sched] fills=%0d exp=%0d from descriptor reuse flags", fills, exp_fills); errors++; end
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_zs640_shared_cap] 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_zs640_shared_cap] PASS"); else $display("[tb_top_psmct32_sh3_zs640_shared_cap] FAIL");
$finish;
end
initial begin #320000000; $error("[tb_top_psmct32_sh3_zs640_shared_cap] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_zs640_shared_cap
@@ -0,0 +1,460 @@
// retroDE_ps2 — tb_top_psmct32_sh3_zs640c12_cap (Ch360 — four-epoch shared-texture capture at native 640x480)
//
// Four authentic SH3 draw groups share one resident TEX0/CLUT and accumulate into one LPDDR framebuffer via the
// C12 epoch descriptor table. Each group keeps the proven 204-triangle feeder-list capacity.
// Also captures the fragment stream {epoch,x,y,persp_z,color} -> zs640c12_frags.txt. Proves the 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_zs640c12_cap;
`include "sh3_zs640c12_params.vh" // FBPXW=640, FBH=480, N_EPOCHS=4, 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; // 4
// per-epoch expected CRC / records (from the descriptor params)
// Ch360 — four epochs share one texture: EPk_REUSE=1 means the epoch runs on the resident cache without a fill.
logic [31:0] EP_CRC [0:3]; int EP_REC [0:3]; bit EP_REUSE [0:3]; int fills;
initial begin
EP_CRC[0]=EP0_CRC; EP_CRC[1]=EP1_CRC; EP_CRC[2]=EP2_CRC; EP_CRC[3]=EP3_CRC;
EP_REC[0]=EP0_NTRIS; EP_REC[1]=EP1_NTRIS; EP_REC[2]=EP2_NTRIS; EP_REC[3]=EP3_NTRIS;
EP_REUSE[0]=EP0_REUSE; EP_REUSE[1]=EP1_REUSE; EP_REUSE[2]=EP2_REUSE; EP_REUSE[3]=EP3_REUSE;
fills=0;
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:3][0:(512*512/4)-1]; logic [31:0] pal [0:3][0:255];
logic [31:0] refmap [0:FB_WORDS-1]; logic [31:0] refmap_ep [0:3][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_zs640c12%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<4000000) 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<4000000) 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;
fills=fills+1;
@(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_zs640c12%0d_tex_lpddr.mem", k);
if (!EP_REUSE[k] || only_k>=0) begin // isolation (+ONLY=k) always fills — nothing resident
$readmemh(fn, lpddr_mem);
fill_cache(k, EP_CRC[k]);
end else begin
// Ch359 — EXPLICIT REUSE: no reload, no fill pulse. Fail-closed residency: the fill CRC register is
// only rewritten by a fill, so it must still hold the prior epoch's verified CRC (same shared texture).
if (fill_crc_w!==EP_CRC[k]) begin $error("%s epoch %0d: REUSE but resident crc=%08x exp %08x — residency broken", "[sched]", k, fill_crc_w, EP_CRC[k]); errors++; end
else $display("%s epoch %0d: REUSE resident texture (crc=0x%08x, no fill)", "[sched]", k, fill_crc_w);
end
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_zs640c12%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_zs640c12%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_zs640c12%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_zs640c12_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_zs640c12_cap] SKIP — sh3_zs640c12*.mem absent (run gs_make_sh3_scheduler_fixture.py --authz --fb640 --tag zs640c12 --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("zs640c12_frags.txt","w"); // Ch357 fragment capture for external persp_z5 verification
tex_fh = $fopen("zs640c12_textrace.txt","w"); // sim-only texture lookup trace for perspective-color diagnosis
issue_fh = $fopen("zs640c12_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
// Ch359 — the WHOLE run must perform exactly ONE verified texture fill (epoch 1 reuses the resident cache)
if (fills!==1) begin $error("[sched] expected exactly ONE texture fill, got %0d (residency contract)", fills); 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_zs640c12_cap] 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_zs640c12_cap] PASS"); else $display("[tb_top_psmct32_sh3_zs640c12_cap] FAIL");
$finish;
end
initial begin #320000000; $error("[tb_top_psmct32_sh3_zs640c12_cap] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_zs640c12_cap
@@ -0,0 +1,461 @@
// retroDE_ps2 — tb_top_psmct32_sh3_zs640c6_cap (Ch358 — authentic-Z capture at NATIVE 640x480, strong-reject scene)
//
// 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_zs640c6_params.vh). Default
// N_EPOCHS=3 strong-reject trio: E0=idx8634 E1=idx12757 E2=idx145742 at AUTHENTIC native coords x[407..619] y[97..306].
// Also captures the fragment stream {epoch,x,y,persp_z,color} -> zs640c6_frags.txt. Proves the 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_zs640c6_cap;
`include "sh3_zs640c6_params.vh" // FBPXW=640, FBH=480, 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)
// Ch359 — TWO epochs sharing ONE texture: EPk_REUSE=1 means the epoch runs on the RESIDENT cache (no fill).
logic [31:0] EP_CRC [0:1]; int EP_REC [0:1]; bit EP_REUSE [0:1]; int fills;
initial begin
EP_CRC[0]=EP0_CRC; EP_CRC[1]=EP1_CRC;
EP_REC[0]=EP0_NTRIS; EP_REC[1]=EP1_NTRIS;
EP_REUSE[0]=EP0_REUSE; EP_REUSE[1]=EP1_REUSE;
fills=0;
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_zs640c6%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<4000000) 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<4000000) 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;
fills=fills+1;
@(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_zs640c6%0d_tex_lpddr.mem", k);
if (!EP_REUSE[k] || only_k>=0) begin // isolation (+ONLY=k) always fills — nothing resident
$readmemh(fn, lpddr_mem);
fill_cache(k, EP_CRC[k]);
end else begin
// Ch359 — EXPLICIT REUSE: no reload, no fill pulse. Fail-closed residency: the fill CRC register is
// only rewritten by a fill, so it must still hold the prior epoch's verified CRC (same shared texture).
if (fill_crc_w!==EP_CRC[k]) begin $error("%s epoch %0d: REUSE but resident crc=%08x exp %08x — residency broken", "[sched]", k, fill_crc_w, EP_CRC[k]); errors++; end
else $display("%s epoch %0d: REUSE resident texture (crc=0x%08x, no fill)", "[sched]", k, fill_crc_w);
end
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_zs640c6%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_zs640c6%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_zs640c6%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_zs640c6_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_zs640c6_cap] SKIP — sh3_zs640c6*.mem absent (run gs_make_sh3_scheduler_fixture.py --authz --fb640 --tag zs640c6 --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("zs640c6_frags.txt","w"); // Ch357 fragment capture for external persp_z5 verification
tex_fh = $fopen("zs640c6_textrace.txt","w"); // sim-only texture lookup trace for perspective-color diagnosis
issue_fh = $fopen("zs640c6_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
// Ch359 — the WHOLE run must perform exactly ONE verified texture fill (epoch 1 reuses the resident cache)
if (fills!==1) begin $error("[sched] expected exactly ONE texture fill, got %0d (residency contract)", fills); 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_zs640c6_cap] 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_zs640c6_cap] PASS"); else $display("[tb_top_psmct32_sh3_zs640c6_cap] FAIL");
$finish;
end
initial begin #320000000; $error("[tb_top_psmct32_sh3_zs640c6_cap] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_zs640c6_cap
+447
View File
@@ -0,0 +1,447 @@
// 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
+178
View File
@@ -0,0 +1,178 @@
// retroDE_ps2 — tb_top_psmct32_sh3_ztrio (Ch357 — persistent-Z ROP acceptance gate, TRACE-based scoreboard)
//
// Codex option A: verify the LPDDR-Z ROP (gs_lpddr_zc_emit) against the raster's ACTUAL emitted fragments. This TB replays
// the captured fragment trace (tb_top_psmct32_sh3_zsched -> ztrio_frags.txt: "epoch x y persp_z color" per line) through
// zc_emit across two clock domains, with a per-epoch scene marker, and scores it with an INDEPENDENT software scoreboard
// (clamp16 / GEQUAL / persistence). Proves Codex's Ch357 gates:
// * every replayed fragment enters the ROP EXACTLY once (we feed each once and wait g_ready — no drop, no double).
// * the scoreboard predicts every pass/reject and the final packed 16-bit Z; failed depth tests emit NO color.
// * final color + Z LPDDR memories match the scoreboard EXACTLY.
// * the ordered end-of-scene marker drain waits BOTH pipelines (color + Z BRESPs) before frame_drained.
// This is the ROP-correctness gate; the raster's Z-interp fidelity (float-oracle 3971 / 87.6% owner agreement) is a SEPARATE
// recorded limitation, not tested here. Primary fixture = sh3_zsched [8634,12757,145742], FB 256x210, Z base 0x140000.
`timescale 1ns/1ps
module tb_top_psmct32_sh3_ztrio;
localparam int FB_PXW=384, FB_H=381;
localparam int NPX=FB_PXW*FB_H; // 53760
localparam int ZBEATS=(NPX+15)/16; // 3360
localparam int CBEATS=(NPX+7)/8; // 6720
localparam [31:0] COLBASE=32'h0000_0000, ZBASE=32'h0014_0000;
localparam int ZBW=$clog2(ZBEATS), CBW=$clog2(CBEATS);
logic gs_clk=0; always #5 gs_clk=~gs_clk;
logic axi_clk=0; always #2 axi_clk=~axi_clk; // emif faster than design -> ROP drains faster than the trace feeds
logic gs_rst_n, axi_rst_n; int errors; initial errors=0;
logic enable, clear_start, clear_done, frame_drained;
logic g_valid, g_ready; logic [11:0] g_x, g_y; logic [15:0] g_zq; logic g_zmsk, g_ztest, g_scene; logic [31:0] g_color;
logic [31:0] z_araddr; logic [7:0] z_arlen; logic [2:0] z_arsize; logic [1:0] z_arburst; logic z_arvalid, z_arready;
logic [255:0] z_rdata; logic [1:0] z_rresp; logic z_rlast, z_rvalid, z_rready;
logic [31:0] z_awaddr; logic [7:0] z_awlen; logic [2:0] z_awsize; logic [1:0] z_awburst; logic z_awvalid, z_awready;
logic [255:0] z_wdata; logic [31:0] z_wstrb; logic z_wlast, z_wvalid, z_wready; logic z_bvalid, z_bready; logic [1:0] z_bresp;
logic [31:0] c_awaddr; logic [7:0] c_awlen; logic [2:0] c_awsize; logic [1:0] c_awburst; logic c_awvalid, c_awready;
logic [255:0] c_wdata; logic [31:0] c_wstrb; logic c_wlast, c_wvalid, c_wready; logic c_bvalid, c_bready; logic [1:0] c_bresp;
logic [31:0] z_beats_read, z_beats_written, c_beats_written, col_ovf, bresp_err; logic idle;
gs_lpddr_zc_emit #(.COLBASE(COLBASE), .ZBASE(ZBASE), .FB_PXW(FB_PXW), .FB_H(FB_H), .REQ_DEPTH(32), .COL_DEPTH(64)) dut (
.gs_clk(gs_clk), .gs_rst_n(gs_rst_n), .enable(enable),
.g_valid(g_valid), .g_ready(g_ready), .g_x(g_x), .g_y(g_y), .g_zq(g_zq), .g_zmsk(g_zmsk),
.g_ztest(g_ztest), .g_ztst(2'd2), .g_color(g_color), .g_be(4'hF), .g_scene(g_scene),
.axi_clk(axi_clk), .axi_rst_n(axi_rst_n), .clear_start(clear_start), .clear_done(clear_done), .frame_drained(frame_drained),
.z_araddr(z_araddr), .z_arlen(z_arlen), .z_arsize(z_arsize), .z_arburst(z_arburst), .z_arvalid(z_arvalid), .z_arready(z_arready),
.z_rdata(z_rdata), .z_rresp(z_rresp), .z_rlast(z_rlast), .z_rvalid(z_rvalid), .z_rready(z_rready),
.z_awaddr(z_awaddr), .z_awlen(z_awlen), .z_awsize(z_awsize), .z_awburst(z_awburst), .z_awvalid(z_awvalid), .z_awready(z_awready),
.z_wdata(z_wdata), .z_wstrb(z_wstrb), .z_wlast(z_wlast), .z_wvalid(z_wvalid), .z_wready(z_wready),
.z_bvalid(z_bvalid), .z_bready(z_bready), .z_bresp(z_bresp),
.c_awaddr(c_awaddr), .c_awlen(c_awlen), .c_awsize(c_awsize), .c_awburst(c_awburst), .c_awvalid(c_awvalid), .c_awready(c_awready),
.c_wdata(c_wdata), .c_wstrb(c_wstrb), .c_wlast(c_wlast), .c_wvalid(c_wvalid), .c_wready(c_wready),
.c_bvalid(c_bvalid), .c_bready(c_bready), .c_bresp(c_bresp),
.z_beats_read(z_beats_read), .z_beats_written(z_beats_written), .c_beats_written(c_beats_written),
.col_ovf(col_ovf), .bresp_err(bresp_err), .idle(idle)
);
logic [15:0] lf=16'hCAFE; always_ff @(posedge axi_clk) lf<={lf[14:0], lf[15]^lf[13]^lf[12]^lf[10]};
// ---- Z LPDDR slave (256b, random backpressure) ----
logic [255:0] zmem [0:ZBEATS-1];
logic [31:0] zra; logic zrp; logic [3:0] zrd; logic [31:0] zwa; logic [255:0] zwd; logic zaw,zw; logic [3:0] zbd; logic zbp;
always_ff @(posedge axi_clk or negedge axi_rst_n) begin
if(!axi_rst_n) begin z_arready<=0;z_rvalid<=0;z_rlast<=0;z_rresp<=0;z_rdata<=0;zrp<=0;zrd<=0;
z_awready<=0;z_wready<=0;z_bvalid<=0;z_bresp<=0;zaw<=0;zw<=0;zbp<=0;zbd<=0; end
else begin
z_arready<=(!zrp&&!z_rvalid)?lf[0]:1'b0;
if(z_arvalid&&z_arready) begin zra<=z_araddr;zrp<=1;zrd<={1'b0,lf[3:1]};z_arready<=0; end
if(zrp&&!z_rvalid) begin if(zrd==0) begin z_rdata<=zmem[(zra-ZBASE)>>5];z_rresp<=0;z_rvalid<=1;z_rlast<=1;zrp<=0; end else zrd<=zrd-1; end
if(z_rvalid&&z_rready) begin z_rvalid<=0;z_rlast<=0; end
z_awready<=(!zaw)?lf[4]:1'b0; z_wready<=(!zw)?lf[5]:1'b0;
if(z_awvalid&&z_awready) begin zwa<=z_awaddr;zaw<=1;z_awready<=0; end
if(z_wvalid&&z_wready) begin zwd<=z_wdata;zw<=1;z_wready<=0; end
if(zaw&&zw&&!zbp&&!z_bvalid) begin zmem[(zwa-ZBASE)>>5]<=zwd;zbp<=1;zbd<={1'b0,lf[8:6]}; end
if(zbp&&!z_bvalid) begin if(zbd==0) begin z_bvalid<=1;z_bresp<=0;zbp<=0;zaw<=0;zw<=0; end else zbd<=zbd-1; end
if(z_bvalid&&z_bready) z_bvalid<=0;
end
end
// ---- Color LPDDR slave (256b, per-byte wstrb, random backpressure) ----
logic [255:0] cmem [0:CBEATS-1];
logic [31:0] cwa; logic [255:0] cwd; logic [31:0] cws; logic caw,cw; logic [3:0] cbd; logic cbp;
always_ff @(posedge axi_clk or negedge axi_rst_n) begin
if(!axi_rst_n) begin c_awready<=0;c_wready<=0;c_bvalid<=0;c_bresp<=0;caw<=0;cw<=0;cbp<=0;cbd<=0; end
else begin
c_awready<=(!caw)?lf[9]:1'b0; c_wready<=(!cw)?lf[11]:1'b0;
if(c_awvalid&&c_awready) begin cwa<=c_awaddr;caw<=1;c_awready<=0; end
if(c_wvalid&&c_wready) begin cwd<=c_wdata;cws<=c_wstrb;cw<=1;c_wready<=0; end
if(caw&&cw&&!cbp&&!c_bvalid) begin
for(int b=0;b<32;b++) if(cws[b]) cmem[(cwa-COLBASE)>>5][b*8+:8]<=cwd[b*8+:8];
cbp<=1;cbd<={1'b0,lf[14:12]};
end
if(cbp&&!c_bvalid) begin if(cbd==0) begin c_bvalid<=1;c_bresp<=0;cbp<=0;caw<=0;cw<=0; end else cbd<=cbd-1; end
if(c_bvalid&&c_bready) c_bvalid<=0;
end
end
// disjoint-range monitors
always_ff @(posedge axi_clk) if(axi_rst_n) begin
if(z_arvalid && (z_araddr<ZBASE || z_araddr>=ZBASE+ZBEATS*32)) begin $error("Z AR %h OOR",z_araddr);errors++; end
if(z_awvalid && (z_awaddr<ZBASE || z_awaddr>=ZBASE+ZBEATS*32)) begin $error("Z AW %h OOR",z_awaddr);errors++; end
if(c_awvalid && (c_awaddr<COLBASE || c_awaddr>=COLBASE+CBEATS*32)) begin $error("C AW %h OOR",c_awaddr);errors++; end
end
// ---- independent scoreboard (clamp16 persistent-Z over the fed fragments) ----
logic [15:0] sb_z [0:NPX-1]; logic [31:0] sb_col [0:NPX-1]; logic sb_seen [0:NPX-1];
function automatic logic [15:0] cl16(input logic [31:0] z); cl16=(|z[31:16])?16'hFFFF:z[15:0]; endfunction
// ---- feed one fragment: drive g_*, wait acceptance (exactly once), update the scoreboard ----
int nfed;
task automatic feed_frag(input int ep, input int x, input int y, input int z, input logic [31:0] col);
int o; logic [15:0] zq; logic pass;
@(negedge gs_clk);
g_valid<=1; g_scene<=0; g_x<=x[11:0]; g_y<=y[11:0]; g_zq<=cl16(z); g_zmsk<=0; g_ztest<=1; g_color<=col;
@(posedge gs_clk);
while(!(g_valid && g_ready)) @(posedge gs_clk); // wait acceptance — NEVER drop a fragment
@(negedge gs_clk); g_valid<=0;
// scoreboard (in feed order == ROP processing order)
o=y*FB_PXW+x; zq=cl16(z); pass=(zq>=sb_z[o]);
if(pass) begin sb_col[o]=col; sb_seen[o]=1; sb_z[o]=zq; end
nfed++;
if(lf[7]) repeat(1+lf[1:0]) @(posedge gs_clk); // random inter-fragment gap
endtask
task automatic send_marker();
@(negedge gs_clk); g_valid<=1; g_scene<=1; @(posedge gs_clk);
while(!(g_valid && g_ready)) @(posedge gs_clk);
@(negedge gs_clk); g_valid<=0; g_scene<=0;
endtask
int fh, r, ep, x, y, z, cep, drains; logic [31:0] col;
initial begin
errors=0; enable=0; clear_start=0; g_valid=0; g_scene=0; g_x=0; g_y=0; g_zq=0; g_zmsk=0; g_ztest=1; g_color=0; nfed=0; drains=0;
for(int i=0;i<NPX;i++) begin sb_z[i]=16'h0000; sb_col[i]=32'd0; sb_seen[i]=1'b0; end
for(int b=0;b<CBEATS;b++) cmem[b]=256'd0; // host preclears the color FB
gs_rst_n=0; axi_rst_n=0; repeat(6) @(posedge axi_clk); gs_rst_n=1; axi_rst_n=1; repeat(4) @(posedge axi_clk);
enable=1;
@(negedge axi_clk) clear_start=1; @(negedge axi_clk) clear_start=0; // preclear Z once
begin int g=0; while(!clear_done && g<400000) begin @(posedge axi_clk); g++; end end
if(!clear_done) begin $error("[zrop] Z preclear timeout"); errors++; end
fh=$fopen("ztrio_frags.txt","r");
if(fh==0) begin $display("[tb_top_psmct32_sh3_ztrio] SKIP — ztrio_frags.txt absent (run make tb_top_psmct32_sh3_zsched)"); $finish; end
cep=0;
while(!$feof(fh)) begin
r=$fscanf(fh, "%d %d %d %d %h\n", ep, x, y, z, col);
if(r==5) begin
if(ep!=cep) begin // epoch boundary -> ordered scene drain
send_marker();
begin int g=0; while(!frame_drained && g<800000) begin @(posedge axi_clk); g++; end end
if(!frame_drained) begin $error("[zrop] epoch %0d: frame_drained never rose",cep); errors++; end
else drains++;
cep=ep;
end
feed_frag(ep, x, y, z, col);
end else begin
r=$fgetc(fh); // skip a char past a blank/partial line
end
end
$fclose(fh);
// final scene marker (last epoch)
send_marker();
begin int g=0; while(!frame_drained && g<800000) begin @(posedge axi_clk); g++; end end
if(!frame_drained) begin $error("[zrop] final: frame_drained never rose"); errors++; end else drains++;
repeat(60) @(posedge axi_clk);
// ---- gates: final Z + color LPDDR == scoreboard ; suppression ; counts ----
for(int o=0;o<NPX;o++) begin
logic [15:0] zs; zs=zmem[o>>4][(o[3:0])*16+:16];
if(zs!==sb_z[o]) begin if(errors<12)$error("[zrop] Z px%0d=%04x exp %04x",o,zs,sb_z[o]);errors++; end
end
for(int o=0;o<NPX;o++) begin
logic [31:0] cs; cs=cmem[o>>3][(o[2:0])*32+:32];
if(sb_seen[o]) begin if(cs!==sb_col[o]) begin if(errors<12)$error("[zrop] COLOR px%0d=%08x exp %08x",o,cs,sb_col[o]);errors++; end end
else begin if(cs!==32'd0) begin if(errors<12)$error("[zrop] px%0d never-passed but color=%08x (suppression fail)",o,cs);errors++; end end
end
if(col_ovf!==0) begin $error("[zrop] col_ovf=%0d",col_ovf);errors++; end
if(bresp_err!==0)begin $error("[zrop] bresp_err=%0d",bresp_err);errors++; end
$display("[zrop] fed=%0d fragments, scene-drains=%0d, z_wr=%0d c_wr=%0d col_ovf=%0d bresp_err=%0d errors=%0d",
nfed, drains, z_beats_written, c_beats_written, col_ovf, bresp_err, errors);
if(errors==0) $display("[tb_top_psmct32_sh3_ztrio] PASS"); else $display("[tb_top_psmct32_sh3_ztrio] FAIL");
$finish;
end
initial begin #400000000; $error("[tb_top_psmct32_sh3_ztrio] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_ztrio
+402
View File
@@ -0,0 +1,402 @@
// retroDE_ps2 — tb_top_psmct32_sh3_ztrio_cap (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_ztrio_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_ztrio_cap;
`include "sh3_ztrio_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; // fragment capture (verify persp_z5 vs the clamp16 oracle)
// 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) if (rst_n && flush_emit_w && (flush_psm_w==6'h00) && cap_fh!=0)
$fwrite(cap_fh, "%0d %0d %0d %0d %08x\n", cap_ep, flush_x_w, flush_y_w, flush_z_w, flush_color32_w);
// 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_ztrio%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_ztrio%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; cap_ep=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_ztrio%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_ztrio%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_ztrio%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_ztrio_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_ztrio_cap] SKIP — sh3_ztrio*.mem absent (run gs_make_sh3_ztriouler_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("ztrio_frags.txt","w"); // Ch357 fragment capture for external persp_z5 verification
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_ztrio_cap] 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 (errors==0) $display("[tb_top_psmct32_sh3_ztrio_cap] PASS"); else $display("[tb_top_psmct32_sh3_ztrio_cap] FAIL");
$finish;
end
initial begin #160000000; $error("[tb_top_psmct32_sh3_ztrio_cap] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_ztrio_cap