UNPKG

wesl-test

Version:

Write GPU shader tests as easily as regular unit tests. Test WGSL and WESL shaders with vitest or your favorite Node.js test framework.

294 lines (275 loc) 8.07 kB
import { DeviceCache } from "./DeviceCache.ts"; /* Texture and sampler creation helpers with internal caching for test performance */ export interface SamplerOptions { addressMode?: "clamp-to-edge" | "repeat" | "mirror-repeat"; filterMode?: "nearest" | "linear"; } const textureCache = new DeviceCache<GPUTexture>(); const samplerCache = new DeviceCache<GPUSampler>(); /** Create texture filled with solid color. Internally cached. */ export function solidTexture( device: GPUDevice, color: [r: number, g: number, b: number, a: number], width: number, height: number, ): GPUTexture { const cacheKey = `solid:${color.join(",")}:${width}x${height}`; return cachedTexture( device, cacheKey, `test-texture-solid-${color.join(",")}`, width, height, data => { for (let i = 0; i < width * height; i++) { data[i * 4 + 0] = Math.round(color[0] * 255); data[i * 4 + 1] = Math.round(color[1] * 255); data[i * 4 + 2] = Math.round(color[2] * 255); data[i * 4 + 3] = Math.round(color[3] * 255); } }, ); } /** Create gradient texture. Direction: 'horizontal' (default) or 'vertical'. */ export function gradientTexture( device: GPUDevice, width: number, height: number, direction: "horizontal" | "vertical" = "horizontal", ): GPUTexture { const cacheKey = `gradient:${direction}:${width}x${height}`; return cachedTexture( device, cacheKey, `test-texture-gradient-${direction}`, width, height, data => { for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const idx = (y * width + x) * 4; const gradient = direction === "horizontal" ? x / (width - 1) : y / (height - 1); const value = Math.round(gradient * 255); data[idx + 0] = value; data[idx + 1] = value; data[idx + 2] = value; data[idx + 3] = 255; } } }, ); } /** Create checkerboard pattern. cellSize: pixels per cell (default: width/4). */ export function checkerboardTexture( device: GPUDevice, width: number, height: number, cellSize?: number, ): GPUTexture { const cell = cellSize ?? Math.floor(width / 4); const cacheKey = `checkerboard:${cell}:${width}x${height}`; return cachedTexture( device, cacheKey, `test-texture-checkerboard-${cell}`, width, height, data => { for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const idx = (y * width + x) * 4; const cellX = Math.floor(x / cell); const cellY = Math.floor(y / cell); const isBlack = (cellX + cellY) % 2 === 0; const value = isBlack ? 0 : 255; data[idx + 0] = value; data[idx + 1] = value; data[idx + 2] = value; data[idx + 3] = 255; } } }, ); } /** Create sampler. Default: linear filtering with clamp-to-edge. Internally cached. */ export function createSampler( device: GPUDevice, options?: SamplerOptions, ): GPUSampler { const { addressMode = "clamp-to-edge", filterMode = "linear" } = options ?? {}; const cacheKey = `${addressMode}:${filterMode}`; const cached = samplerCache.get(device, cacheKey); if (cached) return cached; const sampler = device.createSampler({ addressModeU: addressMode, addressModeV: addressMode, magFilter: filterMode, minFilter: filterMode, }); samplerCache.set(device, cacheKey, sampler); return sampler; } /** Create radial gradient texture (white center to black edge). */ export function radialGradientTexture( device: GPUDevice, size: number, ): GPUTexture { const cacheKey = `radial:${size}`; const centerX = size / 2; const centerY = size / 2; const maxRadius = Math.sqrt(centerX * centerX + centerY * centerY); return cachedTexture( device, cacheKey, `test-texture-radial-${size}`, size, size, data => { for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { const idx = (y * size + x) * 4; const dx = x - centerX; const dy = y - centerY; const distance = Math.sqrt(dx * dx + dy * dy); const gradient = 1.0 - Math.min(distance / maxRadius, 1.0); const value = Math.round(gradient * 255); data[idx + 0] = value; data[idx + 1] = value; data[idx + 2] = value; data[idx + 3] = 255; } } }, ); } /** Create edge pattern texture with sharp vertical, horizontal, and diagonal lines. */ export function edgePatternTexture( device: GPUDevice, size: number, ): GPUTexture { const cacheKey = `edges:${size}`; const lineWidth = 2; return cachedTexture( device, cacheKey, `test-texture-edges-${size}`, size, size, data => { for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { const idx = (y * size + x) * 4; const isLine = Math.abs(x - size / 2) < lineWidth || Math.abs(y - size / 2) < lineWidth || Math.abs(x - y) < lineWidth || Math.abs(x - (size - 1 - y)) < lineWidth; const value = isLine ? 255 : 0; data[idx + 0] = value; data[idx + 1] = value; data[idx + 2] = value; data[idx + 3] = 255; } } }, ); } /** Create color bars texture (RGB primaries and secondaries). */ export function colorBarsTexture(device: GPUDevice, size: number): GPUTexture { const cacheKey = `colorbars:${size}`; const colors = [ [255, 0, 0], [255, 255, 0], [0, 255, 0], [0, 255, 255], [0, 0, 255], [255, 0, 255], ]; const barWidth = size / colors.length; return cachedTexture( device, cacheKey, `test-texture-colorbars-${size}`, size, size, data => { for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { const idx = (y * size + x) * 4; const barIndex = Math.min( Math.floor(x / barWidth), colors.length - 1, ); const color = colors[barIndex]; data[idx + 0] = color[0]; data[idx + 1] = color[1]; data[idx + 2] = color[2]; data[idx + 3] = 255; } } }, ); } /** Create seeded noise pattern (deterministic). */ export function noiseTexture( device: GPUDevice, size: number, seed = 42, ): GPUTexture { const cacheKey = `noise:${size}:${seed}`; return cachedTexture( device, cacheKey, `test-texture-noise-${size}`, size, size, data => { let rng = seed; // Simple seeded PRNG (mulberry32) const random = () => { rng |= 0; rng = (rng + 0x6d2b79f5) | 0; let t = Math.imul(rng ^ (rng >>> 15), 1 | rng); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; for (let i = 0; i < size * size * 4; i += 4) { const value = Math.round(random() * 255); data[i + 0] = value; data[i + 1] = value; data[i + 2] = value; data[i + 3] = 255; } }, ); } /** Common helper for creating cached textures with custom data generation. */ function cachedTexture( device: GPUDevice, cacheKey: string, label: string, width: number, height: number, generateData: (data: Uint8Array, width: number, height: number) => void, ): GPUTexture { const cached = textureCache.get(device, cacheKey); if (cached) return cached; const texture = device.createTexture({ label, size: { width, height, depthOrArrayLayers: 1 }, format: "rgba8unorm", usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST, }); const data = new Uint8Array(width * height * 4); generateData(data, width, height); device.queue.writeTexture( { texture }, data, { bytesPerRow: width * 4 }, { width, height }, ); textureCache.set(device, cacheKey, texture); return texture; }