wesl-debug
Version:
Utilities for testing WESL/WGSL shaders in Node.js environments.
423 lines (355 loc) • 11.2 kB
text/typescript
import { afterAll, beforeAll, expect, test } from "vitest";
import {
checkerboardTexture,
createSampler,
gradientTexture,
solidTexture,
} from "../ExampleTextures.ts";
import {
testAnimatedShader,
testFragmentShader,
} from "../TestFragmentShader.ts";
import { destroySharedDevice, getGPUDevice } from "../WebGPUTestSetup.ts";
let device: GPUDevice;
beforeAll(async () => {
device = await getGPUDevice();
});
afterAll(() => {
destroySharedDevice();
});
test("renders simple constant color", async () => {
const src = `
fn fs_main() -> vec4f {
return vec4f(0.5, 0.25, 0.75, 1.0);
}
`;
const projectDir = import.meta.url;
const textureFormat: GPUTextureFormat = "rgba32float";
const params = { projectDir, device, src, textureFormat };
const result = await testFragmentShader(params);
expect(result).toHaveLength(4);
expect(result[0]).toBeCloseTo(0.5);
expect(result[1]).toBeCloseTo(0.25);
expect(result[2]).toBeCloseTo(0.75);
expect(result[3]).toBeCloseTo(1.0);
});
test("derivative of x coordinate", async () => {
const src = `
fn fs_main( pos: vec4f) -> vec4f {
let dx = dpdx(pos.x);
return vec4f(pos.x, dx, 0.0, 1.0);
}
`;
const result = await testFragmentShader({
projectDir: import.meta.url,
device,
src,
textureFormat: "rg32float",
size: [2, 2],
});
// result at pixel (0, 0)
const [x, dx] = result;
expect(x).toBeCloseTo(0.5);
expect(dx).toBeCloseTo(1);
});
test("samples solid color texture", async () => {
const inputTex = solidTexture(device, [0.5, 0.5, 0.5, 1.0], 256, 256);
const sampler = createSampler(device);
const src = `
var input_tex: texture_2d<f32>;
var input_samp: sampler;
fn fs_main( pos: vec4f) -> vec4f {
let uv = pos.xy / 256.0;
return textureSample(input_tex, input_samp, uv);
}
`;
const result = await testFragmentShader({
projectDir: import.meta.url,
device,
src,
inputTextures: [{ texture: inputTex, sampler }],
});
expect(result[0]).toBeCloseTo(0.5);
expect(result[1]).toBeCloseTo(0.5);
expect(result[2]).toBeCloseTo(0.5);
expect(result[3]).toBeCloseTo(1.0);
});
test("samples gradient texture at center", async () => {
const inputTex = gradientTexture(device, 256, 256, "horizontal");
const sampler = createSampler(device);
const src = `
var input_tex: texture_2d<f32>;
var input_samp: sampler;
fn fs_main( pos: vec4f) -> vec4f {
return textureSample(input_tex, input_samp, vec2f(0.5, 0.5));
}
`;
const result = await testFragmentShader({
projectDir: import.meta.url,
device,
src,
inputTextures: [{ texture: inputTex, sampler }],
});
expect(result[0]).toBeCloseTo(0.5, 1);
expect(result[1]).toBeCloseTo(0.5, 1);
expect(result[2]).toBeCloseTo(0.5, 1);
});
test("samples checkerboard texture", async () => {
const inputTex = checkerboardTexture(device, 256, 256, 128);
const sampler = createSampler(device);
const src = `
var input_tex: texture_2d<f32>;
var input_samp: sampler;
fn fs_main( pos: vec4f) -> vec4f {
// Sample at (0.25, 0.25) - should be black (0.0)
return textureSample(input_tex, input_samp, vec2f(0.25, 0.25));
}
`;
const result = await testFragmentShader({
projectDir: import.meta.url,
device,
src,
inputTextures: [{ texture: inputTex, sampler }],
});
expect(result[0]).toBeCloseTo(0.0);
expect(result[1]).toBeCloseTo(0.0);
expect(result[2]).toBeCloseTo(0.0);
});
test("samples multiple textures", async () => {
const tex1 = solidTexture(device, [1.0, 0.0, 0.0, 1.0], 64, 64);
const tex2 = solidTexture(device, [0.0, 1.0, 0.0, 1.0], 64, 64);
const sampler = createSampler(device);
const src = `
var tex1: texture_2d<f32>;
var samp1: sampler;
var tex2: texture_2d<f32>;
var samp2: sampler;
fn fs_main( pos: vec4f) -> vec4f {
let uv = vec2f(0.5, 0.5);
let c1 = textureSample(tex1, samp1, uv);
let c2 = textureSample(tex2, samp2, uv);
return c1 * 0.5 + c2 * 0.5;
}
`;
const result = await testFragmentShader({
projectDir: import.meta.url,
device,
src,
inputTextures: [
{ texture: tex1, sampler },
{ texture: tex2, sampler },
],
});
expect(result[0]).toBeCloseTo(0.5); // (1.0 + 0.0) / 2
expect(result[1]).toBeCloseTo(0.5); // (0.0 + 1.0) / 2
expect(result[2]).toBeCloseTo(0.0); // (0.0 + 0.0) / 2
});
test("uses scalar constant from constants namespace", async () => {
const src = `
import constants::BRIGHTNESS;
fn fs_main() -> vec4f {
return vec4f(BRIGHTNESS, 0.0, 0.0, 1.0);
}
`;
const result = await testFragmentShader({
projectDir: import.meta.url,
device,
src,
constants: { BRIGHTNESS: 0.75 },
});
expect(result[0]).toBeCloseTo(0.75);
});
test("uses vector constant from constants namespace", async () => {
const src = `
import constants::COLOR;
fn fs_main() -> vec4f {
return vec4f(COLOR, 0.0, 1.0);
}
`;
const result = await testFragmentShader({
projectDir: import.meta.url,
device,
src,
constants: { COLOR: "vec2f(0.25, 0.5)" },
});
expect(result[0]).toBeCloseTo(0.25);
expect(result[1]).toBeCloseTo(0.5);
expect(result[2]).toBeCloseTo(0.0);
expect(result[3]).toBeCloseTo(1.0);
});
test("uses conditions for conditional compilation", async () => {
const src = `
fn fs_main() -> vec4f {
return vec4f(1.0, 0.0, 0.0, 1.0);
return vec4f(0.0, 1.0, 0.0, 1.0);
}
`;
const resultRed = await testFragmentShader({
projectDir: import.meta.url,
device,
src,
conditions: { USE_RED: true },
});
expect(resultRed[0]).toBeCloseTo(1.0);
expect(resultRed[1]).toBeCloseTo(0.0);
const resultGreen = await testFragmentShader({
projectDir: import.meta.url,
device,
src,
conditions: { USE_RED: false },
});
expect(resultGreen[0]).toBeCloseTo(0.0);
expect(resultGreen[1]).toBeCloseTo(1.0);
});
test("uses both conditions and constants together", async () => {
const src = `
import constants::CUSTOM_COLOR;
fn fs_main() -> vec4f {
return vec4f(CUSTOM_COLOR, 0.0, 1.0);
return vec4f(0.0, 0.0, 0.0, 1.0);
}
`;
const resultWithColor = await testFragmentShader({
projectDir: import.meta.url,
device,
src,
conditions: { USE_CUSTOM_COLOR: true },
constants: { CUSTOM_COLOR: "vec2f(0.8, 0.6)" },
});
expect(resultWithColor[0]).toBeCloseTo(0.8);
expect(resultWithColor[1]).toBeCloseTo(0.6);
expect(resultWithColor[2]).toBeCloseTo(0.0);
const resultWithoutColor = await testFragmentShader({
projectDir: import.meta.url,
device,
src,
conditions: { USE_CUSTOM_COLOR: false },
});
expect(resultWithoutColor[0]).toBeCloseTo(0.0);
expect(resultWithoutColor[1]).toBeCloseTo(0.0);
expect(resultWithoutColor[2]).toBeCloseTo(0.0);
});
test("shader with resolution uniform (auto-populated)", async () => {
const src = `
var<uniform> u: test::Uniforms;
fn fs_main( pos: vec4f) -> vec4f {
let st = pos.xy / u.resolution;
return vec4f(st.x, st.y, 0.0, 1.0);
}
`;
const result = await testFragmentShader({
projectDir: import.meta.url,
device,
src,
size: [256, 256],
// resolution auto-populated as [256, 256]
});
// Pixel (0,0) is at fragment coordinate (0.5, 0.5)
// Normalized: (0.5/256, 0.5/256, 0, 1)
expect(result[0]).toBeCloseTo(0.5 / 256, 4);
expect(result[1]).toBeCloseTo(0.5 / 256, 4);
expect(result[2]).toBe(0.0);
expect(result[3]).toBe(1.0);
});
test("shader with time uniform", async () => {
const src = `
var<uniform> u: test::Uniforms;
fn fs_main() -> vec4f {
return vec4f(u.time, u.time * 2.0, u.time * 3.0, 1.0);
}
`;
const result = await testFragmentShader({
projectDir: import.meta.url,
device,
src,
uniforms: { time: 5.0 },
});
expect(result[0]).toBeCloseTo(5.0);
expect(result[1]).toBeCloseTo(10.0);
expect(result[2]).toBeCloseTo(15.0);
expect(result[3]).toBeCloseTo(1.0);
});
test("shader with mouse uniform", async () => {
const src = `
var<uniform> u: test::Uniforms;
fn fs_main( pos: vec4f) -> vec4f {
let st = pos.xy / u.resolution;
let dist = length(st - u.mouse);
return vec4f(dist, 0.0, 0.0, 1.0);
}
`;
const result = await testFragmentShader({
projectDir: import.meta.url,
device,
src,
size: [256, 256],
uniforms: { mouse: [0.5, 0.5] },
});
// Distance from (0.5/256, 0.5/256) to (0.5, 0.5)
const st = [0.5 / 256, 0.5 / 256];
const expectedDist = Math.sqrt((st[0] - 0.5) ** 2 + (st[1] - 0.5) ** 2);
expect(result[0]).toBeCloseTo(expectedDist, 4);
});
test("shader with uniforms and texture", async () => {
const inputTex = solidTexture(device, [0.5, 0.5, 0.5, 1.0], 64, 64);
const sampler = createSampler(device);
const src = `
var<uniform> u: test::Uniforms;
var input_tex: texture_2d<f32>;
var input_samp: sampler;
fn fs_main( pos: vec4f) -> vec4f {
let uv = pos.xy / u.resolution;
let tex_color = textureSample(input_tex, input_samp, uv);
let time_mod = vec4f(u.time * 0.1);
return tex_color + time_mod;
}
`;
const result = await testFragmentShader({
projectDir: import.meta.url,
device,
src,
size: [64, 64],
uniforms: { time: 10.0 },
inputTextures: [{ texture: inputTex, sampler }],
});
// 0.5 (texture) + 1.0 (time * 0.1 where time=10) = 1.5
expect(result[0]).toBeCloseTo(1.5, 2);
});
test("multi-frame animation", async () => {
const src = `
var<uniform> u: test::Uniforms;
fn fs_main() -> vec4f {
let phase = sin(u.time);
return vec4f(phase, 0.0, 0.0, 1.0);
}
`;
const frames = await testAnimatedShader({
projectDir: import.meta.url,
device,
src,
timePoints: [0.0, 1.57, 3.14], // 0, π/2, π
});
expect(frames[0][0]).toBeCloseTo(0.0, 2); // sin(0) = 0
expect(frames[1][0]).toBeCloseTo(1.0, 2); // sin(π/2) = 1
expect(frames[2][0]).toBeCloseTo(0.0, 2); // sin(π) ≈ 0
});