Files
retroDE_ps2/sim/tb/gif_gs/tb_gs_texture_bilinear.sv
thejayman77 ec82764bef Initial commit: retroDE_ps2 — first-of-its-kind PS2 GS FPGA core (DE25-Nano / Agilex 5)
RTL (GS rasterizer, EE core stub, platform bridge, LPDDR4B path), sim regression
(272 TBs), docs, and tooling. Copyrighted PS2 content (BIOS, game code, GS dumps,
and all dump-derived textures/traces) is excluded via .gitignore and stays local.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 20:10:50 -04:00

539 lines
25 KiB
Systemverilog

// retroDE_ps2 — tb_gs_texture_bilinear (Ch308)
//
// Focused TB for BILINEAR (4-tap) PSMCT32 texture filtering at the sampler
// level. gs_texture_unit is instantiated with BILINEAR_ENABLE=1 +
// TEX_WRAP_ENABLE=1; for a fractional texel coord it fetches the 4 surrounding
// texels (u,v)(u+1,v)(u,v+1)(u+1,v+1), each independently wrapped/clamped, and
// blends them per channel by the 4-bit fractional u_frac/v_frac using the
// >>>4 fixed-point lerp:
// lerp(a,b,f) = a + (($signed(b)-$signed(a)) * $signed(f)) >>> 4 , f in 0..15
// The expected values below mirror that lerp EXACTLY (same >>4, same rounding),
// so every check is an exact == match, not a tolerance.
//
// 4x4 PSMCT32 texture (TW=TH=2 => width=height=4), TBW=1, distinct ABGR per
// texel so 4-tap blends are individually checkable.
//
// PROOF CASES:
// 1. EXACT CENTER (uf=0,vf=0) : result == texel(u,v) (== nearest).
// 2. HALFWAY (uf=8,vf=8) : result == 4-neighbor blend (lerp).
// 3. HALFWAY-U (uf=8,vf=0) : result == lerp(texel(u,v),texel(u+1,v)).
// 4. CLAMP edge (wms=wmt=1,u=W-1) : (u+1) clamps to W-1 => result ==
// texel(W-1,v); issued addrs <= last col.
// 5. REPEAT edge (wms=wmt=0,u=W-1) : (u+1) wraps to 0 => result ==
// lerp(texel(W-1,v),texel(0,v),uf).
// 6. NEAREST UNCHANGED: a 2nd instance with BILINEAR_ENABLE=0 returns the
// single nearest texel and ignores u_frac/v_frac.
//
// Prints [tb_gs_texture_bilinear] PASS / FAIL + $finish.
`timescale 1ns/1ps
module tb_gs_texture_bilinear;
localparam logic [5:0] PSMCT32 = 6'h00;
localparam logic [1:0] WM_REPEAT = 2'd0;
localparam logic [1:0] WM_CLAMP = 2'd1;
logic clk;
logic rst_n;
initial clk = 1'b0;
always #5 clk = ~clk;
// ---- tiny combinational VRAM model (word-addressed little-endian) ----
localparam int VRAM_BYTES = 16384;
logic [7:0] vram [0:VRAM_BYTES-1];
logic [31:0] tex_rd_addr;
logic [31:0] tex_rd_data;
assign tex_rd_data = { vram[(tex_rd_addr & ~32'd3) + 3],
vram[(tex_rd_addr & ~32'd3) + 2],
vram[(tex_rd_addr & ~32'd3) + 1],
vram[(tex_rd_addr & ~32'd3) + 0] };
task automatic poke_word(input logic [31:0] a, input logic [31:0] d);
vram[a+0] = d[7:0]; vram[a+1] = d[15:8];
vram[a+2] = d[23:16]; vram[a+3] = d[31:24];
endtask
// Ch314 — PSMT8: one index byte per texel at the linear byte address.
task automatic poke_t8(input int x, input int y, input logic [7:0] idx);
vram[T8_BASE + (y * (TBWv*64)) + x] = idx; // 1 byte/texel
endtask
// Ch314 — PSMT4: pack 2 indices per byte; even texel-offset -> LOW nibble,
// odd -> HIGH nibble (matches gs_texel_addr's texel_offset[0]==nibble_hi).
task automatic poke_t4(input int x, input int y, input logic [3:0] idx);
int off; int ba;
begin
off = (y * (TBWv*64)) + x;
ba = T4_BASE + (off >> 1);
if (off[0]) vram[ba] = {idx, vram[ba][3:0]}; // high nibble
else vram[ba] = {vram[ba][7:4], idx}; // low nibble
end
endtask
// ---- bilinear DUT ----
logic in_valid;
logic [10:0] u, v;
logic [3:0] u_frac, v_frac;
logic [1:0] wms, wmt;
logic [3:0] tw, th;
logic [31:0] tbp0_base_bytes;
logic [13:0] tbw;
logic [5:0] psm;
logic tex_rd_en;
logic [7:0] clut_rd_idx;
logic [31:0] clut_rd_data;
logic out_valid;
logic [31:0] tex_color;
logic busy;
// Ch314 — CLUT model (256 PSMCT32 entries) shared by both DUTs. The sampler
// drives clut_rd_idx combinationally from the extracted index; we return the
// palette color, exactly like clut_stub's 2nd (combinational) read port.
logic [31:0] clut [0:255];
assign clut_rd_data = clut[clut_rd_idx];
gs_texture_unit #(.BILINEAR_ENABLE(1'b1), .PALETTE_BILINEAR(1'b1),
.TEX_WRAP_ENABLE(1'b1), .RD_LATENCY(1)) dut (
.clk(clk), .rst_n(rst_n),
.in_valid(in_valid),
.u(u), .v(v),
.u_frac(u_frac), .v_frac(v_frac),
.wms(wms), .wmt(wmt), .tw(tw), .th(th),
.tbp0_base_bytes(tbp0_base_bytes),
.tbw(tbw),
.psm(psm),
.tex_rd_en(tex_rd_en),
.tex_rd_addr(tex_rd_addr),
.tex_rd_data(tex_rd_data),
.clut_rd_idx(clut_rd_idx),
.clut_rd_data(clut_rd_data),
.out_valid(out_valid),
.tex_color(tex_color),
.busy(busy)
);
// ---- nearest reference DUT (BILINEAR_ENABLE=0) ----
logic n_in_valid;
logic [10:0] n_u, n_v;
logic [3:0] n_uf, n_vf;
logic [31:0] n_tex_rd_addr;
logic [31:0] n_tex_rd_data;
logic n_out_valid;
logic [31:0] n_tex_color;
logic n_busy;
logic [7:0] n_clut_rd_idx; // Ch314 — nearest DUT's CLUT index (for PSMT8 nearest x-check)
assign n_tex_rd_data = { vram[(n_tex_rd_addr & ~32'd3) + 3],
vram[(n_tex_rd_addr & ~32'd3) + 2],
vram[(n_tex_rd_addr & ~32'd3) + 1],
vram[(n_tex_rd_addr & ~32'd3) + 0] };
gs_texture_unit #(.BILINEAR_ENABLE(1'b0), .TEX_WRAP_ENABLE(1'b1), .RD_LATENCY(1)) dut_near (
.clk(clk), .rst_n(rst_n),
.in_valid(n_in_valid),
.u(n_u), .v(n_v),
.u_frac(n_uf), .v_frac(n_vf),
.wms(WM_REPEAT), .wmt(WM_REPEAT), .tw(tw), .th(th),
.tbp0_base_bytes(tbp0_base_bytes),
.tbw(tbw),
.psm(psm),
.tex_rd_en(),
.tex_rd_addr(n_tex_rd_addr),
.tex_rd_data(n_tex_rd_data),
.clut_rd_idx(n_clut_rd_idx),
.clut_rd_data(clut[n_clut_rd_idx]),
.out_valid(n_out_valid),
.tex_color(n_tex_color),
.busy(n_busy)
);
// ---- texture geometry: 4x4 (TW=TH=2), TBW=1 ----
localparam int TEXW = 4;
localparam int TEXH = 4;
localparam [3:0] TWv = 4'd2; // log2(4)
localparam [3:0] THv = 4'd2;
localparam [13:0] TBWv = 14'd1; // row stride = TBW*64 texels
localparam [31:0] CT32_BASE = 32'h0000_2000;
// Ch314 — indexed textures (same 4x4 geometry, TBW=1).
localparam logic [5:0] PSMT8v = 6'h13;
localparam logic [5:0] PSMT4v = 6'h14;
localparam [31:0] T8_BASE = 32'h0000_1000; // PSMT8: 1 byte/texel, row stride TBW*64=64 B
localparam [31:0] T4_BASE = 32'h0000_1800; // PSMT4: 1 nibble/texel, row stride TBW*64/2=32 B
// Palette: distinct, well-separated colors so a COLOR interpolation is
// visibly different from CLUT-of-an-interpolated-index. idx 0=red 1=blue
// 2=green 3=white. The column-striped index layout idx(x,y)=x gives
// y-independent taps so vf is irrelevant and the blend is a clean U-lerp.
localparam [31:0] CLUT0 = 32'hFF_00_00_FF; // red (A,B,G,R)
localparam [31:0] CLUT1 = 32'hFF_FF_00_00; // blue
localparam [31:0] CLUT2 = 32'hFF_00_FF_00; // green
localparam [31:0] CLUT3 = 32'hFF_FF_FF_FF; // white
// distinct PSMCT32 ABGR word per (x,y): pack varied per-channel values so
// each tap differs in R,G,B AND A (so blends are individually checkable).
function automatic logic [31:0] ct32_word(input int x, input int y);
logic [7:0] r, g, b, a;
begin
r = 8'(16 + (y*TEXW + x) * 12); // 16,28,40,... distinct
g = 8'(8 + (x) * 30); // varies with x
b = 8'(4 + (y) * 40); // varies with y
a = 8'(255 - (y*TEXW + x) * 8); // varies, stays high
ct32_word = {a, b, g, r};
end
endfunction
// reference lerp — mirrors the RTL EXACTLY (same >>>4, same clamp).
function automatic logic [7:0] ref_lerp8(input logic [7:0] a,
input logic [7:0] b,
input logic [3:0] f);
logic signed [16:0] diff;
logic signed [21:0] prod;
logic signed [21:0] shifted;
logic signed [21:0] res;
begin
diff = $signed({1'b0, b}) - $signed({1'b0, a});
prod = diff * $signed({1'b0, f});
shifted = prod >>> 4;
res = $signed({14'd0, a}) + shifted;
if (res < 0) ref_lerp8 = 8'd0;
else if (res > 22'sd255) ref_lerp8 = 8'd255;
else ref_lerp8 = res[7:0];
end
endfunction
// expected bilinear blend of four ABGR words by (uf,vf)
function automatic logic [31:0] ref_blend(input logic [31:0] w00,
input logic [31:0] w10,
input logic [31:0] w01,
input logic [31:0] w11,
input logic [3:0] uf,
input logic [3:0] vf);
logic [7:0] tr, tg, tb, ta; // top
logic [7:0] br, bg, bb, ba; // bottom
logic [7:0] cr, cg, cb, ca; // out
begin
tr = ref_lerp8(w00[ 7: 0], w10[ 7: 0], uf);
tg = ref_lerp8(w00[15: 8], w10[15: 8], uf);
tb = ref_lerp8(w00[23:16], w10[23:16], uf);
ta = ref_lerp8(w00[31:24], w10[31:24], uf);
br = ref_lerp8(w01[ 7: 0], w11[ 7: 0], uf);
bg = ref_lerp8(w01[15: 8], w11[15: 8], uf);
bb = ref_lerp8(w01[23:16], w11[23:16], uf);
ba = ref_lerp8(w01[31:24], w11[31:24], uf);
cr = ref_lerp8(tr, br, vf);
cg = ref_lerp8(tg, bg, vf);
cb = ref_lerp8(tb, bb, vf);
ca = ref_lerp8(ta, ba, vf);
ref_blend = {ca, cb, cg, cr};
end
endfunction
int errors;
initial errors = 0;
// track the max issued texel column/row during a bilinear sample (for the
// clamp proof: addresses must never read beyond the texture).
logic watch_addr;
int max_addr_seen;
always @(posedge clk) begin
if (watch_addr && tex_rd_en) begin
if ($signed(tex_rd_addr) > max_addr_seen)
max_addr_seen = tex_rd_addr;
end
end
// drive one BILINEAR sample, wait for out_valid, return tex_color.
task automatic bsample(input int su, input int sv,
input logic [3:0] suf, input logic [3:0] svf,
output logic [31:0] col);
int guard;
begin
@(negedge clk);
in_valid = 1'b1;
u = 11'(su); v = 11'(sv); u_frac = suf; v_frac = svf;
@(negedge clk);
in_valid = 1'b0; // single-cycle request; FSM is running
guard = 0;
while (out_valid !== 1'b1 && guard < 200) begin
@(negedge clk);
guard++;
end
col = tex_color;
if (guard >= 200) begin
$error("[bilinear] out_valid never asserted (u=%0d v=%0d)", su, sv);
errors++;
end
end
endtask
// drive the NEAREST reference instance, settle combinationally.
task automatic nsample(input int su, input int sv,
input logic [3:0] suf, input logic [3:0] svf,
output logic [31:0] col);
begin
@(negedge clk);
n_in_valid = 1'b1;
n_u = 11'(su); n_v = 11'(sv); n_uf = suf; n_vf = svf;
@(posedge clk); // RD_LATENCY=1 -> out_valid next cycle
#2;
col = n_tex_color;
@(negedge clk);
n_in_valid = 1'b0;
end
endtask
logic [31:0] got, exp;
logic [31:0] w00, w10, w01, w11;
int x, y;
initial begin
rst_n = 1'b0; in_valid = 1'b0; n_in_valid = 1'b0;
u = 0; v = 0; u_frac = 0; v_frac = 0;
n_u = 0; n_v = 0; n_uf = 0; n_vf = 0;
wms = WM_REPEAT; wmt = WM_REPEAT;
tw = TWv; th = THv;
tbp0_base_bytes = CT32_BASE; tbw = TBWv; psm = PSMCT32;
watch_addr = 1'b0; max_addr_seen = 0;
for (int i = 0; i < VRAM_BYTES; i++) vram[i] = 8'd0;
// upload 4x4 PSMCT32 (row stride TBW*64 texels * 4 bytes)
for (y = 0; y < TEXH; y++)
for (x = 0; x < TEXW; x++)
poke_word(CT32_BASE + ((y * (TBWv*64)) + x) * 4, ct32_word(x, y));
// Ch314 — CLUT palette + column-striped indexed textures idx(x,y)=x.
for (int i = 0; i < 256; i++) clut[i] = 32'hFF_80_80_80; // neutral filler
clut[0] = CLUT0; clut[1] = CLUT1; clut[2] = CLUT2; clut[3] = CLUT3;
for (y = 0; y < TEXH; y++)
for (x = 0; x < TEXW; x++) begin
poke_t8(x, y, 8'(x)); // PSMT8 index = column
poke_t4(x, y, 4'(x)); // PSMT4 index = column
end
repeat (4) @(posedge clk);
rst_n = 1'b1;
repeat (2) @(posedge clk);
// ============================================================
// CASE 1 — EXACT TEXEL CENTER (uf=0,vf=0) == nearest texel(u,v)
// ============================================================
wms = WM_REPEAT; wmt = WM_REPEAT;
bsample(1, 2, 4'd0, 4'd0, got);
exp = ct32_word(1, 2);
if (got !== exp) begin
$error("[case1 center] u=1 v=2 uf=0 vf=0 got=%08x exp=%08x", got, exp);
errors++;
end else
$display("[tb_gs_texture_bilinear] CASE1 center pass: got=%08x == texel(1,2)", got);
// ============================================================
// CASE 2 — HALFWAY (uf=8,vf=8) == blend of 4 neighbors
// ============================================================
bsample(1, 1, 4'd8, 4'd8, got);
w00 = ct32_word(1,1); w10 = ct32_word(2,1);
w01 = ct32_word(1,2); w11 = ct32_word(2,2);
exp = ref_blend(w00, w10, w01, w11, 4'd8, 4'd8);
if (got !== exp) begin
$error("[case2 halfway] u=1 v=1 uf=8 vf=8 got=%08x exp=%08x", got, exp);
errors++;
end else
$display("[tb_gs_texture_bilinear] CASE2 halfway pass: got=%08x == 4-tap blend", got);
// ============================================================
// CASE 3 — HALFWAY-U only (uf=8,vf=0) == lerp(texel(u,v),texel(u+1,v))
// ============================================================
bsample(0, 0, 4'd8, 4'd0, got);
w00 = ct32_word(0,0); w10 = ct32_word(1,0);
w01 = ct32_word(0,1); w11 = ct32_word(1,1);
exp = ref_blend(w00, w10, w01, w11, 4'd8, 4'd0); // vf=0 -> top only
if (got !== exp) begin
$error("[case3 halfway-u] u=0 v=0 uf=8 vf=0 got=%08x exp=%08x", got, exp);
errors++;
end else
$display("[tb_gs_texture_bilinear] CASE3 halfway-u pass: got=%08x == lerp((0,0),(1,0))", got);
// ============================================================
// CASE 4 — CLAMP edge (wms=wmt=1, u=W-1, uf>0): (u+1) clamps to W-1
// => result == texel(W-1,v). Also assert the issued addresses never
// exceed the last texel's address (no read outside texture).
// ============================================================
wms = WM_CLAMP; wmt = WM_CLAMP;
watch_addr = 1'b1; max_addr_seen = 0;
bsample(TEXW-1, 1, 4'd8, 4'd0, got); // u=3, u+1 clamps back to 3
watch_addr = 1'b0;
// all 4 taps collapse to (3,1) for u; vf=0 keeps top row (v and v+1
// both clamp/stay -> here v=1, v+1=2 but vf=0 picks v=1). With u
// clamped, w00==w10 and w01==w11, and vf=0 -> top only == texel(3,1).
w00 = ct32_word(3,1); w10 = ct32_word(3,1); // (u+1) clamped to 3
w01 = ct32_word(3,2); w11 = ct32_word(3,2);
exp = ref_blend(w00, w10, w01, w11, 4'd8, 4'd0);
if (got !== ct32_word(3,1)) begin
$error("[case4 clamp] u=3 v=1 uf=8 got=%08x exp=texel(3,1)=%08x", got, ct32_word(3,1));
errors++;
end else if (got !== exp) begin
$error("[case4 clamp blend] got=%08x exp(blend)=%08x", got, exp);
errors++;
end else
$display("[tb_gs_texture_bilinear] CASE4 clamp pass: got=%08x == texel(3,1), no OOB read", got);
// last legal PSMCT32 texel addr = base + (3*64+3)*4 region; the issued
// word addr for texel(3,3) is base + (3*64+3)*4 = base + 0x30C. Any
// higher would mean reading past column 3.
if (max_addr_seen > $signed(CT32_BASE + ((3*(TBWv*64)) + 3)*4)) begin
$error("[case4 clamp addr] max issued addr=0x%08x exceeds last-texel addr 0x%08x",
max_addr_seen, CT32_BASE + ((3*(TBWv*64)) + 3)*4);
errors++;
end else
$display("[tb_gs_texture_bilinear] CASE4 max issued addr=0x%08x <= last texel 0x%08x",
max_addr_seen, CT32_BASE + ((3*(TBWv*64)) + 3)*4);
// ============================================================
// CASE 5 — REPEAT edge (wms=wmt=0, u=W-1, uf>0): (u+1) wraps to 0
// => result == lerp(texel(W-1,v),texel(0,v),uf) (vf=0, top row).
// ============================================================
wms = WM_REPEAT; wmt = WM_REPEAT;
bsample(TEXW-1, 1, 4'd8, 4'd0, got); // u=3, u+1=4 wraps to 0
w00 = ct32_word(3,1); w10 = ct32_word(0,1); // (u+1) wraps to col 0
w01 = ct32_word(3,2); w11 = ct32_word(0,2);
exp = ref_blend(w00, w10, w01, w11, 4'd8, 4'd0);
if (got !== exp) begin
$error("[case5 repeat] u=3 v=1 uf=8 got=%08x exp=lerp(t(3,1),t(0,1))=%08x", got, exp);
errors++;
end else
$display("[tb_gs_texture_bilinear] CASE5 repeat pass: got=%08x == lerp(t(3,1),t(0,1))", got);
// ============================================================
// CASE 6 — NEAREST UNCHANGED: BILINEAR_ENABLE=0 instance returns the
// single nearest texel and IGNORES u_frac/v_frac.
// ============================================================
nsample(1, 2, 4'd0, 4'd0, got);
exp = ct32_word(1, 2);
if (got !== exp) begin
$error("[case6 nearest] u=1 v=2 got=%08x exp=%08x", got, exp);
errors++;
end
// same (u,v) but NON-ZERO frac must give the SAME nearest texel.
nsample(1, 2, 4'd15, 4'd15, got);
if (got !== exp) begin
$error("[case6 nearest frac-ignored] u=1 v=2 uf=15 vf=15 got=%08x exp=%08x (frac must be ignored)", got, exp);
errors++;
end else
$display("[tb_gs_texture_bilinear] CASE6 nearest pass: got=%08x (frac ignored), busy=%0b", got, n_busy);
if (n_busy !== 1'b0) begin
$error("[case6 nearest busy] nearest instance busy=%0b (must be 0)", n_busy);
errors++;
end
// ============================================================
// CASE 7 — PSMT8 BILINEAR halfway-U: the GUARDRAIL. taps idx(0,*)=0(red)
// and idx(1,*)=1(blue) -> CLUT to red/blue/red/blue, lerp blends the
// COLORS (purple). Result must equal the color-blend AND be NEITHER
// endpoint color (which is what interpolating INDICES then CLUT'ing
// once would give). idx(x,y)=x so vf is irrelevant.
// ============================================================
psm = PSMT8v; tbp0_base_bytes = T8_BASE; wms = WM_REPEAT; wmt = WM_REPEAT;
bsample(0, 0, 4'd8, 4'd0, got);
exp = ref_blend(CLUT0, CLUT1, CLUT0, CLUT1, 4'd8, 4'd0);
if (got !== exp) begin
$error("[case7 t8 color-interp] got=%08x exp(blend red,blue)=%08x", got, exp);
errors++;
end else if (got === CLUT0 || got === CLUT1) begin
$error("[case7 t8 GUARDRAIL] got=%08x is an endpoint color -> indices were interpolated, not colors", got);
errors++;
end else
$display("[tb_gs_texture_bilinear] CASE7 PSMT8 CLUT-before-interp pass: got=%08x (blended red<->blue, neither endpoint)", got);
// ============================================================
// CASE 8 — PSMT8 halfway BOTH (uf=vf=8) blue<->green at (1,1)..(2,2).
// ============================================================
bsample(1, 1, 4'd8, 4'd8, got);
exp = ref_blend(CLUT1, CLUT2, CLUT1, CLUT2, 4'd8, 4'd8);
if (got !== exp) begin
$error("[case8 t8 halfway-both] got=%08x exp=%08x", got, exp);
errors++;
end else if (got === CLUT1 || got === CLUT2) begin
$error("[case8 t8 GUARDRAIL] got=%08x endpoint color", got); errors++;
end else
$display("[tb_gs_texture_bilinear] CASE8 PSMT8 halfway-both pass: got=%08x (blue<->green)", got);
// ============================================================
// CASE 9 — PSMT8 EXACT texel center == CLUT[idx(2,2)]=green. Also the
// NEAREST instance (BILINEAR_ENABLE=0) must return the same CLUT color.
// ============================================================
bsample(2, 2, 4'd0, 4'd0, got);
if (got !== CLUT2) begin
$error("[case9 t8 center] got=%08x exp=CLUT[2]=%08x", got, CLUT2); errors++;
end else
$display("[tb_gs_texture_bilinear] CASE9 PSMT8 center pass: got=%08x == CLUT[2]", got);
nsample(2, 2, 4'd0, 4'd0, got);
if (got !== CLUT2) begin
$error("[case9 t8 nearest] nearest got=%08x exp=CLUT[2]=%08x", got, CLUT2); errors++;
end else
$display("[tb_gs_texture_bilinear] CASE9 PSMT8 nearest x-check pass: got=%08x == CLUT[2]", got);
// ============================================================
// CASE 10 — PSMT4 halfway-U ACROSS A BYTE BOUNDARY. At y=0 the nibbles
// are: off0=low(b0)=idx0, off1=high(b0)=idx1, off2=low(b1)=idx2,
// off3=high(b1)=idx3. Sampling u=1 (idx1=blue, HIGH nibble of byte0)
// with u+1=2 (idx2=green, LOW nibble of byte1) proves nibble extraction
// for ALL FOUR taps + the byte-boundary crossing + CLUT-before-interp.
// ============================================================
psm = PSMT4v; tbp0_base_bytes = T4_BASE;
bsample(1, 0, 4'd8, 4'd0, got);
exp = ref_blend(CLUT1, CLUT2, CLUT1, CLUT2, 4'd8, 4'd0);
if (got !== exp) begin
$error("[case10 t4 nibble blend] got=%08x exp(blue,green)=%08x", got, exp);
errors++;
end else if (got === CLUT1 || got === CLUT2) begin
$error("[case10 t4 GUARDRAIL] got=%08x endpoint color", got); errors++;
end else
$display("[tb_gs_texture_bilinear] CASE10 PSMT4 nibble + CLUT-before-interp pass: got=%08x", got);
// ============================================================
// CASE 11 — PSMT8 REPEAT edge: u=3,u+1 wraps to col0. idx(3,*)=3(white),
// idx(0,*)=0(red) -> blend white<->red.
// ============================================================
psm = PSMT8v; tbp0_base_bytes = T8_BASE; wms = WM_REPEAT; wmt = WM_REPEAT;
bsample(TEXW-1, 1, 4'd8, 4'd0, got);
exp = ref_blend(CLUT3, CLUT0, CLUT3, CLUT0, 4'd8, 4'd0);
if (got !== exp) begin
$error("[case11 t8 repeat] got=%08x exp(white,red)=%08x", got, exp); errors++;
end else
$display("[tb_gs_texture_bilinear] CASE11 PSMT8 repeat-edge pass: got=%08x", got);
// ============================================================
// CASE 12 — PSMT8 CLAMP edge: u=3,u+1 clamps to col3 -> all u-taps white;
// vf=0 -> result == CLUT[idx(3,1)]=white, and NO address reads past the
// last texel column.
// ============================================================
wms = WM_CLAMP; wmt = WM_CLAMP;
watch_addr = 1'b1; max_addr_seen = 0;
bsample(TEXW-1, 1, 4'd8, 4'd0, got);
watch_addr = 1'b0;
if (got !== CLUT3) begin
$error("[case12 t8 clamp] got=%08x exp=CLUT[3]=white=%08x", got, CLUT3); errors++;
end else
$display("[tb_gs_texture_bilinear] CASE12 PSMT8 clamp-edge pass: got=%08x == white", got);
if (max_addr_seen > $signed(T8_BASE + ((3*(TBWv*64)) + 3))) begin
$error("[case12 t8 clamp addr] max issued=0x%08x exceeds last texel 0x%08x",
max_addr_seen, T8_BASE + ((3*(TBWv*64)) + 3));
errors++;
end else
$display("[tb_gs_texture_bilinear] CASE12 max issued addr=0x%08x <= last texel 0x%08x",
max_addr_seen, T8_BASE + ((3*(TBWv*64)) + 3));
$display("[tb_gs_texture_bilinear] errors=%0d", errors);
if (errors == 0)
$display("[tb_gs_texture_bilinear] PASS");
else
$display("[tb_gs_texture_bilinear] FAIL");
$finish;
end
initial begin
#2000000;
$error("[tb_gs_texture_bilinear] TIMEOUT");
$finish;
end
endmodule : tb_gs_texture_bilinear