// retroDE_ps2 — Ch350 synthetic TB: CSM1 16×16 CT32 grid CLUT load. // // Proves the parameter-gated CSM1-grid path in clut_loader_stub maps palette index i to the VRAM word the // real GS grid order points at — NOT the linear i*4 word. A deliberately permuted layout makes the two // orders disagree for most entries, so a pass can only happen if the grid swizzle is correct. // // Two instances share one behavioural VRAM model: // u_csm1 : CLUT_CSM1_ENABLE=1, driven with csm=0 (CSM1) → must read via the grid offset. // u_csm2 : CLUT_CSM1_ENABLE=1, driven with csm=1 (CSM2) → must STILL read linear i*4 (regression guard: // enabling the param must not perturb CSM2 behaviour). // A third check: u_csm1 driven with csm=0 but CLUT_CSM1_ENABLE=0 (separate instance u_off) must NOT load at // all (load_busy never asserts) — byte-identical-to-legacy proof. `timescale 1ns/1ps module tb_clut_loader_csm1; logic clk = 0, rst_n = 0; always #5 clk = ~clk; // ---- behavioural VRAM: 256 PSMCT32 words at CBP=0 (offsets 0..1020) ---- logic [31:0] vram [0:255]; // CSM1 grid offset (bytes) for entry i — MUST match clut_loader_stub.addr_offset_csm1 exactly. function automatic logic [31:0] off_csm1(input logic [7:0] i); logic [31:0] blk, by32, bx4; blk = ({30'd0, i[7], i[3]}) << 8; // block * 256 by32 = ({29'd0, i[6:4]}) << 5; // iy[2:0] * 32 bx4 = ({29'd0, i[2:0]}) << 2; // ix[2:0] * 4 return blk + by32 + bx4; endfunction // The "logical palette" we want index i to resolve to. Place it at the GRID address for i, so that // reading linearly (word i) lands on a DIFFERENT logical entry for every permuted index. logic [31:0] expect_pal [0:255]; // grid expectation integer k; initial begin for (k = 0; k < 256; k = k + 1) expect_pal[k] = 32'hA0000000 | k; // distinct per index // scatter expect_pal[i] into VRAM at the grid byte-offset for i for (k = 0; k < 256; k = k + 1) vram[off_csm1(k[7:0]) >> 2] = expect_pal[k]; end // ---- DUT ports (shared stimulus) ---- logic tex0_wr_pulse; logic [13:0] tex0_cbp; logic [3:0] tex0_cpsm; logic tex0_csm; logic [4:0] tex0_csa; logic [2:0] tex0_cld; logic [31:0] a_csm1, a_csm2, a_off; logic we_csm1, we_csm2, we_off; logic [7:0] wi_csm1, wi_csm2, wi_off; logic [31:0] wd_csm1, wd_csm2, wd_off; logic busy_csm1, busy_csm2, busy_off; // combinational VRAM read for each instance's addr wire [31:0] rd_csm1 = vram[a_csm1[9:2]]; wire [31:0] rd_csm2 = vram[a_csm2[9:2]]; wire [31:0] rd_off = vram[a_off [9:2]]; clut_loader_stub #(.CLUT_CSM1_ENABLE(1'b1)) u_csm1 ( .clk(clk), .rst_n(rst_n), .tex0_wr_pulse(tex0_wr_pulse), .tex0_cbp(tex0_cbp), .tex0_cpsm(tex0_cpsm), .tex0_csm(1'b0), .tex0_csa(tex0_csa), .tex0_cld(tex0_cld), .vram_read_addr(a_csm1), .vram_read_data(rd_csm1), .clut_write_en(we_csm1), .clut_write_idx(wi_csm1), .clut_write_data(wd_csm1), .load_busy(busy_csm1)); clut_loader_stub #(.CLUT_CSM1_ENABLE(1'b1)) u_csm2 ( .clk(clk), .rst_n(rst_n), .tex0_wr_pulse(tex0_wr_pulse), .tex0_cbp(tex0_cbp), .tex0_cpsm(tex0_cpsm), .tex0_csm(1'b1), .tex0_csa(tex0_csa), .tex0_cld(tex0_cld), .vram_read_addr(a_csm2), .vram_read_data(rd_csm2), .clut_write_en(we_csm2), .clut_write_idx(wi_csm2), .clut_write_data(wd_csm2), .load_busy(busy_csm2)); clut_loader_stub #(.CLUT_CSM1_ENABLE(1'b0)) u_off ( .clk(clk), .rst_n(rst_n), .tex0_wr_pulse(tex0_wr_pulse), .tex0_cbp(tex0_cbp), .tex0_cpsm(tex0_cpsm), .tex0_csm(1'b0), .tex0_csa(tex0_csa), .tex0_cld(tex0_cld), .vram_read_addr(a_off), .vram_read_data(rd_off), .clut_write_en(we_off), .clut_write_idx(wi_off), .clut_write_data(wd_off), .load_busy(busy_off)); // capture each instance's writes logic [31:0] got_csm1 [0:255]; logic [31:0] got_csm2 [0:255]; integer wr_off_count = 0; always_ff @(posedge clk) begin if (we_csm1) got_csm1[wi_csm1] <= wd_csm1; if (we_csm2) got_csm2[wi_csm2] <= wd_csm2; if (we_off) wr_off_count <= wr_off_count + 1; // must stay 0 end integer i, errors = 0, permuted = 0; initial begin tex0_wr_pulse = 0; tex0_cbp = 14'd0; tex0_cpsm = 4'd0; tex0_csm = 1'b0; tex0_csa = 5'd0; tex0_cld = 3'd1; // CLD=1 = always full load repeat (4) @(posedge clk); rst_n = 1; @(posedge clk); // fire one TEX0 commit pulse to all three loaders tex0_wr_pulse <= 1'b1; @(posedge clk); tex0_wr_pulse <= 1'b0; // a full load is 256 cycles; wait comfortably past it. (Can't `wait(busy==0)` — busy is already 0 // in S_IDLE before the load starts, so that returns immediately.) repeat (300) @(posedge clk); // (1) CSM1 grid: got_csm1[i] must equal expect_pal[i] for (i = 0; i < 256; i = i + 1) begin if (got_csm1[i] !== expect_pal[i]) begin if (errors < 8) $display(" CSM1 MISMATCH idx %0d: got %08x exp %08x", i, got_csm1[i], expect_pal[i]); errors = errors + 1; end end // (2) CSM2 linear: got_csm2[i] must equal vram[i] (the raw linear word). Count how many indices // where linear != grid — proves the test is non-trivial (the orders genuinely disagree). for (i = 0; i < 256; i = i + 1) begin if (got_csm2[i] !== vram[i]) begin if (errors < 16) $display(" CSM2 LINEAR BROKEN idx %0d: got %08x exp(linear) %08x", i, got_csm2[i], vram[i]); errors = errors + 1; end if (vram[i] !== expect_pal[i]) permuted = permuted + 1; end // (3) param OFF: csm=0 must have produced NO writes at all if (wr_off_count != 0) begin $display(" PARAM-OFF BROKEN: csm=0 produced %0d writes (expected 0)", wr_off_count); errors = errors + 1; end $display("[tb_clut_loader_csm1] grid-vs-linear disagreeing indices=%0d/256 (test non-triviality)", permuted); if (errors == 0 && permuted > 64) $display("[tb_clut_loader_csm1] PASS"); else $display("[tb_clut_loader_csm1] FAIL errors=%0d permuted=%0d", errors, permuted); $finish; end endmodule