wesl-tooling 0.6.2

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 ADDED
@@ -0,0 +1 @@
1
+ Utilities for nodejs wesl tools
@@ -0,0 +1,95 @@
1
+ import { VirtualLibraryFn, WeslBundle, WeslDevice } from "wesl";
2
+ import { GPUElementFormat } from "thimbleberry";
3
+
4
+ //#region src/ParseDependencies.d.ts
5
+ /**
6
+ * Find the wesl package dependencies in a set of WESL files
7
+ * (for packaging WESL files into a library)
8
+ *
9
+ * Parse the WESL files and partially bind the identifiers,
10
+ * returning any identifiers that are not succesfully bound.
11
+ * Those identifiers are the package dependencies.
12
+ *
13
+ * The dependency might be a default export bundle or
14
+ * a named export bundle. e.g. for 'foo::bar::baz', it could be
15
+ * . package foo, export '.' bundle, module bar
16
+ * . package foo, export './bar' bundle, element baz
17
+ * . package foo, export './bar/baz' bundle, module lib.wesl, element baz
18
+ * To distinguish these, we node resolve the longest path we can.
19
+ */
20
+ /**
21
+ * Find the wesl package dependencies in a set of WESL files
22
+ * (for packaging WESL files into a library)
23
+ *
24
+ * Parse the WESL files and partially bind the identifiers,
25
+ * returning any identifiers that are not succesfully bound.
26
+ * Those identifiers are the package dependencies.
27
+ *
28
+ * The dependency might be a default export bundle or
29
+ * a named export bundle. e.g. for 'foo::bar::baz', it could be
30
+ * . package foo, export '.' bundle, module bar
31
+ * . package foo, export './bar' bundle, element baz
32
+ * . package foo, export './bar/baz' bundle, module lib.wesl, element baz
33
+ * To distinguish these, we node resolve the longest path we can.
34
+ */
35
+ declare function parseDependencies(weslSrc: Record<string, string>, projectDir: string): string[];
36
+ /** @return WeslBundle instances referenced by wesl sources
37
+ *
38
+ * Parse the WESL files to find references to external WESL modules,
39
+ * and then load those modules (weslBundle.js files) using node dynamic imports.
40
+ */
41
+ declare function dependencyBundles(weslSrc: Record<string, string>, projectDir: string): Promise<WeslBundle[]>;
42
+
43
+ //#endregion
44
+ //#region src/SimpleComputeShader.d.ts
45
+ /**
46
+ * Compiles a single WESL shader source string into a GPUShaderModule for testing
47
+ * with automatic package detection.
48
+ *
49
+ * Parses the shader source to find references to wesl packages, and
50
+ * then searches installed npm packages to find the appropriate npm package
51
+ * bundle to include in the link.
52
+ *
53
+ * @param projectDir - The project directory, used for resolving dependencies.
54
+ * @param device - The WeslDevice to use for shader compilation.
55
+ * @param src - The WESL shader source code.
56
+ * @returns A Promise that resolves to the compiled GPUShaderModule.
57
+ */
58
+ declare function compileShader(projectDir: string, device: WeslDevice, src: string, virtualLibs?: Record<string, VirtualLibraryFn>): Promise<GPUShaderModule>;
59
+ /**
60
+ * Transpiles and runs a simple compute shader on the GPU for testing.
61
+ *
62
+ * A storage buffer is available for the shader to write test results.
63
+ * `test::results[0]` is the first element of the buffer in wesl.
64
+ * After execution the storage buffer is copied back to the CPU and returned
65
+ * for test validation.
66
+ *
67
+ * Shader libraries mentioned in the shader source are attached automatically
68
+ * if they are in node_modules.
69
+ *
70
+ * @param module - The compiled GPUShaderModule containing the compute shader.
71
+ * The shader is invoked once.
72
+ * @param resultFormat - format for interpreting the result buffer data. (default u32)
73
+ * @returns storage result array (typically four numbers if the buffer format is u32 or f32)
74
+ */
75
+ declare function testComputeShader(projectDir: string, gpu: GPU, src: string, resultFormat?: GPUElementFormat): Promise<number[]>;
76
+ /**
77
+ * Transpiles and runs a simple compute shader on the GPU for testing.
78
+ *
79
+ * a 16 byte storage buffer is available for the shader at `@group(0) @binding(0)`.
80
+ * Compute shaders can write test results into the buffer.
81
+ * After execution the storage buffer is copied back to the CPU and returned
82
+ * for test validation.
83
+ *
84
+ * Shader libraries mentioned in the shader source are attached automatically
85
+ * if they are in node_modules.
86
+ *
87
+ * @param module - The compiled GPUShaderModule containing the compute shader.
88
+ * The shader is invoked once.
89
+ * @param resultFormat - format for interpreting the result buffer data. (default u32)
90
+ * @returns storage result array
91
+ */
92
+ declare function runSimpleComputePipeline(device: GPUDevice, module: GPUShaderModule, resultFormat?: GPUElementFormat): Promise<number[]>;
93
+
94
+ //#endregion
95
+ export { compileShader, dependencyBundles, parseDependencies, runSimpleComputePipeline, testComputeShader };
package/dist/index.js ADDED
@@ -0,0 +1,199 @@
1
+ import { resolve } from "import-meta-resolve";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { WeslParseError, filterMap, findUnboundIdents, link, parseIntoRegistry, parsedRegistry, requestWeslDevice } from "wesl";
5
+ import { copyBuffer } from "thimbleberry";
6
+
7
+ //#region src/ParseDependencies.ts
8
+ /**
9
+ * Find the wesl package dependencies in a set of WESL files
10
+ * (for packaging WESL files into a library)
11
+ *
12
+ * Parse the WESL files and partially bind the identifiers,
13
+ * returning any identifiers that are not succesfully bound.
14
+ * Those identifiers are the package dependencies.
15
+ *
16
+ * The dependency might be a default export bundle or
17
+ * a named export bundle. e.g. for 'foo::bar::baz', it could be
18
+ * . package foo, export '.' bundle, module bar
19
+ * . package foo, export './bar' bundle, element baz
20
+ * . package foo, export './bar/baz' bundle, module lib.wesl, element baz
21
+ * To distinguish these, we node resolve the longest path we can.
22
+ */
23
+ function parseDependencies(weslSrc, projectDir) {
24
+ const registry = parsedRegistry();
25
+ try {
26
+ parseIntoRegistry(weslSrc, registry);
27
+ } catch (e) {
28
+ if (e.cause instanceof WeslParseError) console.error(e.message, "\n");
29
+ else throw e;
30
+ }
31
+ const unbound = findUnboundIdents(registry);
32
+ if (!unbound) return [];
33
+ const pkgRefs = unbound.filter((modulePath) => modulePath.length > 1);
34
+ if (pkgRefs.length === 0) return [];
35
+ const fullProjectDir = path.resolve(path.join(projectDir, "foo"));
36
+ const projectURL = pathToFileURL(fullProjectDir).href;
37
+ const deps = filterMap(pkgRefs, (mPath) => unboundToDependency(mPath, projectURL));
38
+ const uniqueDeps = [...new Set(deps)];
39
+ return uniqueDeps;
40
+ }
41
+ /**
42
+ * Find the longest resolvable npm subpath from a module path.
43
+ *
44
+ * @param mPath module path, e.g. ['foo', 'bar', 'baz', 'elem']
45
+ * @param importerURL URL of the importer, e.g. 'file:///path/to/project/foo/bar/baz.wesl' (doesn't need to be a real file)
46
+ * @returns longest resolvable subpath of mPath, e.g. 'foo/bar/baz' or 'foo/bar'
47
+ */
48
+ function unboundToDependency(mPath, importerURL) {
49
+ return exportSubpaths(mPath).find((subPath) => tryResolve(subPath, importerURL));
50
+ }
51
+ /** Try to resolve a path using node's resolve algorithm.
52
+ * @return the resolved path */
53
+ function tryResolve(path$1, importerURL) {
54
+ try {
55
+ return resolve(path$1, importerURL);
56
+ } catch (e) {
57
+ return void 0;
58
+ }
59
+ }
60
+ /**
61
+ * Yield possible export entry subpaths from module path
62
+ * longest subpath first.
63
+ */
64
+ function* exportSubpaths(mPath) {
65
+ const longest = mPath.length - 1;
66
+ for (let i = longest; i >= 0; i--) {
67
+ const subPath = mPath.slice(0, i).join("/");
68
+ yield subPath;
69
+ }
70
+ }
71
+ /** @return WeslBundle instances referenced by wesl sources
72
+ *
73
+ * Parse the WESL files to find references to external WESL modules,
74
+ * and then load those modules (weslBundle.js files) using node dynamic imports.
75
+ */
76
+ async function dependencyBundles(weslSrc, projectDir) {
77
+ const deps = parseDependencies(weslSrc, projectDir);
78
+ const bundles = deps.map(async (dep) => {
79
+ const url = resolve(dep, projectDir);
80
+ const module = await import(url);
81
+ return module.default;
82
+ });
83
+ return await Promise.all(bundles);
84
+ }
85
+
86
+ //#endregion
87
+ //#region src/SimpleComputeShader.ts
88
+ const resultBufferSize = 16;
89
+ /**
90
+ * Compiles a single WESL shader source string into a GPUShaderModule for testing
91
+ * with automatic package detection.
92
+ *
93
+ * Parses the shader source to find references to wesl packages, and
94
+ * then searches installed npm packages to find the appropriate npm package
95
+ * bundle to include in the link.
96
+ *
97
+ * @param projectDir - The project directory, used for resolving dependencies.
98
+ * @param device - The WeslDevice to use for shader compilation.
99
+ * @param src - The WESL shader source code.
100
+ * @returns A Promise that resolves to the compiled GPUShaderModule.
101
+ */
102
+ async function compileShader(projectDir, device, src, virtualLibs) {
103
+ const weslSrc = { main: src };
104
+ const libs = await dependencyBundles(weslSrc, projectDir);
105
+ const linked = await link({
106
+ weslSrc,
107
+ libs,
108
+ virtualLibs
109
+ });
110
+ return device.createShaderModule({ code: linked.dest });
111
+ }
112
+ /**
113
+ * Transpiles and runs a simple compute shader on the GPU for testing.
114
+ *
115
+ * A storage buffer is available for the shader to write test results.
116
+ * `test::results[0]` is the first element of the buffer in wesl.
117
+ * After execution the storage buffer is copied back to the CPU and returned
118
+ * for test validation.
119
+ *
120
+ * Shader libraries mentioned in the shader source are attached automatically
121
+ * if they are in node_modules.
122
+ *
123
+ * @param module - The compiled GPUShaderModule containing the compute shader.
124
+ * The shader is invoked once.
125
+ * @param resultFormat - format for interpreting the result buffer data. (default u32)
126
+ * @returns storage result array (typically four numbers if the buffer format is u32 or f32)
127
+ */
128
+ async function testComputeShader(projectDir, gpu, src, resultFormat = "f32") {
129
+ const adapter = await gpu.requestAdapter();
130
+ const device = await requestWeslDevice(adapter);
131
+ try {
132
+ const arraySize = resultBufferSize / elementByteSize(resultFormat);
133
+ const arrayType = `array<${resultFormat}, ${arraySize}>`;
134
+ const virtualLibs = { test: () => `@group(0) @binding(0) var <storage, read_write> results: ${arrayType};` };
135
+ const module = await compileShader(projectDir, device, src, virtualLibs);
136
+ const result = await runSimpleComputePipeline(device, module, resultFormat);
137
+ return result;
138
+ } finally {
139
+ device.destroy();
140
+ }
141
+ }
142
+ /** size in bytes of a wgsl numeric type, e.g. 'f32' => 4 */
143
+ function elementByteSize(fmt) {
144
+ const found = fmt.match(/\d+/);
145
+ const bits = Number.parseInt(found?.[0]);
146
+ return bits / 8;
147
+ }
148
+ /**
149
+ * Transpiles and runs a simple compute shader on the GPU for testing.
150
+ *
151
+ * a 16 byte storage buffer is available for the shader at `@group(0) @binding(0)`.
152
+ * Compute shaders can write test results into the buffer.
153
+ * After execution the storage buffer is copied back to the CPU and returned
154
+ * for test validation.
155
+ *
156
+ * Shader libraries mentioned in the shader source are attached automatically
157
+ * if they are in node_modules.
158
+ *
159
+ * @param module - The compiled GPUShaderModule containing the compute shader.
160
+ * The shader is invoked once.
161
+ * @param resultFormat - format for interpreting the result buffer data. (default u32)
162
+ * @returns storage result array
163
+ */
164
+ async function runSimpleComputePipeline(device, module, resultFormat) {
165
+ const bgLayout = device.createBindGroupLayout({ entries: [{
166
+ binding: 0,
167
+ visibility: GPUShaderStage.COMPUTE,
168
+ buffer: { type: "storage" }
169
+ }] });
170
+ const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [bgLayout] });
171
+ const pipeline = device.createComputePipeline({
172
+ layout: pipelineLayout,
173
+ compute: { module }
174
+ });
175
+ const storageBuffer = device.createBuffer({
176
+ label: "storage",
177
+ size: resultBufferSize,
178
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
179
+ });
180
+ const bindGroup = device.createBindGroup({
181
+ layout: bgLayout,
182
+ entries: [{
183
+ binding: 0,
184
+ resource: { buffer: storageBuffer }
185
+ }]
186
+ });
187
+ const commands = device.createCommandEncoder();
188
+ const pass = commands.beginComputePass();
189
+ pass.setPipeline(pipeline);
190
+ pass.setBindGroup(0, bindGroup);
191
+ pass.dispatchWorkgroups(1);
192
+ pass.end();
193
+ device.queue.submit([commands.finish()]);
194
+ const data = await copyBuffer(device, storageBuffer, resultFormat);
195
+ return data;
196
+ }
197
+
198
+ //#endregion
199
+ export { compileShader, dependencyBundles, parseDependencies, runSimpleComputePipeline, testComputeShader };
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "wesl-tooling",
3
+ "version": "0.6.2",
4
+ "type": "module",
5
+ "files": [
6
+ "dist"
7
+ ],
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "dependencies": {
12
+ "import-meta-resolve": "^4.1.0",
13
+ "thimbleberry": "^0.2.9"
14
+ },
15
+ "devDependencies": {
16
+ "dependent_package": "x",
17
+ "tsdown": "^0.11.3",
18
+ "wesl": "0.6.2"
19
+ },
20
+ "scripts": {
21
+ "echo": "echo",
22
+ "build": "tsdown",
23
+ "dev": "tsdown --watch",
24
+ "format": "prettier . --write",
25
+ "lint": "eslint src",
26
+ "typecheck": "tsc",
27
+ "test": "FORCE_COLOR=1 vitest",
28
+ "test:once": "vitest run"
29
+ }
30
+ }