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 +1 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.js +199 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Utilities for nodejs wesl tools
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|