wgsl-test 0.2.1
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/README.md +111 -0
- package/dist/index.d.ts +230 -0
- package/dist/index.js +11556 -0
- package/dist/index.js.map +1 -0
- package/images/lemur256.png +0 -0
- package/images/lemur512.png +0 -0
- package/package.json +61 -0
- package/src/CompileShader.ts +185 -0
- package/src/ExampleImages.ts +27 -0
- package/src/ImageHelpers.ts +38 -0
- package/src/ShaderModuleLoader.ts +46 -0
- package/src/TestComputeShader.ts +178 -0
- package/src/TestFragmentShader.ts +216 -0
- package/src/WebGPUTestSetup.ts +30 -0
- package/src/index.ts +41 -0
- package/src/test/ImageSnapshot.test.ts +212 -0
- package/src/test/PackageImport.test.ts +110 -0
- package/src/test/TestComputeShader.test.ts +231 -0
- package/src/test/TestFragmentShader.test.ts +397 -0
- package/src/test/__image_snapshots__/box-blur.png +0 -0
- package/src/test/__image_snapshots__/edge-detection.png +0 -0
- package/src/test/__image_snapshots__/effects-checkerboard.png +0 -0
- package/src/test/__image_snapshots__/grayscale.png +0 -0
- package/src/test/__image_snapshots__/lemur-sharpen.png +0 -0
- package/src/test/__image_snapshots__/solid-red-small.png +0 -0
- package/src/test/__image_snapshots__/solid_red.png +0 -0
- package/src/test/fixtures/test_shader_pkg/package.json +10 -0
- package/src/test/fixtures/test_shader_pkg/shaders/algorithms/compute_multiply.wgsl +5 -0
- package/src/test/fixtures/test_shader_pkg/shaders/compute_sum.wgsl +5 -0
- package/src/test/fixtures/test_shader_pkg/shaders/effects/checkerboard.wgsl +11 -0
- package/src/test/fixtures/test_shader_pkg/shaders/foo/bar/zap.wesl +5 -0
- package/src/test/fixtures/test_shader_pkg/shaders/foo/bar.wesl +4 -0
- package/src/test/fixtures/test_shader_pkg/shaders/legacy.wgsl +3 -0
- package/src/test/fixtures/test_shader_pkg/shaders/math.wesl +3 -0
- package/src/test/fixtures/test_shader_pkg/shaders/nested/deeper/func.wesl +3 -0
- package/src/test/fixtures/test_shader_pkg/shaders/priority.wesl +9 -0
- package/src/test/fixtures/test_shader_pkg/shaders/solid_red.wgsl +4 -0
- package/src/test/fixtures/test_shader_pkg/shaders/utils.wesl +3 -0
- package/src/test/fixtures/test_shader_pkg/wesl.toml +3 -0
- package/src/test/fixtures/test_shader_pkg/weslBundle.js +16 -0
|
Binary file
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wgsl-test",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist",
|
|
7
|
+
"images",
|
|
8
|
+
"src"
|
|
9
|
+
],
|
|
10
|
+
"repository": "github:wgsl-tooling-wg/wesl-js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"pngjs": "^7.0.0",
|
|
19
|
+
"thimbleberry": "^0.2.10",
|
|
20
|
+
"webgpu": "^0.3.8",
|
|
21
|
+
"wesl": "0.6.47",
|
|
22
|
+
"wesl-gpu": "0.1.1"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/pngjs": "^6.0.0",
|
|
26
|
+
"@webgpu/types": "^0.1.65",
|
|
27
|
+
"dependent_package": "x",
|
|
28
|
+
"wesl-tooling": "x"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"vitest": "^3.2.4",
|
|
32
|
+
"vitest-image-snapshot": "^0.6.38"
|
|
33
|
+
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"vitest": {
|
|
36
|
+
"optional": true
|
|
37
|
+
},
|
|
38
|
+
"vitest-image-snapshot": {
|
|
39
|
+
"optional": true
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"shader",
|
|
44
|
+
"snapshot",
|
|
45
|
+
"test",
|
|
46
|
+
"testing",
|
|
47
|
+
"unit-test",
|
|
48
|
+
"visual-regression",
|
|
49
|
+
"WebGPU",
|
|
50
|
+
"WESL",
|
|
51
|
+
"WGSL"
|
|
52
|
+
],
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsdown",
|
|
55
|
+
"dev": "tsdown --watch",
|
|
56
|
+
"test": "cross-env FORCE_COLOR=1 vitest",
|
|
57
|
+
"test:once": "vitest run",
|
|
58
|
+
"typecheck": "tsgo"
|
|
59
|
+
},
|
|
60
|
+
"main": "./dist/index.js"
|
|
61
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import type { LinkParams, ModuleResolver, WeslBundle } from "wesl";
|
|
4
|
+
import { CompositeResolver, link, RecordResolver } from "wesl";
|
|
5
|
+
import {
|
|
6
|
+
dependencyBundles,
|
|
7
|
+
FileModuleResolver,
|
|
8
|
+
findWeslToml,
|
|
9
|
+
readPackageJson,
|
|
10
|
+
resolveProjectDir,
|
|
11
|
+
} from "wesl-tooling";
|
|
12
|
+
|
|
13
|
+
export interface ShaderContext {
|
|
14
|
+
/** Dependency bundles for the shader. */
|
|
15
|
+
libs: WeslBundle[];
|
|
16
|
+
|
|
17
|
+
/** Resolver for lazy loading (when useSourceShaders is true). */
|
|
18
|
+
resolver?: ModuleResolver;
|
|
19
|
+
|
|
20
|
+
/** Package name for module resolution. */
|
|
21
|
+
packageName?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ResolveContextParams {
|
|
25
|
+
/** WESL/WGSL shader source code. */
|
|
26
|
+
src: string;
|
|
27
|
+
|
|
28
|
+
/** Project directory for resolving dependencies. */
|
|
29
|
+
projectDir?: string;
|
|
30
|
+
|
|
31
|
+
/** Use source shaders instead of built bundles. Default: true. */
|
|
32
|
+
useSourceShaders?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CompileShaderParams {
|
|
36
|
+
/** Project directory for resolving shader dependencies.
|
|
37
|
+
* Used to locate installed npm shader libraries.
|
|
38
|
+
* Optional: defaults to searching upward from cwd for package.json or wesl.toml. */
|
|
39
|
+
projectDir?: string;
|
|
40
|
+
|
|
41
|
+
/** GPU device to use for shader compilation. */
|
|
42
|
+
device: GPUDevice;
|
|
43
|
+
|
|
44
|
+
/** WESL/WGSL shader source code to compile. */
|
|
45
|
+
src: string;
|
|
46
|
+
|
|
47
|
+
/** Conditions for conditional compilation.
|
|
48
|
+
* Used to control `@if` directives in the shader. */
|
|
49
|
+
conditions?: LinkParams["conditions"];
|
|
50
|
+
|
|
51
|
+
/** Constants for shader compilation.
|
|
52
|
+
* Injects host-provided values via the `constants::` namespace. */
|
|
53
|
+
constants?: LinkParams["constants"];
|
|
54
|
+
|
|
55
|
+
/** Virtual libraries to include in the shader.
|
|
56
|
+
* Allows dynamic generation of shader code at runtime. */
|
|
57
|
+
virtualLibs?: LinkParams["virtualLibs"];
|
|
58
|
+
|
|
59
|
+
/** Use source shaders from current package instead of built bundles.
|
|
60
|
+
* Default: true for faster iteration during development.
|
|
61
|
+
* Set to false or use TEST_BUNDLES=true environment variable to test built bundles.
|
|
62
|
+
*
|
|
63
|
+
* Precedence: explicit parameter > TEST_BUNDLES env var > default (true)
|
|
64
|
+
*/
|
|
65
|
+
useSourceShaders?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Compiles a WESL shader source string into a GPUShaderModule with automatic dependency resolution.
|
|
70
|
+
*
|
|
71
|
+
* Parses the shader source to detect references to shader packages, then automatically
|
|
72
|
+
* includes the required npm package bundles. By default, loads source shaders from the
|
|
73
|
+
* current package for fast iteration without requiring rebuilds.
|
|
74
|
+
*
|
|
75
|
+
* @returns Compiled GPUShaderModule ready for use in render or compute pipelines
|
|
76
|
+
* @throws Error if shader compilation fails with compilation error details
|
|
77
|
+
*/
|
|
78
|
+
export async function compileShader(
|
|
79
|
+
params: CompileShaderParams,
|
|
80
|
+
): Promise<GPUShaderModule> {
|
|
81
|
+
const { device, src, conditions, constants, virtualLibs } = params;
|
|
82
|
+
const ctx = await resolveShaderContext({
|
|
83
|
+
src,
|
|
84
|
+
projectDir: params.projectDir,
|
|
85
|
+
useSourceShaders: params.useSourceShaders,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
let linkParams: Pick<LinkParams, "resolver" | "libs" | "weslSrc">;
|
|
89
|
+
if (ctx.resolver) {
|
|
90
|
+
linkParams = { resolver: ctx.resolver, libs: ctx.libs };
|
|
91
|
+
} else {
|
|
92
|
+
linkParams = { weslSrc: { main: src }, libs: ctx.libs };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const linked = await link({
|
|
96
|
+
...linkParams,
|
|
97
|
+
rootModuleName: "main",
|
|
98
|
+
virtualLibs,
|
|
99
|
+
conditions,
|
|
100
|
+
constants,
|
|
101
|
+
packageName: ctx.packageName,
|
|
102
|
+
});
|
|
103
|
+
const module = linked.createShaderModule(device);
|
|
104
|
+
|
|
105
|
+
await verifyCompilation(module);
|
|
106
|
+
return module;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Resolve project context for shader compilation: bundles, resolver, and package name. */
|
|
110
|
+
export async function resolveShaderContext(
|
|
111
|
+
params: ResolveContextParams,
|
|
112
|
+
): Promise<ShaderContext> {
|
|
113
|
+
const { src, useSourceShaders = !process.env.TEST_BUNDLES } = params;
|
|
114
|
+
const projectDir = await resolveProjectDir(params.projectDir);
|
|
115
|
+
const packageName = await getPackageName(projectDir);
|
|
116
|
+
|
|
117
|
+
const libs = await dependencyBundles(
|
|
118
|
+
{ main: src },
|
|
119
|
+
projectDir,
|
|
120
|
+
packageName,
|
|
121
|
+
!useSourceShaders, // include current package when testing bundles
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const resolver = useSourceShaders
|
|
125
|
+
? await lazyFileResolver(projectDir, src, packageName)
|
|
126
|
+
: undefined;
|
|
127
|
+
|
|
128
|
+
return { libs, resolver, packageName };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Create a project resolver for loading modules from the filesystem.
|
|
132
|
+
* Handles wesl.toml configuration and creates FileModuleResolver with correct baseDir.
|
|
133
|
+
*
|
|
134
|
+
* @param projectDir Project directory (defaults to cwd)
|
|
135
|
+
* @param packageName Package name for module resolution (optional)
|
|
136
|
+
* @returns FileModuleResolver configured for the project
|
|
137
|
+
*/
|
|
138
|
+
export async function createProjectResolver(
|
|
139
|
+
projectDir?: string,
|
|
140
|
+
packageName?: string,
|
|
141
|
+
): Promise<ModuleResolver> {
|
|
142
|
+
const resolved = await resolveProjectDir(projectDir);
|
|
143
|
+
const projectPath = fileURLToPath(resolved);
|
|
144
|
+
const tomlInfo = await findWeslToml(projectPath);
|
|
145
|
+
const baseDir = path.isAbsolute(tomlInfo.resolvedRoot)
|
|
146
|
+
? tomlInfo.resolvedRoot
|
|
147
|
+
: path.join(projectPath, tomlInfo.resolvedRoot);
|
|
148
|
+
|
|
149
|
+
return new FileModuleResolver(baseDir, packageName);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Create a lazy resolver that loads local shaders on-demand from the filesystem.
|
|
153
|
+
* Laziness allows testing without rebuilding the current package after edits. */
|
|
154
|
+
async function lazyFileResolver(
|
|
155
|
+
projectDir: string,
|
|
156
|
+
mainSrc: string,
|
|
157
|
+
packageName: string | undefined,
|
|
158
|
+
): Promise<CompositeResolver> {
|
|
159
|
+
const mainResolver = new RecordResolver({ main: mainSrc }, { packageName });
|
|
160
|
+
const fileResolver = await createProjectResolver(projectDir, packageName);
|
|
161
|
+
return new CompositeResolver([mainResolver, fileResolver]);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Read package name from package.json, normalized for WGSL identifiers. */
|
|
165
|
+
async function getPackageName(projectDir: string): Promise<string | undefined> {
|
|
166
|
+
try {
|
|
167
|
+
const pkg = await readPackageJson(projectDir);
|
|
168
|
+
const name = pkg.name as string;
|
|
169
|
+
return name.replace(/-/g, "_");
|
|
170
|
+
} catch {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Verify shader compilation succeeded, throw on errors. */
|
|
176
|
+
async function verifyCompilation(module: GPUShaderModule): Promise<void> {
|
|
177
|
+
const info = await module.getCompilationInfo();
|
|
178
|
+
const errors = info.messages.filter(msg => msg.type === "error");
|
|
179
|
+
if (errors.length > 0) {
|
|
180
|
+
const messages = errors
|
|
181
|
+
.map(e => `${e.lineNum}:${e.linePos} ${e.message}`)
|
|
182
|
+
.join("\n");
|
|
183
|
+
throw new Error(`Shader compilation failed:\n${messages}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { DeviceCache } from "wesl-gpu";
|
|
4
|
+
import { pngToTexture } from "./ImageHelpers.ts";
|
|
5
|
+
|
|
6
|
+
const textureCache = new DeviceCache<GPUTexture>();
|
|
7
|
+
|
|
8
|
+
/** return a texture to the bundled lemur test image. */
|
|
9
|
+
export async function lemurTexture(
|
|
10
|
+
device: GPUDevice,
|
|
11
|
+
size: 256 | 512 = 512,
|
|
12
|
+
): Promise<GPUTexture> {
|
|
13
|
+
const lemurPath = lemurImagePath(size);
|
|
14
|
+
const cached = textureCache.get(device, lemurPath);
|
|
15
|
+
if (cached) return cached;
|
|
16
|
+
|
|
17
|
+
const texture = await pngToTexture(device, lemurImagePath(size));
|
|
18
|
+
textureCache.set(device, lemurPath, texture);
|
|
19
|
+
|
|
20
|
+
return texture;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Get the path to the bundled lemur test image. */
|
|
24
|
+
export function lemurImagePath(size: 256 | 512 = 512): string {
|
|
25
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
return path.join(moduleDir, "..", "images", `lemur${size}.png`);
|
|
27
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { PNG } from "pngjs";
|
|
4
|
+
|
|
5
|
+
/** Load PNG file and create GPU texture. */
|
|
6
|
+
export async function pngToTexture(
|
|
7
|
+
device: GPUDevice,
|
|
8
|
+
imagePath: string,
|
|
9
|
+
): Promise<GPUTexture> {
|
|
10
|
+
const png = await loadPNG(imagePath);
|
|
11
|
+
|
|
12
|
+
const texture = device.createTexture({
|
|
13
|
+
label: `test-texture-photo-${path.basename(imagePath)}`,
|
|
14
|
+
size: { width: png.width, height: png.height, depthOrArrayLayers: 1 },
|
|
15
|
+
format: "rgba8unorm",
|
|
16
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
device.queue.writeTexture(
|
|
20
|
+
{ texture },
|
|
21
|
+
new Uint8Array(png.data),
|
|
22
|
+
{ bytesPerRow: png.width * 4 },
|
|
23
|
+
{ width: png.width, height: png.height },
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
return texture;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function loadPNG(imagePath: string): Promise<PNG> {
|
|
30
|
+
const fileData = await fs.readFile(imagePath);
|
|
31
|
+
return new Promise<PNG>((resolve, reject) => {
|
|
32
|
+
const png = new PNG();
|
|
33
|
+
png.parse(fileData, (err, data) => {
|
|
34
|
+
if (err) reject(err);
|
|
35
|
+
else resolve(data);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { normalizeModuleName } from "wesl";
|
|
2
|
+
import { createProjectResolver } from "./CompileShader.ts";
|
|
3
|
+
|
|
4
|
+
/** Validates that exactly one of src or moduleName is provided.
|
|
5
|
+
* @throws Error if neither or both are provided */
|
|
6
|
+
export function validateSourceParams(src?: string, moduleName?: string): void {
|
|
7
|
+
if (!src && !moduleName) {
|
|
8
|
+
throw new Error("Either src or moduleName must be provided");
|
|
9
|
+
}
|
|
10
|
+
if (src && moduleName) {
|
|
11
|
+
throw new Error("Cannot provide both src and moduleName");
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Loads shader source from module name using filesystem resolver.
|
|
16
|
+
* @param moduleName Shader module name (bare name, path, or module path)
|
|
17
|
+
* @param projectDir Project directory for module resolution
|
|
18
|
+
* @returns Shader source code
|
|
19
|
+
*/
|
|
20
|
+
export async function loadShaderSourceFromModule(
|
|
21
|
+
moduleName: string,
|
|
22
|
+
projectDir?: string,
|
|
23
|
+
): Promise<string> {
|
|
24
|
+
const resolver = await createProjectResolver(projectDir);
|
|
25
|
+
const normalizedName = normalizeModuleName(moduleName);
|
|
26
|
+
const ast = resolver.resolveModule(normalizedName);
|
|
27
|
+
if (!ast) throw new Error(`Could not resolve module: ${moduleName}`);
|
|
28
|
+
return ast.srcModule.src;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Resolves shader source from either inline src or moduleName.
|
|
32
|
+
* @param src Inline shader source code
|
|
33
|
+
* @param moduleName Shader module name to load from filesystem
|
|
34
|
+
* @param projectDir Project directory for module resolution
|
|
35
|
+
* @returns Shader source code
|
|
36
|
+
*/
|
|
37
|
+
export async function resolveShaderSource(
|
|
38
|
+
src?: string,
|
|
39
|
+
moduleName?: string,
|
|
40
|
+
projectDir?: string,
|
|
41
|
+
): Promise<string> {
|
|
42
|
+
validateSourceParams(src, moduleName);
|
|
43
|
+
return moduleName
|
|
44
|
+
? await loadShaderSourceFromModule(moduleName, projectDir)
|
|
45
|
+
: src!;
|
|
46
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { copyBuffer, elementStride, type WgslElementType } from "thimbleberry";
|
|
2
|
+
import type { LinkParams } from "wesl";
|
|
3
|
+
import { withErrorScopes } from "wesl-gpu";
|
|
4
|
+
import { compileShader } from "./CompileShader.ts";
|
|
5
|
+
import { resolveShaderSource } from "./ShaderModuleLoader.ts";
|
|
6
|
+
|
|
7
|
+
const defaultResultSize = 4; // 4 elements
|
|
8
|
+
|
|
9
|
+
export interface ComputeTestParams {
|
|
10
|
+
/** WESL/WGSL source code for the compute shader to test.
|
|
11
|
+
* Either src or moduleName must be provided, but not both. */
|
|
12
|
+
src?: string;
|
|
13
|
+
|
|
14
|
+
/** Name of shader module to load from filesystem.
|
|
15
|
+
* Supports: bare name (sum.wgsl), path (algorithms/sum.wgsl), or module path (package::algorithms::sum).
|
|
16
|
+
* Either src or moduleName must be provided, but not both. */
|
|
17
|
+
moduleName?: string;
|
|
18
|
+
|
|
19
|
+
/** Project directory for resolving shader dependencies.
|
|
20
|
+
* Allows the shader to import from npm shader libraries.
|
|
21
|
+
* Optional: defaults to searching upward from cwd for package.json or wesl.toml.
|
|
22
|
+
* Typically use `import.meta.url`. */
|
|
23
|
+
projectDir?: string;
|
|
24
|
+
|
|
25
|
+
/** GPU device for running the tests.
|
|
26
|
+
* Typically use `getGPUDevice()` from wgsl-test. */
|
|
27
|
+
device: GPUDevice;
|
|
28
|
+
|
|
29
|
+
/** Format of the result buffer. Default: "u32" */
|
|
30
|
+
resultFormat?: WgslElementType;
|
|
31
|
+
|
|
32
|
+
/** Size of result buffer in elements. Default: 4 */
|
|
33
|
+
size?: number;
|
|
34
|
+
|
|
35
|
+
/** Flags for conditional compilation to test shader specialization.
|
|
36
|
+
* Useful for testing `@if` statements in the shader. */
|
|
37
|
+
conditions?: LinkParams["conditions"];
|
|
38
|
+
|
|
39
|
+
/** Constants for shader compilation.
|
|
40
|
+
* Injects host-provided values via the `constants::` namespace. */
|
|
41
|
+
constants?: LinkParams["constants"];
|
|
42
|
+
|
|
43
|
+
/** Use source shaders from current package instead of built bundles.
|
|
44
|
+
* Default: true for faster iteration during development. */
|
|
45
|
+
useSourceShaders?: boolean;
|
|
46
|
+
|
|
47
|
+
/** Number of workgroups to dispatch. Default: 1
|
|
48
|
+
* Can be a single number or [x, y, z] for multi-dimensional dispatch. */
|
|
49
|
+
dispatchWorkgroups?: number | [number, number, number];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Compiles and runs a compute shader on the GPU for testing.
|
|
54
|
+
*
|
|
55
|
+
* Provides a storage buffer available at `test::results` where the shader
|
|
56
|
+
* can write test output. After execution, the storage buffer is copied back
|
|
57
|
+
* to the CPU and returned for validation.
|
|
58
|
+
*
|
|
59
|
+
* Shader libraries mentioned in the source are automatically resolved from node_modules.
|
|
60
|
+
*
|
|
61
|
+
* @returns Array of numbers from the storage buffer (typically 4 elements for u32/f32 format)
|
|
62
|
+
*/
|
|
63
|
+
export async function testCompute(
|
|
64
|
+
params: ComputeTestParams,
|
|
65
|
+
): Promise<number[]> {
|
|
66
|
+
const {
|
|
67
|
+
projectDir,
|
|
68
|
+
device,
|
|
69
|
+
src,
|
|
70
|
+
moduleName,
|
|
71
|
+
conditions = {},
|
|
72
|
+
constants,
|
|
73
|
+
useSourceShaders,
|
|
74
|
+
dispatchWorkgroups = 1,
|
|
75
|
+
} = params;
|
|
76
|
+
const { resultFormat = "u32", size = defaultResultSize } = params;
|
|
77
|
+
|
|
78
|
+
// Resolve shader source from either src or moduleName
|
|
79
|
+
const shaderSrc = await resolveShaderSource(src, moduleName, projectDir);
|
|
80
|
+
|
|
81
|
+
const arrayType = `array<${resultFormat}, ${size}>`;
|
|
82
|
+
const virtualLibs = {
|
|
83
|
+
test: () =>
|
|
84
|
+
`@group(0) @binding(0) var <storage, read_write> results: ${arrayType};`,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const module = await compileShader({
|
|
88
|
+
projectDir,
|
|
89
|
+
device,
|
|
90
|
+
src: shaderSrc,
|
|
91
|
+
conditions,
|
|
92
|
+
constants,
|
|
93
|
+
virtualLibs,
|
|
94
|
+
useSourceShaders,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return await runCompute(
|
|
98
|
+
device,
|
|
99
|
+
module,
|
|
100
|
+
resultFormat,
|
|
101
|
+
size,
|
|
102
|
+
dispatchWorkgroups,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Runs a compiled compute shader and returns the result buffer.
|
|
108
|
+
*
|
|
109
|
+
* Creates a storage buffer at @group(0) @binding(0) where the shader can
|
|
110
|
+
* write output. The shader is invoked once, then the buffer is copied back
|
|
111
|
+
* to the CPU for reading.
|
|
112
|
+
*
|
|
113
|
+
* @param module - The compiled GPUShaderModule containing the compute shader
|
|
114
|
+
* @param resultFormat - Format for interpreting result buffer data (default: u32)
|
|
115
|
+
* @param size - Size of result buffer in bytes (default: 16)
|
|
116
|
+
* @param dispatchWorkgroups - Number of workgroups to dispatch (default: 1)
|
|
117
|
+
* @returns Array containing the shader's output from the storage buffer
|
|
118
|
+
*/
|
|
119
|
+
export async function runCompute(
|
|
120
|
+
device: GPUDevice,
|
|
121
|
+
module: GPUShaderModule,
|
|
122
|
+
resultFormat: WgslElementType = "u32",
|
|
123
|
+
size = defaultResultSize,
|
|
124
|
+
dispatchWorkgroups: number | [number, number, number] = 1,
|
|
125
|
+
): Promise<number[]> {
|
|
126
|
+
return await withErrorScopes(device, async () => {
|
|
127
|
+
const bgLayout = device.createBindGroupLayout({
|
|
128
|
+
entries: [
|
|
129
|
+
{
|
|
130
|
+
binding: 0,
|
|
131
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
132
|
+
buffer: { type: "storage" },
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const pipeline = device.createComputePipeline({
|
|
138
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [bgLayout] }),
|
|
139
|
+
compute: { module },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const storageBuffer = createStorageBuffer(
|
|
143
|
+
device,
|
|
144
|
+
size * elementStride(resultFormat),
|
|
145
|
+
);
|
|
146
|
+
const bindGroup = device.createBindGroup({
|
|
147
|
+
layout: bgLayout,
|
|
148
|
+
entries: [{ binding: 0, resource: { buffer: storageBuffer } }],
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const commands = device.createCommandEncoder();
|
|
152
|
+
const pass = commands.beginComputePass();
|
|
153
|
+
pass.setPipeline(pipeline);
|
|
154
|
+
pass.setBindGroup(0, bindGroup);
|
|
155
|
+
if (typeof dispatchWorkgroups === "number") {
|
|
156
|
+
pass.dispatchWorkgroups(dispatchWorkgroups);
|
|
157
|
+
} else {
|
|
158
|
+
pass.dispatchWorkgroups(...dispatchWorkgroups);
|
|
159
|
+
}
|
|
160
|
+
pass.end();
|
|
161
|
+
device.queue.submit([commands.finish()]);
|
|
162
|
+
|
|
163
|
+
return await copyBuffer(device, storageBuffer, resultFormat);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function createStorageBuffer(device: GPUDevice, targetSize: number): GPUBuffer {
|
|
168
|
+
const buffer = device.createBuffer({
|
|
169
|
+
label: "storage",
|
|
170
|
+
size: targetSize,
|
|
171
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
|
|
172
|
+
mappedAtCreation: true,
|
|
173
|
+
});
|
|
174
|
+
// Sentinel values to detect unwritten results
|
|
175
|
+
new Float32Array(buffer.getMappedRange()).fill(-999.0);
|
|
176
|
+
buffer.unmap();
|
|
177
|
+
return buffer;
|
|
178
|
+
}
|