wgsl-test 0.2.19 → 0.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{WebGPUTestSetup-C4U3urwO.js → WebGPUTestSetup-f3N5MdWa.js} +1 -1
- package/dist/{WebGPUTestSetup-C4U3urwO.js.map → WebGPUTestSetup-f3N5MdWa.js.map} +1 -1
- package/dist/index.js +151 -3
- package/dist/index.js.map +1 -0
- package/dist/runTestCli.js +15983 -2
- package/dist/runTestCli.js.map +1 -1
- package/dist/webgpu-BRL1ElJY.js +14 -0
- package/dist/webgpu-BRL1ElJY.js.map +1 -0
- package/dist/wgslTestMain.js +1 -1
- package/package.json +5 -5
- package/dist/runTestCli.d.ts +0 -1
- package/dist/src-Heai9oox.js +0 -152
- package/dist/src-Heai9oox.js.map +0 -1
- package/src/test/__image_actual__/box-blur.png +0 -0
- package/src/test/__image_actual__/edge-detection.png +0 -0
- package/src/test/__image_actual__/effects-checkerboard.png +0 -0
- package/src/test/__image_actual__/grayscale.png +0 -0
- package/src/test/__image_actual__/lemur-sharpen.png +0 -0
- package/src/test/__image_actual__/solid-red-small.png +0 -0
- package/src/test/__image_actual__/solid_red.png +0 -0
|
@@ -417,4 +417,4 @@ function destroySharedDevice() {
|
|
|
417
417
|
|
|
418
418
|
//#endregion
|
|
419
419
|
export { createProjectResolver as _, isDeno as a, testWesl as c, findTestFunctions as d, testDisplayName as f, compileShader as g, resolveShaderSource as h, getGPUDevice as i, importImageSnapshot as l, testCompute as m, getGPU as n, expectWesl as o, runCompute as p, getGPUAdapter as r, runWesl as s, destroySharedDevice as t, importVitest as u, resolveShaderContext as v };
|
|
420
|
-
//# sourceMappingURL=WebGPUTestSetup-
|
|
420
|
+
//# sourceMappingURL=WebGPUTestSetup-f3N5MdWa.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebGPUTestSetup-C4U3urwO.js","names":["path"],"sources":["../src/CompileShader.ts","../src/ShaderModuleLoader.ts","../src/TestComputeShader.ts","../src/TestDiscovery.ts","../src/VitestImport.ts","../src/TestWesl.ts","../src/WebGPUTestSetup.ts"],"sourcesContent":["import * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { LinkParams, ModuleResolver, WeslBundle } from \"wesl\";\nimport { CompositeResolver, link, RecordResolver } from \"wesl\";\nimport {\n dependencyBundles,\n FileModuleResolver,\n findWeslToml,\n readPackageJson,\n resolveProjectDir,\n} from \"wesl-tooling\";\n\nexport interface ShaderContext {\n /** Dependency bundles for the shader. */\n libs: WeslBundle[];\n\n /** Resolver for lazy loading (when useSourceShaders is true). */\n resolver?: ModuleResolver;\n\n /** Package name for module resolution. */\n packageName?: string;\n}\n\nexport interface ResolveContextParams {\n /** WESL/WGSL shader source code. */\n src: string;\n\n /** Project directory for resolving dependencies. */\n projectDir?: string;\n\n /** Use source shaders instead of built bundles. Default: true. */\n useSourceShaders?: boolean;\n\n /** Virtual lib names to exclude from dependency resolution. */\n virtualLibNames?: string[];\n}\n\nexport interface CompileShaderParams {\n /** Project directory for resolving shader dependencies.\n * Used to locate installed npm shader libraries.\n * Optional: defaults to searching upward from cwd for package.json or wesl.toml. */\n projectDir?: string;\n\n /** GPU device to use for shader compilation. */\n device: GPUDevice;\n\n /** WESL/WGSL shader source code to compile. */\n src: string;\n\n /** Conditions for conditional compilation.\n * Used to control `@if` directives in the shader. */\n conditions?: LinkParams[\"conditions\"];\n\n /** Constants for shader compilation.\n * Injects host-provided values via the `constants::` namespace. */\n constants?: LinkParams[\"constants\"];\n\n /** Virtual libraries to include in the shader.\n * Allows dynamic generation of shader code at runtime. */\n virtualLibs?: LinkParams[\"virtualLibs\"];\n\n /** Additional WESL bundles to include.\n * These are merged with auto-discovered dependencies. */\n libs?: WeslBundle[];\n\n /** Override the package name for module resolution.\n * Used to ensure package:: references resolve correctly. */\n packageName?: string;\n\n /** Use source shaders from current package instead of built bundles.\n * Default: true for faster iteration during development.\n * Set to false or use TEST_BUNDLES=true environment variable to test built bundles.\n *\n * Precedence: explicit parameter > TEST_BUNDLES env var > default (true)\n */\n useSourceShaders?: boolean;\n}\n\n/**\n * Compiles a WESL shader source string into a GPUShaderModule with automatic dependency resolution.\n *\n * Parses the shader source to detect references to shader packages, then automatically\n * includes the required npm package bundles. By default, loads source shaders from the\n * current package for fast iteration without requiring rebuilds.\n *\n * @returns Compiled GPUShaderModule ready for use in render or compute pipelines\n * @throws Error if shader compilation fails with compilation error details\n */\nexport async function compileShader(\n params: CompileShaderParams,\n): Promise<GPUShaderModule> {\n const { device, src, conditions, constants, virtualLibs, libs = [] } = params;\n const ctx = await resolveShaderContext({\n src,\n projectDir: params.projectDir,\n useSourceShaders: params.useSourceShaders,\n virtualLibNames: virtualLibs ? Object.keys(virtualLibs) : [],\n });\n\n // Filter out undefined values that can occur when auto-discovery finds packages\n // that aren't resolvable (e.g., wgsl_test when running tests within wgsl-test itself)\n const allLibs = [...ctx.libs, ...libs].filter(Boolean) as WeslBundle[];\n let linkParams: Pick<LinkParams, \"resolver\" | \"libs\" | \"weslSrc\">;\n if (ctx.resolver) {\n linkParams = { resolver: ctx.resolver, libs: allLibs };\n } else {\n linkParams = { weslSrc: { main: src }, libs: allLibs };\n }\n\n const linked = await link({\n ...linkParams,\n rootModuleName: \"main\",\n virtualLibs,\n conditions,\n constants,\n packageName: params.packageName ?? ctx.packageName,\n });\n const module = linked.createShaderModule(device);\n\n await verifyCompilation(module);\n return module;\n}\n\n/** Resolve project context for shader compilation: bundles, resolver, and package name. */\nexport async function resolveShaderContext(\n params: ResolveContextParams,\n): Promise<ShaderContext> {\n const { src, useSourceShaders = !process.env.TEST_BUNDLES } = params;\n const { virtualLibNames = [] } = params;\n const projectDir = await resolveProjectDir(params.projectDir);\n const packageName = await getPackageName(projectDir);\n\n const libs = await dependencyBundles(\n { main: src },\n projectDir,\n packageName,\n !useSourceShaders, // include current package when testing bundles\n virtualLibNames,\n );\n\n const resolver = useSourceShaders\n ? await lazyFileResolver(projectDir, src, packageName)\n : undefined;\n\n return { libs, resolver, packageName };\n}\n\n/** Create a project resolver for loading modules from the filesystem.\n * Handles wesl.toml configuration and creates FileModuleResolver with correct baseDir.\n *\n * @param projectDir Project directory (defaults to cwd)\n * @param packageName Package name for module resolution (optional)\n * @returns FileModuleResolver configured for the project\n */\nexport async function createProjectResolver(\n projectDir?: string,\n packageName?: string,\n): Promise<ModuleResolver> {\n const resolved = await resolveProjectDir(projectDir);\n const projectPath = fileURLToPath(resolved);\n const tomlInfo = await findWeslToml(projectPath);\n const baseDir = path.isAbsolute(tomlInfo.resolvedRoot)\n ? tomlInfo.resolvedRoot\n : path.join(projectPath, tomlInfo.resolvedRoot);\n\n return new FileModuleResolver(baseDir, packageName);\n}\n\n/** Verify shader compilation succeeded, throw on errors. */\nasync function verifyCompilation(module: GPUShaderModule): Promise<void> {\n const info = await module.getCompilationInfo();\n const errors = info.messages.filter(msg => msg.type === \"error\");\n if (errors.length > 0) {\n const messages = errors\n .map(e => `${e.lineNum}:${e.linePos} ${e.message}`)\n .join(\"\\n\");\n throw new Error(`Shader compilation failed:\\n${messages}`);\n }\n}\n\n/** Read package name from package.json, normalized for WGSL identifiers. */\nasync function getPackageName(projectDir: string): Promise<string | undefined> {\n try {\n const pkg = await readPackageJson(projectDir);\n const name = pkg.name as string;\n return name.replace(/-/g, \"_\");\n } catch {\n return undefined;\n }\n}\n\n/** Create a lazy resolver that loads local shaders on-demand from the filesystem.\n * Laziness allows testing without rebuilding the current package after edits. */\nasync function lazyFileResolver(\n projectDir: string,\n mainSrc: string,\n packageName: string | undefined,\n): Promise<CompositeResolver> {\n const mainResolver = new RecordResolver({ main: mainSrc }, { packageName });\n const fileResolver = await createProjectResolver(projectDir, packageName);\n return new CompositeResolver([mainResolver, fileResolver]);\n}\n","import { normalizeModuleName } from \"wesl\";\nimport { createProjectResolver } from \"./CompileShader.ts\";\n\n/** Validates that exactly one of src or moduleName is provided.\n * @throws Error if neither or both are provided */\nexport function validateSourceParams(src?: string, moduleName?: string): void {\n if (!src && !moduleName) {\n throw new Error(\"Either src or moduleName must be provided\");\n }\n if (src && moduleName) {\n throw new Error(\"Cannot provide both src and moduleName\");\n }\n}\n\n/** Loads shader source from module name using filesystem resolver.\n * @param moduleName Shader module name (bare name, path, or module path)\n * @param projectDir Project directory for module resolution\n * @returns Shader source code\n */\nexport async function loadShaderSourceFromModule(\n moduleName: string,\n projectDir?: string,\n): Promise<string> {\n const resolver = await createProjectResolver(projectDir);\n const normalizedName = normalizeModuleName(moduleName);\n const ast = resolver.resolveModule(normalizedName);\n if (!ast) throw new Error(`Could not resolve module: ${moduleName}`);\n return ast.srcModule.src;\n}\n\n/** Resolves shader source from either inline src or moduleName.\n * @param src Inline shader source code\n * @param moduleName Shader module name to load from filesystem\n * @param projectDir Project directory for module resolution\n * @returns Shader source code\n */\nexport async function resolveShaderSource(\n src?: string,\n moduleName?: string,\n projectDir?: string,\n): Promise<string> {\n validateSourceParams(src, moduleName);\n return moduleName\n ? await loadShaderSourceFromModule(moduleName, projectDir)\n : src!;\n}\n","import { copyBuffer, elementStride, type WgslElementType } from \"thimbleberry\";\nimport type { LinkParams } from \"wesl\";\nimport { withErrorScopes } from \"wesl-gpu\";\nimport { compileShader } from \"./CompileShader.ts\";\nimport { resolveShaderSource } from \"./ShaderModuleLoader.ts\"; // 4 elements\n\nexport interface ComputeTestParams {\n /** WESL/WGSL source code for the compute shader to test.\n * Either src or moduleName must be provided, but not both. */\n src?: string;\n\n /** Name of shader module to load from filesystem.\n * Supports: bare name (sum.wgsl), path (algorithms/sum.wgsl), or module path (package::algorithms::sum).\n * Either src or moduleName must be provided, but not both. */\n moduleName?: string;\n\n /** Project directory for resolving shader dependencies.\n * Allows the shader to import from npm shader libraries.\n * Optional: defaults to searching upward from cwd for package.json or wesl.toml.\n * Typically use `import.meta.url`. */\n projectDir?: string;\n\n /** GPU device for running the tests.\n * Typically use `getGPUDevice()` from wgsl-test. */\n device: GPUDevice;\n\n /** Format of the result buffer. Default: \"u32\" */\n resultFormat?: WgslElementType;\n\n /** Size of result buffer in elements. Default: 4 */\n size?: number;\n\n /** Flags for conditional compilation to test shader specialization.\n * Useful for testing `@if` statements in the shader. */\n conditions?: LinkParams[\"conditions\"];\n\n /** Constants for shader compilation.\n * Injects host-provided values via the `constants::` namespace. */\n constants?: LinkParams[\"constants\"];\n\n /** Use source shaders from current package instead of built bundles.\n * Default: true for faster iteration during development. */\n useSourceShaders?: boolean;\n\n /** Number of workgroups to dispatch. Default: 1\n * Can be a single number or [x, y, z] for multi-dimensional dispatch. */\n dispatchWorkgroups?: number | [number, number, number];\n}\n\nexport interface RunComputeParams {\n device: GPUDevice;\n module: GPUShaderModule;\n resultFormat?: WgslElementType;\n size?: number;\n dispatchWorkgroups?: number | [number, number, number];\n entryPoint?: string;\n}\n\nconst defaultResultSize = 4;\n\n/**\n * Compiles and runs a compute shader on the GPU for testing.\n *\n * Provides a storage buffer available at `test::results` where the shader\n * can write test output. After execution, the storage buffer is copied back\n * to the CPU and returned for validation.\n *\n * Shader libraries mentioned in the source are automatically resolved from node_modules.\n *\n * @returns Array of numbers from the storage buffer (typically 4 elements for u32/f32 format)\n */\nexport async function testCompute(\n params: ComputeTestParams,\n): Promise<number[]> {\n const {\n projectDir,\n device,\n src,\n moduleName,\n conditions = {},\n constants,\n useSourceShaders,\n dispatchWorkgroups = 1,\n } = params;\n const { resultFormat = \"u32\", size = defaultResultSize } = params;\n\n // Resolve shader source from either src or moduleName\n const shaderSrc = await resolveShaderSource(src, moduleName, projectDir);\n\n const arrayType = `array<${resultFormat}, ${size}>`;\n const virtualLibs = {\n test: () =>\n `@group(0) @binding(0) var <storage, read_write> results: ${arrayType};`,\n };\n\n const module = await compileShader({\n projectDir,\n device,\n src: shaderSrc,\n conditions,\n constants,\n virtualLibs,\n useSourceShaders,\n });\n\n return await runCompute({\n device,\n module,\n resultFormat,\n size,\n dispatchWorkgroups,\n });\n}\n\n/**\n * Runs a compiled compute shader and returns the result buffer.\n *\n * Creates a storage buffer at @group(0) @binding(0) where the shader can\n * write output. The shader is invoked once, then the buffer is copied back\n * to the CPU for reading.\n */\nexport async function runCompute(params: RunComputeParams): Promise<number[]> {\n const { device, module, entryPoint } = params;\n const { resultFormat = \"u32\", size = defaultResultSize } = params;\n const { dispatchWorkgroups = 1 } = params;\n return await withErrorScopes(device, async () => {\n const bgLayout = device.createBindGroupLayout({\n entries: [\n {\n binding: 0,\n visibility: GPUShaderStage.COMPUTE,\n buffer: { type: \"storage\" },\n },\n ],\n });\n\n const pipeline = device.createComputePipeline({\n layout: device.createPipelineLayout({ bindGroupLayouts: [bgLayout] }),\n compute: { module, entryPoint },\n });\n\n const storageBuffer = createStorageBuffer(\n device,\n size * elementStride(resultFormat),\n );\n const bindGroup = device.createBindGroup({\n layout: bgLayout,\n entries: [{ binding: 0, resource: { buffer: storageBuffer } }],\n });\n\n const commands = device.createCommandEncoder();\n const pass = commands.beginComputePass();\n pass.setPipeline(pipeline);\n pass.setBindGroup(0, bindGroup);\n if (typeof dispatchWorkgroups === \"number\") {\n pass.dispatchWorkgroups(dispatchWorkgroups);\n } else {\n pass.dispatchWorkgroups(...dispatchWorkgroups);\n }\n pass.end();\n device.queue.submit([commands.finish()]);\n\n return await copyBuffer(device, storageBuffer, resultFormat);\n });\n}\n\nfunction createStorageBuffer(device: GPUDevice, targetSize: number): GPUBuffer {\n const buffer = device.createBuffer({\n label: \"storage\",\n size: targetSize,\n usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,\n mappedAtCreation: true,\n });\n new Float32Array(buffer.getMappedRange()).fill(-999.0); // sentinel values\n buffer.unmap();\n return buffer;\n}\n","import type { FnElem, StandardAttribute, WeslAST } from \"wesl\";\n\nexport interface TestFunctionInfo {\n name: string;\n description?: string;\n fn: FnElem;\n}\n\n/** Format test name for display: \"fnName\" or \"fnName - description\" */\nexport function testDisplayName(name: string, description?: string): string {\n return description ? `${name} - ${description}` : name;\n}\n\n/** Find all functions marked with @test attribute in a parsed WESL module. */\nexport function findTestFunctions(ast: WeslAST): TestFunctionInfo[] {\n return ast.moduleElem.contents\n .filter((e): e is FnElem => e.kind === \"fn\")\n .filter(hasTestAttribute)\n .filter(fn => {\n if (fn.params.length > 0) {\n const name = fn.name.ident.originalName;\n console.warn(\n `@test function '${name}' has parameters and will be skipped`,\n );\n return false;\n }\n return true;\n })\n .map(fn => ({\n name: fn.name.ident.originalName,\n description: getTestDescription(fn),\n fn,\n }));\n}\n\nfunction hasTestAttribute(fn: FnElem): boolean {\n return !!getTestAttribute(fn);\n}\n\n/** Extract description from @test(description) attribute. */\nfunction getTestDescription(fn: FnElem): string | undefined {\n const testAttr = getTestAttribute(fn);\n const param = testAttr?.params?.[0];\n if (!param) return undefined;\n // Extract the identifier text from the expression contents\n const text = param.contents.find(c => c.kind === \"ref\");\n return text?.kind === \"ref\" ? text.ident.originalName : undefined;\n}\n\nfunction getTestAttribute(fn: FnElem): StandardAttribute | undefined {\n for (const e of fn.attributes ?? []) {\n const attr = e.attribute;\n if (attr.kind === \"@attribute\" && attr.name === \"test\") return attr;\n }\n}\n","/** Dynamically import vitest with helpful error if not installed. */\nexport async function importVitest(): Promise<typeof import(\"vitest\")> {\n try {\n return await import(\"vitest\");\n } catch {\n throw new Error(\n \"This function requires vitest. Use framework-agnostic APIs like testWesl() or testFragment() instead.\",\n );\n }\n}\n\n/** Dynamically import vitest-image-snapshot with helpful error if not installed. */\nexport async function importImageSnapshot(): Promise<\n typeof import(\"vitest-image-snapshot\")\n> {\n try {\n return await import(\"vitest-image-snapshot\");\n } catch {\n throw new Error(\n \"This function requires vitest-image-snapshot. Use testFragmentImage() for framework-agnostic testing.\",\n );\n }\n}\n","import { type LinkParams, parseSrcModule } from \"wesl\";\nimport weslBundle from \"../lib/weslBundle.js\";\nimport { compileShader } from \"./CompileShader.ts\";\nimport { resolveShaderSource } from \"./ShaderModuleLoader.ts\";\nimport { type ComputeTestParams, runCompute } from \"./TestComputeShader.ts\";\nimport {\n findTestFunctions,\n type TestFunctionInfo,\n testDisplayName,\n} from \"./TestDiscovery.ts\";\n\n/** Size of TestResult struct in bytes (u32 + u32 + padding + vec4f + vec4f = 48). */\nconst testResultSize = 48;\n\nimport { importVitest } from \"./VitestImport.ts\";\n\n/** Parameters for running @test functions in a WESL module. */\nexport type RunWeslParams = Omit<\n ComputeTestParams,\n \"resultFormat\" | \"size\" | \"dispatchWorkgroups\"\n> & {\n /** Run only the @test function with this name */\n testName?: string;\n};\n\n/** Result from running a single @test function on the GPU. */\nexport interface TestResult {\n name: string;\n passed: boolean;\n actual: number[];\n expected: number[];\n}\n\n/** Parameters for testWesl() which registers all @test functions with vitest. */\nexport type TestWeslParams = Omit<RunWeslParams, \"testName\">;\n\n/** Internal params for executing one @test function as a compute shader. */\ninterface RunSingleTestParams {\n testFn: TestFunctionInfo;\n shaderSrc: string;\n projectDir?: string;\n device: GPUDevice;\n conditions?: LinkParams[\"conditions\"];\n constants?: LinkParams[\"constants\"];\n useSourceShaders?: boolean;\n}\n\n/** Parsed WESL source with its AST for test discovery. */\ninterface ParsedTestModule {\n shaderSrc: string;\n ast: ReturnType<typeof parseSrcModule>;\n}\n\n/**\n * Discovers @test functions in a WESL module and registers each as a vitest test.\n * Use top-level await in your test file to call this function.\n */\nexport async function testWesl(params: TestWeslParams): Promise<void> {\n const { test } = await importVitest();\n const { ast } = await parseTestModule(params);\n const testFns = findTestFunctions(ast);\n for (const fn of testFns) {\n test(testDisplayName(fn.name, fn.description), async () => {\n await expectWesl({ ...params, testName: fn.name });\n });\n }\n}\n\n/**\n * Runs all @test functions and asserts they pass.\n * Throws descriptive error on failure showing test name and actual/expected values.\n */\nexport async function expectWesl(params: RunWeslParams): Promise<void> {\n const results = await runWesl(params);\n const failures = results.filter(r => !r.passed);\n\n if (failures.length > 0) {\n const messages = failures.map(f => {\n let msg = ` ${f.name}: FAILED`;\n msg += `\\n actual: [${f.actual.join(\", \")}]`;\n msg += `\\n expected: [${f.expected.join(\", \")}]`;\n return msg;\n });\n throw new Error(`WESL tests failed:\\n${messages.join(\"\\n\")}`);\n }\n}\n\n/**\n * Runs all @test functions in a WESL module.\n * Each test function is wrapped in a compute shader and dispatched.\n * Returns results for all tests.\n */\nexport async function runWesl(params: RunWeslParams): Promise<TestResult[]> {\n const { testName } = params;\n const { shaderSrc, ast } = await parseTestModule(params);\n let testFns = findTestFunctions(ast);\n if (testName) {\n testFns = testFns.filter(t => t.name === testName);\n }\n const results: TestResult[] = [];\n for (const testFn of testFns) {\n results.push(await runSingleTest({ testFn, shaderSrc, ...params }));\n }\n return results;\n}\n\n/** Load and parse a WESL module to extract @test functions. */\nasync function parseTestModule(params: {\n src?: string;\n moduleName?: string;\n projectDir?: string;\n}): Promise<ParsedTestModule> {\n const { projectDir, src, moduleName } = params;\n const shaderSrc = await resolveShaderSource(src, moduleName, projectDir);\n const modPath = moduleName || \"test\";\n const ast = parseSrcModule({\n modulePath: modPath,\n debugFilePath: modPath + \".wesl\",\n src: shaderSrc,\n });\n return { shaderSrc, ast };\n}\n\n/** Wrap a @test function in a compute shader, dispatch it, and return results. */\nasync function runSingleTest(params: RunSingleTestParams): Promise<TestResult> {\n const { testFn, shaderSrc, device, ...rest } = params;\n // Generate wrapper that calls the test function\n // Call initTestResult() function to initialize the result buffer\n const wrapper = `\nimport wgsl_test::TestResult::initTestResult;\n\n${shaderSrc}\n\n@compute @workgroup_size(1)\nfn _weslTestEntry() {\n initTestResult();\n ${testFn.name}();\n}\n`;\n const module = await compileShader({\n ...rest,\n device,\n src: wrapper,\n libs: [weslBundle],\n });\n\n const resultElems = testResultSize / 4; // 48 bytes / 4 bytes per u32 = 12\n const gpuResult = await runCompute({\n device,\n module,\n resultFormat: \"u32\",\n size: resultElems,\n entryPoint: \"_weslTestEntry\",\n });\n return parseTestResult(testFn.name, gpuResult);\n}\n\n/** Decode TestResult struct from GPU buffer (passed flag + actual/expected vec4f). */\nfunction parseTestResult(name: string, gpuResult: number[]): TestResult {\n // TestResult struct layout (with vec4f 16-byte alignment):\n // [0] passed (u32)\n // [1] failCount (u32)\n // [2-3] padding (8 bytes to align vec4f)\n // [4-7] actual (vec4f)\n // [8-11] expected (vec4f)\n const passed = gpuResult[0] === 1;\n\n // Reinterpret u32 bits as f32 for actual/expected (always captured now)\n const u32Array = new Uint32Array(gpuResult.slice(4, 12));\n const f32Array = new Float32Array(u32Array.buffer);\n const actual = Array.from(f32Array.slice(0, 4));\n const expected = Array.from(f32Array.slice(4, 8));\n\n return { name, passed, actual, expected };\n}\n","export const isDeno = !!(globalThis as any).Deno;\n\nlet sharedGpu: GPU | undefined;\nlet sharedAdapter: GPUAdapter | undefined;\nlet sharedDevice: GPUDevice | undefined;\n\n/** get or create shared GPU device for testing */\nexport async function getGPUDevice(): Promise<GPUDevice> {\n if (!sharedDevice) {\n const adapter = await getGPUAdapter();\n sharedDevice = await adapter.requestDevice();\n }\n return sharedDevice;\n}\n\n/** get or create shared GPU object for testing */\nexport async function getGPU(): Promise<GPU> {\n if (!sharedGpu) {\n if (isDeno) {\n sharedGpu = navigator.gpu;\n } else if (typeof navigator !== \"undefined\" && navigator.gpu) {\n sharedGpu = navigator.gpu;\n } else {\n const webgpu = await import(\"webgpu\");\n Object.assign(globalThis, webgpu.globals);\n sharedGpu = webgpu.create([]);\n }\n }\n return sharedGpu;\n}\n\n/** get or create shared GPU adapter for testing */\nexport async function getGPUAdapter(): Promise<GPUAdapter> {\n if (!sharedAdapter) {\n const gpu = await getGPU();\n const adapter = await gpu.requestAdapter();\n if (!adapter) throw new Error(\"Failed to get GPU adapter\");\n sharedAdapter = adapter;\n }\n return sharedAdapter;\n}\n\n/** destroy globally shared GPU test device */\nexport function destroySharedDevice(): void {\n sharedDevice?.destroy();\n sharedDevice = undefined;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"WebGPUTestSetup-f3N5MdWa.js","names":["path"],"sources":["../src/CompileShader.ts","../src/ShaderModuleLoader.ts","../src/TestComputeShader.ts","../src/TestDiscovery.ts","../src/VitestImport.ts","../src/TestWesl.ts","../src/WebGPUTestSetup.ts"],"sourcesContent":["import * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { LinkParams, ModuleResolver, WeslBundle } from \"wesl\";\nimport { CompositeResolver, link, RecordResolver } from \"wesl\";\nimport {\n dependencyBundles,\n FileModuleResolver,\n findWeslToml,\n readPackageJson,\n resolveProjectDir,\n} from \"wesl-tooling\";\n\nexport interface ShaderContext {\n /** Dependency bundles for the shader. */\n libs: WeslBundle[];\n\n /** Resolver for lazy loading (when useSourceShaders is true). */\n resolver?: ModuleResolver;\n\n /** Package name for module resolution. */\n packageName?: string;\n}\n\nexport interface ResolveContextParams {\n /** WESL/WGSL shader source code. */\n src: string;\n\n /** Project directory for resolving dependencies. */\n projectDir?: string;\n\n /** Use source shaders instead of built bundles. Default: true. */\n useSourceShaders?: boolean;\n\n /** Virtual lib names to exclude from dependency resolution. */\n virtualLibNames?: string[];\n}\n\nexport interface CompileShaderParams {\n /** Project directory for resolving shader dependencies.\n * Used to locate installed npm shader libraries.\n * Optional: defaults to searching upward from cwd for package.json or wesl.toml. */\n projectDir?: string;\n\n /** GPU device to use for shader compilation. */\n device: GPUDevice;\n\n /** WESL/WGSL shader source code to compile. */\n src: string;\n\n /** Conditions for conditional compilation.\n * Used to control `@if` directives in the shader. */\n conditions?: LinkParams[\"conditions\"];\n\n /** Constants for shader compilation.\n * Injects host-provided values via the `constants::` namespace. */\n constants?: LinkParams[\"constants\"];\n\n /** Virtual libraries to include in the shader.\n * Allows dynamic generation of shader code at runtime. */\n virtualLibs?: LinkParams[\"virtualLibs\"];\n\n /** Additional WESL bundles to include.\n * These are merged with auto-discovered dependencies. */\n libs?: WeslBundle[];\n\n /** Override the package name for module resolution.\n * Used to ensure package:: references resolve correctly. */\n packageName?: string;\n\n /** Use source shaders from current package instead of built bundles.\n * Default: true for faster iteration during development.\n * Set to false or use TEST_BUNDLES=true environment variable to test built bundles.\n *\n * Precedence: explicit parameter > TEST_BUNDLES env var > default (true)\n */\n useSourceShaders?: boolean;\n}\n\n/**\n * Compiles a WESL shader source string into a GPUShaderModule with automatic dependency resolution.\n *\n * Parses the shader source to detect references to shader packages, then automatically\n * includes the required npm package bundles. By default, loads source shaders from the\n * current package for fast iteration without requiring rebuilds.\n *\n * @returns Compiled GPUShaderModule ready for use in render or compute pipelines\n * @throws Error if shader compilation fails with compilation error details\n */\nexport async function compileShader(\n params: CompileShaderParams,\n): Promise<GPUShaderModule> {\n const { device, src, conditions, constants, virtualLibs, libs = [] } = params;\n const ctx = await resolveShaderContext({\n src,\n projectDir: params.projectDir,\n useSourceShaders: params.useSourceShaders,\n virtualLibNames: virtualLibs ? Object.keys(virtualLibs) : [],\n });\n\n // Filter out undefined values that can occur when auto-discovery finds packages\n // that aren't resolvable (e.g., wgsl_test when running tests within wgsl-test itself)\n const allLibs = [...ctx.libs, ...libs].filter(Boolean) as WeslBundle[];\n let linkParams: Pick<LinkParams, \"resolver\" | \"libs\" | \"weslSrc\">;\n if (ctx.resolver) {\n linkParams = { resolver: ctx.resolver, libs: allLibs };\n } else {\n linkParams = { weslSrc: { main: src }, libs: allLibs };\n }\n\n const linked = await link({\n ...linkParams,\n rootModuleName: \"main\",\n virtualLibs,\n conditions,\n constants,\n packageName: params.packageName ?? ctx.packageName,\n });\n const module = linked.createShaderModule(device);\n\n await verifyCompilation(module);\n return module;\n}\n\n/** Resolve project context for shader compilation: bundles, resolver, and package name. */\nexport async function resolveShaderContext(\n params: ResolveContextParams,\n): Promise<ShaderContext> {\n const { src, useSourceShaders = !process.env.TEST_BUNDLES } = params;\n const { virtualLibNames = [] } = params;\n const projectDir = await resolveProjectDir(params.projectDir);\n const packageName = await getPackageName(projectDir);\n\n const libs = await dependencyBundles(\n { main: src },\n projectDir,\n packageName,\n !useSourceShaders, // include current package when testing bundles\n virtualLibNames,\n );\n\n const resolver = useSourceShaders\n ? await lazyFileResolver(projectDir, src, packageName)\n : undefined;\n\n return { libs, resolver, packageName };\n}\n\n/** Create a project resolver for loading modules from the filesystem.\n * Handles wesl.toml configuration and creates FileModuleResolver with correct baseDir.\n *\n * @param projectDir Project directory (defaults to cwd)\n * @param packageName Package name for module resolution (optional)\n * @returns FileModuleResolver configured for the project\n */\nexport async function createProjectResolver(\n projectDir?: string,\n packageName?: string,\n): Promise<ModuleResolver> {\n const resolved = await resolveProjectDir(projectDir);\n const projectPath = fileURLToPath(resolved);\n const tomlInfo = await findWeslToml(projectPath);\n const baseDir = path.isAbsolute(tomlInfo.resolvedRoot)\n ? tomlInfo.resolvedRoot\n : path.join(projectPath, tomlInfo.resolvedRoot);\n\n return new FileModuleResolver(baseDir, packageName);\n}\n\n/** Verify shader compilation succeeded, throw on errors. */\nasync function verifyCompilation(module: GPUShaderModule): Promise<void> {\n const info = await module.getCompilationInfo();\n const errors = info.messages.filter(msg => msg.type === \"error\");\n if (errors.length > 0) {\n const messages = errors\n .map(e => `${e.lineNum}:${e.linePos} ${e.message}`)\n .join(\"\\n\");\n throw new Error(`Shader compilation failed:\\n${messages}`);\n }\n}\n\n/** Read package name from package.json, normalized for WGSL identifiers. */\nasync function getPackageName(projectDir: string): Promise<string | undefined> {\n try {\n const pkg = await readPackageJson(projectDir);\n const name = pkg.name as string;\n return name.replace(/-/g, \"_\");\n } catch {\n return undefined;\n }\n}\n\n/** Create a lazy resolver that loads local shaders on-demand from the filesystem.\n * Laziness allows testing without rebuilding the current package after edits. */\nasync function lazyFileResolver(\n projectDir: string,\n mainSrc: string,\n packageName: string | undefined,\n): Promise<CompositeResolver> {\n const mainResolver = new RecordResolver({ main: mainSrc }, { packageName });\n const fileResolver = await createProjectResolver(projectDir, packageName);\n return new CompositeResolver([mainResolver, fileResolver]);\n}\n","import { normalizeModuleName } from \"wesl\";\nimport { createProjectResolver } from \"./CompileShader.ts\";\n\n/** Validates that exactly one of src or moduleName is provided.\n * @throws Error if neither or both are provided */\nexport function validateSourceParams(src?: string, moduleName?: string): void {\n if (!src && !moduleName) {\n throw new Error(\"Either src or moduleName must be provided\");\n }\n if (src && moduleName) {\n throw new Error(\"Cannot provide both src and moduleName\");\n }\n}\n\n/** Loads shader source from module name using filesystem resolver.\n * @param moduleName Shader module name (bare name, path, or module path)\n * @param projectDir Project directory for module resolution\n * @returns Shader source code\n */\nexport async function loadShaderSourceFromModule(\n moduleName: string,\n projectDir?: string,\n): Promise<string> {\n const resolver = await createProjectResolver(projectDir);\n const normalizedName = normalizeModuleName(moduleName);\n const ast = resolver.resolveModule(normalizedName);\n if (!ast) throw new Error(`Could not resolve module: ${moduleName}`);\n return ast.srcModule.src;\n}\n\n/** Resolves shader source from either inline src or moduleName.\n * @param src Inline shader source code\n * @param moduleName Shader module name to load from filesystem\n * @param projectDir Project directory for module resolution\n * @returns Shader source code\n */\nexport async function resolveShaderSource(\n src?: string,\n moduleName?: string,\n projectDir?: string,\n): Promise<string> {\n validateSourceParams(src, moduleName);\n return moduleName\n ? await loadShaderSourceFromModule(moduleName, projectDir)\n : src!;\n}\n","import { copyBuffer, elementStride, type WgslElementType } from \"thimbleberry\";\nimport type { LinkParams } from \"wesl\";\nimport { withErrorScopes } from \"wesl-gpu\";\nimport { compileShader } from \"./CompileShader.ts\";\nimport { resolveShaderSource } from \"./ShaderModuleLoader.ts\"; // 4 elements\n\nexport interface ComputeTestParams {\n /** WESL/WGSL source code for the compute shader to test.\n * Either src or moduleName must be provided, but not both. */\n src?: string;\n\n /** Name of shader module to load from filesystem.\n * Supports: bare name (sum.wgsl), path (algorithms/sum.wgsl), or module path (package::algorithms::sum).\n * Either src or moduleName must be provided, but not both. */\n moduleName?: string;\n\n /** Project directory for resolving shader dependencies.\n * Allows the shader to import from npm shader libraries.\n * Optional: defaults to searching upward from cwd for package.json or wesl.toml.\n * Typically use `import.meta.url`. */\n projectDir?: string;\n\n /** GPU device for running the tests.\n * Typically use `getGPUDevice()` from wgsl-test. */\n device: GPUDevice;\n\n /** Format of the result buffer. Default: \"u32\" */\n resultFormat?: WgslElementType;\n\n /** Size of result buffer in elements. Default: 4 */\n size?: number;\n\n /** Flags for conditional compilation to test shader specialization.\n * Useful for testing `@if` statements in the shader. */\n conditions?: LinkParams[\"conditions\"];\n\n /** Constants for shader compilation.\n * Injects host-provided values via the `constants::` namespace. */\n constants?: LinkParams[\"constants\"];\n\n /** Use source shaders from current package instead of built bundles.\n * Default: true for faster iteration during development. */\n useSourceShaders?: boolean;\n\n /** Number of workgroups to dispatch. Default: 1\n * Can be a single number or [x, y, z] for multi-dimensional dispatch. */\n dispatchWorkgroups?: number | [number, number, number];\n}\n\nexport interface RunComputeParams {\n device: GPUDevice;\n module: GPUShaderModule;\n resultFormat?: WgslElementType;\n size?: number;\n dispatchWorkgroups?: number | [number, number, number];\n entryPoint?: string;\n}\n\nconst defaultResultSize = 4;\n\n/**\n * Compiles and runs a compute shader on the GPU for testing.\n *\n * Provides a storage buffer available at `test::results` where the shader\n * can write test output. After execution, the storage buffer is copied back\n * to the CPU and returned for validation.\n *\n * Shader libraries mentioned in the source are automatically resolved from node_modules.\n *\n * @returns Array of numbers from the storage buffer (typically 4 elements for u32/f32 format)\n */\nexport async function testCompute(\n params: ComputeTestParams,\n): Promise<number[]> {\n const {\n projectDir,\n device,\n src,\n moduleName,\n conditions = {},\n constants,\n useSourceShaders,\n dispatchWorkgroups = 1,\n } = params;\n const { resultFormat = \"u32\", size = defaultResultSize } = params;\n\n // Resolve shader source from either src or moduleName\n const shaderSrc = await resolveShaderSource(src, moduleName, projectDir);\n\n const arrayType = `array<${resultFormat}, ${size}>`;\n const virtualLibs = {\n test: () =>\n `@group(0) @binding(0) var <storage, read_write> results: ${arrayType};`,\n };\n\n const module = await compileShader({\n projectDir,\n device,\n src: shaderSrc,\n conditions,\n constants,\n virtualLibs,\n useSourceShaders,\n });\n\n return await runCompute({\n device,\n module,\n resultFormat,\n size,\n dispatchWorkgroups,\n });\n}\n\n/**\n * Runs a compiled compute shader and returns the result buffer.\n *\n * Creates a storage buffer at @group(0) @binding(0) where the shader can\n * write output. The shader is invoked once, then the buffer is copied back\n * to the CPU for reading.\n */\nexport async function runCompute(params: RunComputeParams): Promise<number[]> {\n const { device, module, entryPoint } = params;\n const { resultFormat = \"u32\", size = defaultResultSize } = params;\n const { dispatchWorkgroups = 1 } = params;\n return await withErrorScopes(device, async () => {\n const bgLayout = device.createBindGroupLayout({\n entries: [\n {\n binding: 0,\n visibility: GPUShaderStage.COMPUTE,\n buffer: { type: \"storage\" },\n },\n ],\n });\n\n const pipeline = device.createComputePipeline({\n layout: device.createPipelineLayout({ bindGroupLayouts: [bgLayout] }),\n compute: { module, entryPoint },\n });\n\n const storageBuffer = createStorageBuffer(\n device,\n size * elementStride(resultFormat),\n );\n const bindGroup = device.createBindGroup({\n layout: bgLayout,\n entries: [{ binding: 0, resource: { buffer: storageBuffer } }],\n });\n\n const commands = device.createCommandEncoder();\n const pass = commands.beginComputePass();\n pass.setPipeline(pipeline);\n pass.setBindGroup(0, bindGroup);\n if (typeof dispatchWorkgroups === \"number\") {\n pass.dispatchWorkgroups(dispatchWorkgroups);\n } else {\n pass.dispatchWorkgroups(...dispatchWorkgroups);\n }\n pass.end();\n device.queue.submit([commands.finish()]);\n\n return await copyBuffer(device, storageBuffer, resultFormat);\n });\n}\n\nfunction createStorageBuffer(device: GPUDevice, targetSize: number): GPUBuffer {\n const buffer = device.createBuffer({\n label: \"storage\",\n size: targetSize,\n usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,\n mappedAtCreation: true,\n });\n new Float32Array(buffer.getMappedRange()).fill(-999.0); // sentinel values\n buffer.unmap();\n return buffer;\n}\n","import type { FnElem, StandardAttribute, WeslAST } from \"wesl\";\n\nexport interface TestFunctionInfo {\n name: string;\n description?: string;\n fn: FnElem;\n}\n\n/** Format test name for display: \"fnName\" or \"fnName - description\" */\nexport function testDisplayName(name: string, description?: string): string {\n return description ? `${name} - ${description}` : name;\n}\n\n/** Find all functions marked with @test attribute in a parsed WESL module. */\nexport function findTestFunctions(ast: WeslAST): TestFunctionInfo[] {\n return ast.moduleElem.contents\n .filter((e): e is FnElem => e.kind === \"fn\")\n .filter(hasTestAttribute)\n .filter(fn => {\n if (fn.params.length > 0) {\n const name = fn.name.ident.originalName;\n console.warn(\n `@test function '${name}' has parameters and will be skipped`,\n );\n return false;\n }\n return true;\n })\n .map(fn => ({\n name: fn.name.ident.originalName,\n description: getTestDescription(fn),\n fn,\n }));\n}\n\nfunction hasTestAttribute(fn: FnElem): boolean {\n return !!getTestAttribute(fn);\n}\n\n/** Extract description from @test(description) attribute. */\nfunction getTestDescription(fn: FnElem): string | undefined {\n const testAttr = getTestAttribute(fn);\n const param = testAttr?.params?.[0];\n if (!param) return undefined;\n // Extract the identifier text from the expression contents\n const text = param.contents.find(c => c.kind === \"ref\");\n return text?.kind === \"ref\" ? text.ident.originalName : undefined;\n}\n\nfunction getTestAttribute(fn: FnElem): StandardAttribute | undefined {\n for (const e of fn.attributes ?? []) {\n const attr = e.attribute;\n if (attr.kind === \"@attribute\" && attr.name === \"test\") return attr;\n }\n}\n","/** Dynamically import vitest with helpful error if not installed. */\nexport async function importVitest(): Promise<typeof import(\"vitest\")> {\n try {\n return await import(\"vitest\");\n } catch {\n throw new Error(\n \"This function requires vitest. Use framework-agnostic APIs like testWesl() or testFragment() instead.\",\n );\n }\n}\n\n/** Dynamically import vitest-image-snapshot with helpful error if not installed. */\nexport async function importImageSnapshot(): Promise<\n typeof import(\"vitest-image-snapshot\")\n> {\n try {\n return await import(\"vitest-image-snapshot\");\n } catch {\n throw new Error(\n \"This function requires vitest-image-snapshot. Use testFragmentImage() for framework-agnostic testing.\",\n );\n }\n}\n","import { type LinkParams, parseSrcModule } from \"wesl\";\nimport weslBundle from \"../lib/weslBundle.js\";\nimport { compileShader } from \"./CompileShader.ts\";\nimport { resolveShaderSource } from \"./ShaderModuleLoader.ts\";\nimport { type ComputeTestParams, runCompute } from \"./TestComputeShader.ts\";\nimport {\n findTestFunctions,\n type TestFunctionInfo,\n testDisplayName,\n} from \"./TestDiscovery.ts\";\n\n/** Size of TestResult struct in bytes (u32 + u32 + padding + vec4f + vec4f = 48). */\nconst testResultSize = 48;\n\nimport { importVitest } from \"./VitestImport.ts\";\n\n/** Parameters for running @test functions in a WESL module. */\nexport type RunWeslParams = Omit<\n ComputeTestParams,\n \"resultFormat\" | \"size\" | \"dispatchWorkgroups\"\n> & {\n /** Run only the @test function with this name */\n testName?: string;\n};\n\n/** Result from running a single @test function on the GPU. */\nexport interface TestResult {\n name: string;\n passed: boolean;\n actual: number[];\n expected: number[];\n}\n\n/** Parameters for testWesl() which registers all @test functions with vitest. */\nexport type TestWeslParams = Omit<RunWeslParams, \"testName\">;\n\n/** Internal params for executing one @test function as a compute shader. */\ninterface RunSingleTestParams {\n testFn: TestFunctionInfo;\n shaderSrc: string;\n projectDir?: string;\n device: GPUDevice;\n conditions?: LinkParams[\"conditions\"];\n constants?: LinkParams[\"constants\"];\n useSourceShaders?: boolean;\n}\n\n/** Parsed WESL source with its AST for test discovery. */\ninterface ParsedTestModule {\n shaderSrc: string;\n ast: ReturnType<typeof parseSrcModule>;\n}\n\n/**\n * Discovers @test functions in a WESL module and registers each as a vitest test.\n * Use top-level await in your test file to call this function.\n */\nexport async function testWesl(params: TestWeslParams): Promise<void> {\n const { test } = await importVitest();\n const { ast } = await parseTestModule(params);\n const testFns = findTestFunctions(ast);\n for (const fn of testFns) {\n test(testDisplayName(fn.name, fn.description), async () => {\n await expectWesl({ ...params, testName: fn.name });\n });\n }\n}\n\n/**\n * Runs all @test functions and asserts they pass.\n * Throws descriptive error on failure showing test name and actual/expected values.\n */\nexport async function expectWesl(params: RunWeslParams): Promise<void> {\n const results = await runWesl(params);\n const failures = results.filter(r => !r.passed);\n\n if (failures.length > 0) {\n const messages = failures.map(f => {\n let msg = ` ${f.name}: FAILED`;\n msg += `\\n actual: [${f.actual.join(\", \")}]`;\n msg += `\\n expected: [${f.expected.join(\", \")}]`;\n return msg;\n });\n throw new Error(`WESL tests failed:\\n${messages.join(\"\\n\")}`);\n }\n}\n\n/**\n * Runs all @test functions in a WESL module.\n * Each test function is wrapped in a compute shader and dispatched.\n * Returns results for all tests.\n */\nexport async function runWesl(params: RunWeslParams): Promise<TestResult[]> {\n const { testName } = params;\n const { shaderSrc, ast } = await parseTestModule(params);\n let testFns = findTestFunctions(ast);\n if (testName) {\n testFns = testFns.filter(t => t.name === testName);\n }\n const results: TestResult[] = [];\n for (const testFn of testFns) {\n results.push(await runSingleTest({ testFn, shaderSrc, ...params }));\n }\n return results;\n}\n\n/** Load and parse a WESL module to extract @test functions. */\nasync function parseTestModule(params: {\n src?: string;\n moduleName?: string;\n projectDir?: string;\n}): Promise<ParsedTestModule> {\n const { projectDir, src, moduleName } = params;\n const shaderSrc = await resolveShaderSource(src, moduleName, projectDir);\n const modPath = moduleName || \"test\";\n const ast = parseSrcModule({\n modulePath: modPath,\n debugFilePath: modPath + \".wesl\",\n src: shaderSrc,\n });\n return { shaderSrc, ast };\n}\n\n/** Wrap a @test function in a compute shader, dispatch it, and return results. */\nasync function runSingleTest(params: RunSingleTestParams): Promise<TestResult> {\n const { testFn, shaderSrc, device, ...rest } = params;\n // Generate wrapper that calls the test function\n // Call initTestResult() function to initialize the result buffer\n const wrapper = `\nimport wgsl_test::TestResult::initTestResult;\n\n${shaderSrc}\n\n@compute @workgroup_size(1)\nfn _weslTestEntry() {\n initTestResult();\n ${testFn.name}();\n}\n`;\n const module = await compileShader({\n ...rest,\n device,\n src: wrapper,\n libs: [weslBundle],\n });\n\n const resultElems = testResultSize / 4; // 48 bytes / 4 bytes per u32 = 12\n const gpuResult = await runCompute({\n device,\n module,\n resultFormat: \"u32\",\n size: resultElems,\n entryPoint: \"_weslTestEntry\",\n });\n return parseTestResult(testFn.name, gpuResult);\n}\n\n/** Decode TestResult struct from GPU buffer (passed flag + actual/expected vec4f). */\nfunction parseTestResult(name: string, gpuResult: number[]): TestResult {\n // TestResult struct layout (with vec4f 16-byte alignment):\n // [0] passed (u32)\n // [1] failCount (u32)\n // [2-3] padding (8 bytes to align vec4f)\n // [4-7] actual (vec4f)\n // [8-11] expected (vec4f)\n const passed = gpuResult[0] === 1;\n\n // Reinterpret u32 bits as f32 for actual/expected (always captured now)\n const u32Array = new Uint32Array(gpuResult.slice(4, 12));\n const f32Array = new Float32Array(u32Array.buffer);\n const actual = Array.from(f32Array.slice(0, 4));\n const expected = Array.from(f32Array.slice(4, 8));\n\n return { name, passed, actual, expected };\n}\n","export const isDeno = !!(globalThis as any).Deno;\n\nlet sharedGpu: GPU | undefined;\nlet sharedAdapter: GPUAdapter | undefined;\nlet sharedDevice: GPUDevice | undefined;\n\n/** get or create shared GPU device for testing */\nexport async function getGPUDevice(): Promise<GPUDevice> {\n if (!sharedDevice) {\n const adapter = await getGPUAdapter();\n sharedDevice = await adapter.requestDevice();\n }\n return sharedDevice;\n}\n\n/** get or create shared GPU object for testing */\nexport async function getGPU(): Promise<GPU> {\n if (!sharedGpu) {\n if (isDeno) {\n sharedGpu = navigator.gpu;\n } else if (typeof navigator !== \"undefined\" && navigator.gpu) {\n sharedGpu = navigator.gpu;\n } else {\n const webgpu = await import(\"webgpu\");\n Object.assign(globalThis, webgpu.globals);\n sharedGpu = webgpu.create([]);\n }\n }\n return sharedGpu;\n}\n\n/** get or create shared GPU adapter for testing */\nexport async function getGPUAdapter(): Promise<GPUAdapter> {\n if (!sharedAdapter) {\n const gpu = await getGPU();\n const adapter = await gpu.requestAdapter();\n if (!adapter) throw new Error(\"Failed to get GPU adapter\");\n sharedAdapter = adapter;\n }\n return sharedAdapter;\n}\n\n/** destroy globally shared GPU test device */\nexport function destroySharedDevice(): void {\n sharedDevice?.destroy();\n sharedDevice = undefined;\n}\n"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,152 @@
|
|
|
1
|
-
import { _ as createProjectResolver, a as isDeno, c as testWesl, d as findTestFunctions, f as testDisplayName, g as compileShader, i as getGPUDevice, m as testCompute, n as getGPU, o as expectWesl, p as runCompute, r as getGPUAdapter, s as runWesl, t as destroySharedDevice, v as resolveShaderContext } from "./WebGPUTestSetup-
|
|
2
|
-
import {
|
|
1
|
+
import { _ as createProjectResolver, a as isDeno, c as testWesl, d as findTestFunctions, f as testDisplayName, g as compileShader, h as resolveShaderSource, i as getGPUDevice, l as importImageSnapshot, m as testCompute, n as getGPU, o as expectWesl, p as runCompute, r as getGPUAdapter, s as runWesl, t as destroySharedDevice, u as importVitest, v as resolveShaderContext } from "./WebGPUTestSetup-f3N5MdWa.js";
|
|
2
|
+
import { DeviceCache, DeviceCache as DeviceCache$1, checkerboardTexture, colorBarsTexture, createSampler, createUniformsVirtualLib, edgePatternTexture, fullscreenTriangleVertex, gradientTexture, noiseTexture, radialGradientTexture, renderUniformBuffer, runFragment, simpleRender, solidTexture, updateRenderUniforms, withErrorScopes } from "wesl-gpu";
|
|
3
|
+
import * as path$1 from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { normalizeModuleName } from "wesl";
|
|
6
|
+
import * as fs$1 from "node:fs/promises";
|
|
7
|
+
import { PNG } from "pngjs";
|
|
8
|
+
import { componentByteSize, numComponents, texelLoadType } from "thimbleberry";
|
|
3
9
|
|
|
4
|
-
|
|
10
|
+
//#region src/ImageHelpers.ts
|
|
11
|
+
/** Load PNG file and create GPU texture. */
|
|
12
|
+
async function pngToTexture(device, imagePath) {
|
|
13
|
+
const png = await loadPNG(imagePath);
|
|
14
|
+
const texture = device.createTexture({
|
|
15
|
+
label: `test-texture-photo-${path$1.basename(imagePath)}`,
|
|
16
|
+
size: {
|
|
17
|
+
width: png.width,
|
|
18
|
+
height: png.height,
|
|
19
|
+
depthOrArrayLayers: 1
|
|
20
|
+
},
|
|
21
|
+
format: "rgba8unorm",
|
|
22
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
|
|
23
|
+
});
|
|
24
|
+
device.queue.writeTexture({ texture }, new Uint8Array(png.data), { bytesPerRow: png.width * 4 }, {
|
|
25
|
+
width: png.width,
|
|
26
|
+
height: png.height
|
|
27
|
+
});
|
|
28
|
+
return texture;
|
|
29
|
+
}
|
|
30
|
+
async function loadPNG(imagePath) {
|
|
31
|
+
const fileData = await fs$1.readFile(imagePath);
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
new PNG().parse(fileData, (err, data) => {
|
|
34
|
+
if (err) reject(err);
|
|
35
|
+
else resolve(data);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/ExampleImages.ts
|
|
42
|
+
const textureCache = new DeviceCache$1();
|
|
43
|
+
/** return a texture to the bundled lemur test image. */
|
|
44
|
+
async function lemurTexture(device, size = 512) {
|
|
45
|
+
const lemurPath = lemurImagePath(size);
|
|
46
|
+
const cached = textureCache.get(device, lemurPath);
|
|
47
|
+
if (cached) return cached;
|
|
48
|
+
const texture = await pngToTexture(device, lemurImagePath(size));
|
|
49
|
+
textureCache.set(device, lemurPath, texture);
|
|
50
|
+
return texture;
|
|
51
|
+
}
|
|
52
|
+
/** Get the path to the bundled lemur test image. */
|
|
53
|
+
function lemurImagePath(size = 512) {
|
|
54
|
+
const moduleDir = path$1.dirname(fileURLToPath(import.meta.url));
|
|
55
|
+
return path$1.join(moduleDir, "..", "images", `lemur${size}.png`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/TestFragmentShader.ts
|
|
60
|
+
/** Run a fragment shader test and validate image snapshot.
|
|
61
|
+
* @param device GPU device for rendering
|
|
62
|
+
* @param moduleName Shader name to load - supports:
|
|
63
|
+
* - Bare name: "blur.wgsl" → resolves to shaders/blur.wgsl
|
|
64
|
+
* - Relative path: "effects/blur.wgsl" → resolves to shaders/effects/blur.wgsl
|
|
65
|
+
* - Module path: "package::effects::blur" → same resolution
|
|
66
|
+
* @param opts Test parameters (size defaults to 256×256 for snapshots)
|
|
67
|
+
*/
|
|
68
|
+
async function expectFragmentImage(device, moduleName, opts = {}) {
|
|
69
|
+
const { textureFormat = "rgba32float", size = [256, 256] } = opts;
|
|
70
|
+
const imageData = await testFragmentImage({
|
|
71
|
+
...opts,
|
|
72
|
+
device,
|
|
73
|
+
moduleName,
|
|
74
|
+
textureFormat,
|
|
75
|
+
size
|
|
76
|
+
});
|
|
77
|
+
const snapshotName = opts.snapshotName ?? moduleNameToSnapshotName(moduleName);
|
|
78
|
+
const { imageMatcher } = await importImageSnapshot();
|
|
79
|
+
imageMatcher();
|
|
80
|
+
const { expect } = await importVitest();
|
|
81
|
+
await expect(imageData).toMatchImage(snapshotName);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Renders a fragment shader and returns pixel (0,0) color values for validation.
|
|
85
|
+
*
|
|
86
|
+
* Useful for simple color tests where you only need to check a single pixel result.
|
|
87
|
+
*
|
|
88
|
+
* @returns Array of color component values from pixel (0,0)
|
|
89
|
+
*/
|
|
90
|
+
async function testFragment(params) {
|
|
91
|
+
const { textureFormat = "rgba32float" } = params;
|
|
92
|
+
const data = await runFragment$1(params);
|
|
93
|
+
const count = numComponents(textureFormat);
|
|
94
|
+
return data.slice(0, count);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Renders a fragment shader and returns the complete rendered image.
|
|
98
|
+
*
|
|
99
|
+
* Useful for image snapshot testing or when you need to validate the entire output.
|
|
100
|
+
* For snapshot testing shader files, consider using `expectFragmentImage` instead.
|
|
101
|
+
*
|
|
102
|
+
* @returns ImageData containing the full rendered output
|
|
103
|
+
*/
|
|
104
|
+
async function testFragmentImage(params) {
|
|
105
|
+
const { textureFormat = "rgba32float", size = [1, 1] } = params;
|
|
106
|
+
const data = imageToUint8(await runFragment$1(params), textureFormat, size[0], size[1]);
|
|
107
|
+
return {
|
|
108
|
+
data: new Uint8ClampedArray(data),
|
|
109
|
+
width: size[0],
|
|
110
|
+
height: size[1],
|
|
111
|
+
colorSpace: "srgb"
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** Convert module path to snapshot name (e.g., "package::effects::blur" → "effects-blur") */
|
|
115
|
+
function moduleNameToSnapshotName(moduleName) {
|
|
116
|
+
return normalizeModuleName(moduleName).replace(/^package::/, "").replaceAll("::", "-");
|
|
117
|
+
}
|
|
118
|
+
async function runFragment$1(params) {
|
|
119
|
+
const { projectDir, src, moduleName, useSourceShaders } = params;
|
|
120
|
+
const fragmentSrc = await resolveShaderSource(src, moduleName, projectDir);
|
|
121
|
+
const ctx = await resolveShaderContext({
|
|
122
|
+
src: fragmentSrc,
|
|
123
|
+
projectDir,
|
|
124
|
+
useSourceShaders,
|
|
125
|
+
virtualLibNames: ["test"]
|
|
126
|
+
});
|
|
127
|
+
return runFragment({
|
|
128
|
+
...params,
|
|
129
|
+
src: fragmentSrc,
|
|
130
|
+
libs: params.libs ?? ctx.libs,
|
|
131
|
+
resolver: params.resolver ?? ctx.resolver,
|
|
132
|
+
packageName: params.packageName ?? ctx.packageName
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/** Convert texture data to RGBA Uint8ClampedArray for image comparison. */
|
|
136
|
+
function imageToUint8(data, format, width, height) {
|
|
137
|
+
const totalPixels = width * height;
|
|
138
|
+
const components = numComponents(format);
|
|
139
|
+
const byteSize = componentByteSize(format);
|
|
140
|
+
const texelType = texelLoadType(format);
|
|
141
|
+
if (byteSize === 1 && format.includes("unorm")) return data instanceof Uint8ClampedArray ? data : new Uint8ClampedArray(Array.from(data));
|
|
142
|
+
if (texelType === "f32") {
|
|
143
|
+
const uint8Data = new Uint8ClampedArray(totalPixels * 4);
|
|
144
|
+
for (let i = 0; i < totalPixels * components; i++) uint8Data[i] = Math.round(Math.max(0, Math.min(1, data[i])) * 255);
|
|
145
|
+
return uint8Data;
|
|
146
|
+
}
|
|
147
|
+
throw new Error(`Unsupported texture format for image export: ${format}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
//#endregion
|
|
151
|
+
export { DeviceCache, checkerboardTexture, colorBarsTexture, compileShader, createProjectResolver, createSampler, createUniformsVirtualLib, destroySharedDevice, edgePatternTexture, expectFragmentImage, expectWesl, findTestFunctions, fullscreenTriangleVertex, getGPU, getGPUAdapter, getGPUDevice, gradientTexture, isDeno, lemurImagePath, lemurTexture, noiseTexture, pngToTexture, radialGradientTexture, renderUniformBuffer, resolveShaderContext, runCompute, runWesl, simpleRender, solidTexture, testCompute, testDisplayName, testFragment, testFragmentImage, testWesl, updateRenderUniforms, withErrorScopes };
|
|
152
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["path","fs","DeviceCache","path","runFragment","runFragmentCore"],"sources":["../src/ImageHelpers.ts","../src/ExampleImages.ts","../src/TestFragmentShader.ts"],"sourcesContent":["import * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { PNG } from \"pngjs\";\n\n/** Load PNG file and create GPU texture. */\nexport async function pngToTexture(\n device: GPUDevice,\n imagePath: string,\n): Promise<GPUTexture> {\n const png = await loadPNG(imagePath);\n\n const texture = device.createTexture({\n label: `test-texture-photo-${path.basename(imagePath)}`,\n size: { width: png.width, height: png.height, depthOrArrayLayers: 1 },\n format: \"rgba8unorm\",\n usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,\n });\n\n device.queue.writeTexture(\n { texture },\n new Uint8Array(png.data),\n { bytesPerRow: png.width * 4 },\n { width: png.width, height: png.height },\n );\n\n return texture;\n}\n\nasync function loadPNG(imagePath: string): Promise<PNG> {\n const fileData = await fs.readFile(imagePath);\n return new Promise<PNG>((resolve, reject) => {\n const png = new PNG();\n png.parse(fileData, (err, data) => {\n if (err) reject(err);\n else resolve(data);\n });\n });\n}\n","import * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { DeviceCache } from \"wesl-gpu\";\nimport { pngToTexture } from \"./ImageHelpers.ts\";\n\nconst textureCache = new DeviceCache<GPUTexture>();\n\n/** return a texture to the bundled lemur test image. */\nexport async function lemurTexture(\n device: GPUDevice,\n size: 256 | 512 = 512,\n): Promise<GPUTexture> {\n const lemurPath = lemurImagePath(size);\n const cached = textureCache.get(device, lemurPath);\n if (cached) return cached;\n\n const texture = await pngToTexture(device, lemurImagePath(size));\n textureCache.set(device, lemurPath, texture);\n\n return texture;\n}\n\n/** Get the path to the bundled lemur test image. */\nexport function lemurImagePath(size: 256 | 512 = 512): string {\n const moduleDir = path.dirname(fileURLToPath(import.meta.url));\n return path.join(moduleDir, \"..\", \"images\", `lemur${size}.png`);\n}\n","import { componentByteSize, numComponents, texelLoadType } from \"thimbleberry\";\nimport type { ImageData } from \"vitest-image-snapshot\";\nimport { normalizeModuleName } from \"wesl\";\nimport {\n type FragmentRenderParams,\n runFragment as runFragmentCore,\n type WeslOptions,\n} from \"wesl-gpu\";\nimport { resolveShaderContext } from \"./CompileShader.ts\";\nimport { resolveShaderSource } from \"./ShaderModuleLoader.ts\";\nimport { importImageSnapshot, importVitest } from \"./VitestImport.ts\";\n\nexport interface FragmentTestParams extends WeslOptions, FragmentRenderParams {\n /** WESL/WGSL source code for the fragment shader to test.\n * Either src or moduleName must be provided, but not both. */\n src?: string;\n\n /** Name of shader module to load from filesystem.\n * Supports: bare name (blur.wgsl), path (effects/blur.wgsl), or module path (package::effects::blur).\n * Either src or moduleName must be provided, but not both. */\n moduleName?: string;\n\n /** Project directory for resolving shader dependencies.\n * Allows the shader to import from npm shader libraries.\n * Optional: defaults to searching upward from cwd for package.json or wesl.toml.\n * Typically use `import.meta.url`. */\n projectDir?: string;\n\n /** Use source shaders from current package instead of built bundles.\n * Default: true for faster iteration during development. */\n useSourceShaders?: boolean;\n}\n\nexport interface FragmentImageTestParams\n extends Omit<FragmentTestParams, \"src\" | \"moduleName\" | \"device\"> {\n /** Optional snapshot name override. If not provided, derived from shader name. */\n snapshotName?: string;\n}\n\n/** Run a fragment shader test and validate image snapshot.\n * @param device GPU device for rendering\n * @param moduleName Shader name to load - supports:\n * - Bare name: \"blur.wgsl\" → resolves to shaders/blur.wgsl\n * - Relative path: \"effects/blur.wgsl\" → resolves to shaders/effects/blur.wgsl\n * - Module path: \"package::effects::blur\" → same resolution\n * @param opts Test parameters (size defaults to 256×256 for snapshots)\n */\nexport async function expectFragmentImage(\n device: GPUDevice,\n moduleName: string,\n opts: FragmentImageTestParams = {},\n): Promise<void> {\n const { textureFormat = \"rgba32float\", size = [256, 256] } = opts;\n\n // Render shader image using moduleName\n const imageData = await testFragmentImage({\n ...opts,\n device,\n moduleName,\n textureFormat,\n size,\n });\n\n const snapshotName =\n opts.snapshotName ?? moduleNameToSnapshotName(moduleName);\n const { imageMatcher } = await importImageSnapshot();\n imageMatcher();\n const { expect } = await importVitest();\n await expect(imageData).toMatchImage(snapshotName);\n}\n\n/**\n * Renders a fragment shader and returns pixel (0,0) color values for validation.\n *\n * Useful for simple color tests where you only need to check a single pixel result.\n *\n * @returns Array of color component values from pixel (0,0)\n */\nexport async function testFragment(\n params: FragmentTestParams,\n): Promise<number[]> {\n const { textureFormat = \"rgba32float\" } = params;\n const data = await runFragment(params);\n const count = numComponents(textureFormat);\n return data.slice(0, count);\n}\n\n/**\n * Renders a fragment shader and returns the complete rendered image.\n *\n * Useful for image snapshot testing or when you need to validate the entire output.\n * For snapshot testing shader files, consider using `expectFragmentImage` instead.\n *\n * @returns ImageData containing the full rendered output\n */\nexport async function testFragmentImage(\n params: FragmentTestParams,\n): Promise<ImageData> {\n const { textureFormat = \"rgba32float\", size = [1, 1] } = params;\n const texData = await runFragment(params);\n const data = imageToUint8(texData, textureFormat, size[0], size[1]);\n return {\n data: new Uint8ClampedArray(data),\n width: size[0],\n height: size[1],\n colorSpace: \"srgb\" as const,\n } as ImageData;\n}\n\n/** Convert module path to snapshot name (e.g., \"package::effects::blur\" → \"effects-blur\") */\nfunction moduleNameToSnapshotName(moduleName: string): string {\n const normalized = normalizeModuleName(moduleName);\n return normalized\n .replace(/^package::/, \"\") // Strip \"package::\" prefix\n .replaceAll(\"::\", \"-\"); // Replace :: with -\n}\n\nasync function runFragment(params: FragmentTestParams): Promise<number[]> {\n const { projectDir, src, moduleName, useSourceShaders } = params;\n\n // Resolve shader source from either src or moduleName\n const fragmentSrc = await resolveShaderSource(src, moduleName, projectDir);\n\n // Resolve context (libs, resolver, packageName) from project\n // Note: \"test\" virtualLib is provided by wesl-gpu for test::Uniforms\n const ctx = await resolveShaderContext({\n src: fragmentSrc,\n projectDir,\n useSourceShaders,\n virtualLibNames: [\"test\"],\n });\n\n // Use shared runFragment with resolved source and context\n return runFragmentCore({\n ...params,\n src: fragmentSrc,\n libs: params.libs ?? ctx.libs,\n resolver: params.resolver ?? ctx.resolver,\n packageName: params.packageName ?? ctx.packageName,\n });\n}\n\n/** Convert texture data to RGBA Uint8ClampedArray for image comparison. */\nfunction imageToUint8(\n data: ArrayLike<number>,\n format: GPUTextureFormat,\n width: number,\n height: number,\n): Uint8ClampedArray {\n const totalPixels = width * height;\n const components = numComponents(format);\n const byteSize = componentByteSize(format);\n const texelType = texelLoadType(format);\n\n if (byteSize === 1 && format.includes(\"unorm\")) {\n return data instanceof Uint8ClampedArray\n ? data\n : new Uint8ClampedArray(Array.from(data));\n }\n\n if (texelType === \"f32\") {\n const uint8Data = new Uint8ClampedArray(totalPixels * 4);\n for (let i = 0; i < totalPixels * components; i++) {\n uint8Data[i] = Math.round(Math.max(0, Math.min(1, data[i])) * 255);\n }\n return uint8Data;\n }\n\n throw new Error(`Unsupported texture format for image export: ${format}`);\n}\n"],"mappings":"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"}
|