// 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>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=H_BP)&&(rawx=V_BP)&&(rawy=V_BP&&rawyB 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= (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 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>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)=0 && (tv+dv)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 %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