@simulatte/webgpu 0.2.4 → 0.3.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/CHANGELOG.md +33 -0
- package/README.md +263 -71
- package/api-contract.md +70 -139
- package/assets/package-layers.svg +63 -0
- package/examples/direct-webgpu/compute-dispatch.js +66 -0
- package/examples/direct-webgpu/explicit-bind-group.js +85 -0
- package/examples/direct-webgpu/request-device.js +10 -0
- package/examples/doe-api/buffers-readback.js +9 -0
- package/examples/doe-api/compile-and-dispatch.js +30 -0
- package/examples/doe-api/compute-dispatch.js +25 -0
- package/examples/doe-routines/compute-once-like-input.js +36 -0
- package/examples/doe-routines/compute-once-matmul.js +53 -0
- package/examples/doe-routines/compute-once-multiple-inputs.js +27 -0
- package/examples/doe-routines/compute-once.js +23 -0
- package/headless-webgpu-comparison.md +2 -2
- package/layering-plan.md +1 -1
- package/native/doe_napi.c +102 -12
- package/package.json +2 -1
- package/prebuilds/darwin-arm64/doe_napi.node +0 -0
- package/prebuilds/darwin-arm64/libwebgpu_doe.dylib +0 -0
- package/prebuilds/darwin-arm64/metadata.json +6 -6
- package/prebuilds/linux-x64/doe_napi.node +0 -0
- package/prebuilds/linux-x64/libwebgpu_doe.so +0 -0
- package/prebuilds/linux-x64/metadata.json +5 -5
- package/scripts/generate-readme-assets.js +79 -6
- package/scripts/prebuild.js +23 -19
- package/src/auto_bind_group_layout.js +32 -0
- package/src/bun-ffi.js +93 -12
- package/src/bun.js +23 -2
- package/src/compute.d.ts +2 -1
- package/src/compute.js +671 -33
- package/src/doe.d.ts +127 -27
- package/src/doe.js +480 -114
- package/src/full.d.ts +8 -1
- package/src/full.js +28 -3
- package/src/index.js +1013 -38
package/src/doe.js
CHANGED
|
@@ -16,46 +16,49 @@ const DOE_GPU_MAP_MODE = {
|
|
|
16
16
|
|
|
17
17
|
const DOE_BUFFER_META = new WeakMap();
|
|
18
18
|
|
|
19
|
-
function
|
|
19
|
+
function resolveBufferUsageToken(token, combined = false) {
|
|
20
20
|
switch (token) {
|
|
21
21
|
case 'upload':
|
|
22
22
|
return DOE_GPU_BUFFER_USAGE.COPY_DST;
|
|
23
23
|
case 'readback':
|
|
24
|
-
return
|
|
24
|
+
return combined
|
|
25
|
+
? DOE_GPU_BUFFER_USAGE.COPY_SRC
|
|
26
|
+
: DOE_GPU_BUFFER_USAGE.COPY_SRC | DOE_GPU_BUFFER_USAGE.COPY_DST | DOE_GPU_BUFFER_USAGE.MAP_READ;
|
|
25
27
|
case 'uniform':
|
|
26
28
|
return DOE_GPU_BUFFER_USAGE.UNIFORM | DOE_GPU_BUFFER_USAGE.COPY_DST;
|
|
27
|
-
case '
|
|
29
|
+
case 'storageRead':
|
|
28
30
|
return DOE_GPU_BUFFER_USAGE.STORAGE | DOE_GPU_BUFFER_USAGE.COPY_DST;
|
|
29
|
-
case '
|
|
31
|
+
case 'storageReadWrite':
|
|
30
32
|
return DOE_GPU_BUFFER_USAGE.STORAGE | DOE_GPU_BUFFER_USAGE.COPY_DST | DOE_GPU_BUFFER_USAGE.COPY_SRC;
|
|
31
33
|
default:
|
|
32
34
|
throw new Error(`Unknown Doe buffer usage token: ${token}`);
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
function
|
|
38
|
+
function resolveBufferUsage(usage) {
|
|
37
39
|
if (typeof usage === 'number') return usage;
|
|
38
|
-
if (typeof usage === 'string') return
|
|
40
|
+
if (typeof usage === 'string') return resolveBufferUsageToken(usage);
|
|
39
41
|
if (Array.isArray(usage)) {
|
|
40
|
-
|
|
42
|
+
const combined = usage.length > 1;
|
|
43
|
+
return usage.reduce((mask, token) => mask | resolveBufferUsageToken(token, combined), 0);
|
|
41
44
|
}
|
|
42
45
|
throw new Error('Doe buffer usage must be a number, string, or string array.');
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
function
|
|
48
|
+
function inferBindingAccessToken(token) {
|
|
46
49
|
switch (token) {
|
|
47
50
|
case 'uniform':
|
|
48
51
|
return 'uniform';
|
|
49
|
-
case '
|
|
50
|
-
return '
|
|
51
|
-
case '
|
|
52
|
-
return '
|
|
52
|
+
case 'storageRead':
|
|
53
|
+
return 'storageRead';
|
|
54
|
+
case 'storageReadWrite':
|
|
55
|
+
return 'storageReadWrite';
|
|
53
56
|
default:
|
|
54
57
|
return null;
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
function
|
|
61
|
+
function inferBindingAccess(usage) {
|
|
59
62
|
if (typeof usage === 'number' || usage == null) return null;
|
|
60
63
|
const tokens = typeof usage === 'string'
|
|
61
64
|
? [usage]
|
|
@@ -65,35 +68,38 @@ function infer_binding_access(usage) {
|
|
|
65
68
|
if (!tokens) {
|
|
66
69
|
throw new Error('Doe buffer usage must be a number, string, or string array.');
|
|
67
70
|
}
|
|
68
|
-
const inferred = [...new Set(tokens.map(
|
|
71
|
+
const inferred = [...new Set(tokens.map(inferBindingAccessToken).filter(Boolean))];
|
|
69
72
|
if (inferred.length > 1) {
|
|
70
73
|
throw new Error(`Doe buffer usage cannot imply multiple binding access modes: ${inferred.join(', ')}`);
|
|
71
74
|
}
|
|
72
75
|
return inferred[0] ?? null;
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
function
|
|
78
|
+
function rememberBufferUsage(buffer, usage) {
|
|
76
79
|
DOE_BUFFER_META.set(buffer, {
|
|
77
|
-
binding_access:
|
|
80
|
+
binding_access: inferBindingAccess(usage),
|
|
78
81
|
});
|
|
79
82
|
return buffer;
|
|
80
83
|
}
|
|
81
84
|
|
|
82
|
-
function
|
|
85
|
+
function inferredBindingAccessForBuffer(buffer) {
|
|
83
86
|
return DOE_BUFFER_META.get(buffer)?.binding_access ?? null;
|
|
84
87
|
}
|
|
85
88
|
|
|
86
|
-
function
|
|
89
|
+
function normalizeWorkgroups(workgroups) {
|
|
87
90
|
if (typeof workgroups === 'number') {
|
|
88
91
|
return [workgroups, 1, 1];
|
|
89
92
|
}
|
|
93
|
+
if (Array.isArray(workgroups) && workgroups.length === 2) {
|
|
94
|
+
return [workgroups[0], workgroups[1], 1];
|
|
95
|
+
}
|
|
90
96
|
if (Array.isArray(workgroups) && workgroups.length === 3) {
|
|
91
97
|
return workgroups;
|
|
92
98
|
}
|
|
93
|
-
throw new Error('Doe workgroups must be a number or
|
|
99
|
+
throw new Error('Doe workgroups must be a number, [x, y], or [x, y, z].');
|
|
94
100
|
}
|
|
95
101
|
|
|
96
|
-
function
|
|
102
|
+
function normalizeDataView(data) {
|
|
97
103
|
if (ArrayBuffer.isView(data)) {
|
|
98
104
|
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
99
105
|
}
|
|
@@ -103,15 +109,28 @@ function normalize_data_view(data) {
|
|
|
103
109
|
throw new Error('Doe buffer data must be an ArrayBuffer or ArrayBufferView.');
|
|
104
110
|
}
|
|
105
111
|
|
|
106
|
-
function
|
|
112
|
+
function resolveBufferSize(source) {
|
|
113
|
+
if (source && typeof source === 'object' && typeof source.size === 'number') {
|
|
114
|
+
return source.size;
|
|
115
|
+
}
|
|
116
|
+
if (ArrayBuffer.isView(source)) {
|
|
117
|
+
return source.byteLength;
|
|
118
|
+
}
|
|
119
|
+
if (source instanceof ArrayBuffer) {
|
|
120
|
+
return source.byteLength;
|
|
121
|
+
}
|
|
122
|
+
throw new Error('Doe buffer-like source must expose a byte size or be ArrayBuffer-backed data.');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeBinding(binding, index) {
|
|
107
126
|
const entry = binding && typeof binding === 'object' && 'buffer' in binding
|
|
108
127
|
? binding
|
|
109
128
|
: { buffer: binding };
|
|
110
|
-
const access = entry.access ??
|
|
129
|
+
const access = entry.access ?? inferredBindingAccessForBuffer(entry.buffer);
|
|
111
130
|
if (!access) {
|
|
112
131
|
throw new Error(
|
|
113
132
|
'Doe binding access is required for buffers without Doe helper usage metadata. ' +
|
|
114
|
-
'Pass { buffer, access } or create the buffer through doe.
|
|
133
|
+
'Pass { buffer, access } or create the buffer through doe.buffers.* with a bindable usage token.'
|
|
115
134
|
);
|
|
116
135
|
}
|
|
117
136
|
return {
|
|
@@ -121,10 +140,10 @@ function normalize_binding(binding, index) {
|
|
|
121
140
|
};
|
|
122
141
|
}
|
|
123
142
|
|
|
124
|
-
function
|
|
143
|
+
function bindGroupLayoutEntry(binding) {
|
|
125
144
|
const buffer_type = binding.access === 'uniform'
|
|
126
145
|
? 'uniform'
|
|
127
|
-
: binding.access === '
|
|
146
|
+
: binding.access === 'storageRead'
|
|
128
147
|
? 'read-only-storage'
|
|
129
148
|
: 'storage';
|
|
130
149
|
return {
|
|
@@ -134,34 +153,49 @@ function bind_group_layout_entry(binding) {
|
|
|
134
153
|
};
|
|
135
154
|
}
|
|
136
155
|
|
|
137
|
-
function
|
|
156
|
+
function bindGroupEntry(binding) {
|
|
138
157
|
return {
|
|
139
158
|
binding: binding.binding,
|
|
140
159
|
resource: { buffer: binding.buffer },
|
|
141
160
|
};
|
|
142
161
|
}
|
|
143
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Reusable compute kernel returned by `doe.compute.compile(...)`.
|
|
165
|
+
*
|
|
166
|
+
* This keeps the compiled pipeline and bind-group layout needed for repeated
|
|
167
|
+
* dispatches of the same WGSL shape.
|
|
168
|
+
*
|
|
169
|
+
* - Instances are returned through the `Doe API` surface rather than exported directly.
|
|
170
|
+
* - Dispatches still require bindings and workgroup counts for each run.
|
|
171
|
+
*/
|
|
144
172
|
class DoeKernel {
|
|
145
|
-
constructor(device, pipeline, layout,
|
|
173
|
+
constructor(device, pipeline, layout, entryPoint) {
|
|
146
174
|
this.device = device;
|
|
147
175
|
this.pipeline = pipeline;
|
|
148
176
|
this.layout = layout;
|
|
149
|
-
this.entryPoint =
|
|
177
|
+
this.entryPoint = entryPoint;
|
|
150
178
|
}
|
|
151
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Dispatch the compiled kernel once.
|
|
182
|
+
*
|
|
183
|
+
* - `workgroups` may be `number`, `[x, y]`, or `[x, y, z]`.
|
|
184
|
+
* - Bare buffers without Doe helper metadata still require `{ buffer, access }`.
|
|
185
|
+
*/
|
|
152
186
|
async dispatch(options) {
|
|
153
|
-
const bindings = (options.bindings ?? []).map(
|
|
154
|
-
const workgroups =
|
|
155
|
-
const
|
|
187
|
+
const bindings = (options.bindings ?? []).map(normalizeBinding);
|
|
188
|
+
const workgroups = normalizeWorkgroups(options.workgroups);
|
|
189
|
+
const bindGroup = this.device.createBindGroup({
|
|
156
190
|
label: options.label ?? undefined,
|
|
157
191
|
layout: this.layout,
|
|
158
|
-
entries: bindings.map(
|
|
192
|
+
entries: bindings.map(bindGroupEntry),
|
|
159
193
|
});
|
|
160
194
|
const encoder = this.device.createCommandEncoder({ label: options.label ?? undefined });
|
|
161
195
|
const pass = encoder.beginComputePass({ label: options.label ?? undefined });
|
|
162
196
|
pass.setPipeline(this.pipeline);
|
|
163
197
|
if (bindings.length > 0) {
|
|
164
|
-
pass.setBindGroup(0,
|
|
198
|
+
pass.setBindGroup(0, bindGroup);
|
|
165
199
|
}
|
|
166
200
|
pass.dispatchWorkgroups(workgroups[0], workgroups[1], workgroups[2]);
|
|
167
201
|
pass.end();
|
|
@@ -172,104 +206,436 @@ class DoeKernel {
|
|
|
172
206
|
}
|
|
173
207
|
}
|
|
174
208
|
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
device,
|
|
178
|
-
createBuffer(options) {
|
|
179
|
-
return doe.createBuffer(device, options);
|
|
180
|
-
},
|
|
181
|
-
createBufferFromData(data, options = {}) {
|
|
182
|
-
return doe.createBufferFromData(device, data, options);
|
|
183
|
-
},
|
|
184
|
-
readBuffer(buffer, type, options = {}) {
|
|
185
|
-
return doe.readBuffer(device, buffer, type, options);
|
|
186
|
-
},
|
|
187
|
-
runCompute(options) {
|
|
188
|
-
return doe.runCompute(device, options);
|
|
189
|
-
},
|
|
190
|
-
compileCompute(options) {
|
|
191
|
-
return doe.compileCompute(device, options);
|
|
192
|
-
},
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function compile_compute(device, options) {
|
|
197
|
-
const bindings = (options.bindings ?? []).map(normalize_binding);
|
|
209
|
+
function compileCompute(device, options) {
|
|
210
|
+
const bindings = (options.bindings ?? []).map(normalizeBinding);
|
|
198
211
|
const shader = device.createShaderModule({ code: options.code });
|
|
199
|
-
const
|
|
200
|
-
entries: bindings.map(
|
|
212
|
+
const bindGroupLayout = device.createBindGroupLayout({
|
|
213
|
+
entries: bindings.map(bindGroupLayoutEntry),
|
|
201
214
|
});
|
|
202
|
-
const
|
|
203
|
-
bindGroupLayouts: [
|
|
215
|
+
const pipelineLayout = device.createPipelineLayout({
|
|
216
|
+
bindGroupLayouts: [bindGroupLayout],
|
|
204
217
|
});
|
|
205
218
|
const pipeline = device.createComputePipeline({
|
|
206
|
-
layout:
|
|
219
|
+
layout: pipelineLayout,
|
|
207
220
|
compute: {
|
|
208
221
|
module: shader,
|
|
209
222
|
entryPoint: options.entryPoint ?? 'main',
|
|
210
223
|
},
|
|
211
224
|
});
|
|
212
|
-
return new DoeKernel(device, pipeline,
|
|
225
|
+
return new DoeKernel(device, pipeline, bindGroupLayout, options.entryPoint ?? 'main');
|
|
213
226
|
}
|
|
214
227
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
228
|
+
function createBuffer(device, options) {
|
|
229
|
+
return rememberBufferUsage(device.createBuffer({
|
|
230
|
+
label: options.label ?? undefined,
|
|
231
|
+
size: options.size,
|
|
232
|
+
usage: resolveBufferUsage(options.usage),
|
|
233
|
+
mappedAtCreation: options.mappedAtCreation ?? false,
|
|
234
|
+
}), options.usage);
|
|
235
|
+
}
|
|
219
236
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
},
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
size,
|
|
247
|
-
usage: DOE_GPU_BUFFER_USAGE.COPY_DST | DOE_GPU_BUFFER_USAGE.MAP_READ,
|
|
248
|
-
});
|
|
249
|
-
const encoder = device.createCommandEncoder({ label: options.label ?? undefined });
|
|
250
|
-
encoder.copyBufferToBuffer(buffer, offset, staging, 0, size);
|
|
251
|
-
device.queue.submit([encoder.finish()]);
|
|
252
|
-
await staging.mapAsync(DOE_GPU_MAP_MODE.READ);
|
|
253
|
-
const copy = staging.getMappedRange().slice(0);
|
|
254
|
-
staging.unmap();
|
|
255
|
-
if (typeof staging.destroy === 'function') {
|
|
256
|
-
staging.destroy();
|
|
257
|
-
}
|
|
237
|
+
function createBufferFromData(device, data, options = {}) {
|
|
238
|
+
const view = normalizeDataView(data);
|
|
239
|
+
const usage = options.usage ?? 'storageRead';
|
|
240
|
+
const buffer = rememberBufferUsage(device.createBuffer({
|
|
241
|
+
label: options.label ?? undefined,
|
|
242
|
+
size: view.byteLength,
|
|
243
|
+
usage: resolveBufferUsage(usage),
|
|
244
|
+
}), usage);
|
|
245
|
+
device.queue.writeBuffer(buffer, 0, view);
|
|
246
|
+
return buffer;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function createBufferLike(device, source, options = {}) {
|
|
250
|
+
return createBuffer(device, {
|
|
251
|
+
...options,
|
|
252
|
+
size: options.size ?? resolveBufferSize(source),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function readBuffer(device, buffer, type, options = {}) {
|
|
257
|
+
const offset = options.offset ?? 0;
|
|
258
|
+
const size = options.size ?? Math.max(0, (buffer.size ?? 0) - offset);
|
|
259
|
+
if (((buffer.usage ?? 0) & DOE_GPU_BUFFER_USAGE.MAP_READ) !== 0) {
|
|
260
|
+
await buffer.mapAsync(DOE_GPU_MAP_MODE.READ, offset, size);
|
|
261
|
+
const copy = buffer.getMappedRange(offset, size).slice(0);
|
|
262
|
+
buffer.unmap();
|
|
258
263
|
return new type(copy);
|
|
259
|
-
}
|
|
264
|
+
}
|
|
265
|
+
const staging = device.createBuffer({
|
|
266
|
+
label: options.label ?? undefined,
|
|
267
|
+
size,
|
|
268
|
+
usage: DOE_GPU_BUFFER_USAGE.COPY_DST | DOE_GPU_BUFFER_USAGE.MAP_READ,
|
|
269
|
+
});
|
|
270
|
+
const encoder = device.createCommandEncoder({ label: options.label ?? undefined });
|
|
271
|
+
encoder.copyBufferToBuffer(buffer, offset, staging, 0, size);
|
|
272
|
+
device.queue.submit([encoder.finish()]);
|
|
273
|
+
await staging.mapAsync(DOE_GPU_MAP_MODE.READ);
|
|
274
|
+
const copy = staging.getMappedRange().slice(0);
|
|
275
|
+
staging.unmap();
|
|
276
|
+
if (typeof staging.destroy === 'function') {
|
|
277
|
+
staging.destroy();
|
|
278
|
+
}
|
|
279
|
+
return new type(copy);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function runCompute(device, options) {
|
|
283
|
+
const kernel = compileCompute(device, options);
|
|
284
|
+
await kernel.dispatch({
|
|
285
|
+
bindings: options.bindings ?? [],
|
|
286
|
+
workgroups: options.workgroups,
|
|
287
|
+
label: options.label,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function assertLayer3Usage(usage, path) {
|
|
292
|
+
if (typeof usage === 'number') {
|
|
293
|
+
throw new Error(`Doe ${path} does not accept raw numeric usage flags. Use Doe usage tokens on compute.once(...) or drop to gpu.buffers.*.`);
|
|
294
|
+
}
|
|
295
|
+
if (Array.isArray(usage) && usage.some((token) => typeof token === 'number')) {
|
|
296
|
+
throw new Error(`Doe ${path} does not accept raw numeric usage flags. Use Doe usage tokens on compute.once(...) or drop to gpu.buffers.*.`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function normalizeOnceInput(device, input, index) {
|
|
301
|
+
if (ArrayBuffer.isView(input) || input instanceof ArrayBuffer) {
|
|
302
|
+
const buffer = createBufferFromData(device, input, {});
|
|
303
|
+
return {
|
|
304
|
+
binding: buffer,
|
|
305
|
+
buffer,
|
|
306
|
+
byte_length: resolveBufferSize(input),
|
|
307
|
+
owned: true,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (input && typeof input === 'object' && 'data' in input) {
|
|
312
|
+
assertLayer3Usage(input.usage, `compute.once input ${index} usage`);
|
|
313
|
+
const buffer = createBufferFromData(device, input.data, {
|
|
314
|
+
usage: input.usage ?? 'storageRead',
|
|
315
|
+
label: input.label,
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
binding: input.access ? { buffer, access: input.access } : buffer,
|
|
319
|
+
buffer,
|
|
320
|
+
byte_length: resolveBufferSize(input.data),
|
|
321
|
+
owned: true,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (input && typeof input === 'object' && 'buffer' in input) {
|
|
326
|
+
return {
|
|
327
|
+
binding: input,
|
|
328
|
+
buffer: input.buffer,
|
|
329
|
+
byte_length: resolveBufferSize(input.buffer),
|
|
330
|
+
owned: false,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (input && typeof input === 'object' && typeof input.size === 'number') {
|
|
335
|
+
return {
|
|
336
|
+
binding: input,
|
|
337
|
+
buffer: input,
|
|
338
|
+
byte_length: input.size,
|
|
339
|
+
owned: false,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
throw new Error(`Doe compute.once input ${index} must be data, a Doe input spec, or a buffer.`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function normalizeOnceOutput(device, output, inputs) {
|
|
347
|
+
if (!output || typeof output !== 'object') {
|
|
348
|
+
throw new Error('Doe compute.once output is required.');
|
|
349
|
+
}
|
|
350
|
+
if (typeof output.type !== 'function') {
|
|
351
|
+
throw new Error('Doe compute.once output.type must be a typed-array constructor.');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const fallback_input_index = inputs.length > 0 ? 0 : null;
|
|
355
|
+
const like_input_index = output.likeInput ?? fallback_input_index;
|
|
356
|
+
const size = output.size ?? (
|
|
357
|
+
like_input_index != null && inputs[like_input_index]
|
|
358
|
+
? inputs[like_input_index].byte_length
|
|
359
|
+
: null
|
|
360
|
+
);
|
|
260
361
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
362
|
+
if (!(size > 0)) {
|
|
363
|
+
throw new Error('Doe compute.once output size must be provided or derived from likeInput.');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
assertLayer3Usage(output.usage, 'compute.once output usage');
|
|
367
|
+
const buffer = createBuffer(device, {
|
|
368
|
+
size,
|
|
369
|
+
usage: output.usage ?? 'storageReadWrite',
|
|
370
|
+
label: output.label,
|
|
371
|
+
});
|
|
372
|
+
return {
|
|
373
|
+
binding: output.access ? { buffer, access: output.access } : buffer,
|
|
374
|
+
buffer,
|
|
375
|
+
type: output.type,
|
|
376
|
+
read_options: output.read ?? {},
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function computeOnce(device, options) {
|
|
381
|
+
const inputs = (options.inputs ?? []).map((input, index) => normalizeOnceInput(device, input, index));
|
|
382
|
+
const output = normalizeOnceOutput(device, options.output, inputs);
|
|
383
|
+
try {
|
|
384
|
+
await runCompute(device, {
|
|
385
|
+
code: options.code,
|
|
386
|
+
entryPoint: options.entryPoint,
|
|
387
|
+
bindings: [...inputs.map((input) => input.binding), output.binding],
|
|
265
388
|
workgroups: options.workgroups,
|
|
266
389
|
label: options.label,
|
|
267
390
|
});
|
|
268
|
-
|
|
391
|
+
return await readBuffer(device, output.buffer, output.type, output.read_options);
|
|
392
|
+
} finally {
|
|
393
|
+
if (typeof output.buffer.destroy === 'function') {
|
|
394
|
+
output.buffer.destroy();
|
|
395
|
+
}
|
|
396
|
+
for (const input of inputs) {
|
|
397
|
+
if (input.owned && typeof input.buffer.destroy === 'function') {
|
|
398
|
+
input.buffer.destroy();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
269
403
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
404
|
+
function createBoundDoe(device) {
|
|
405
|
+
return {
|
|
406
|
+
device,
|
|
407
|
+
buffers: {
|
|
408
|
+
/**
|
|
409
|
+
* Create a buffer with explicit size and Doe usage tokens.
|
|
410
|
+
*
|
|
411
|
+
* This is part of the `Doe API` surface over `device.createBuffer(...)`.
|
|
412
|
+
* It accepts Doe usage tokens and remembers bindability metadata for
|
|
413
|
+
* later Doe API calls.
|
|
414
|
+
*
|
|
415
|
+
* - Raw numeric usage flags are allowed here for explicit control.
|
|
416
|
+
* - If you later pass a raw-usage buffer to `compute.run(...)`, you may still need `{ buffer, access }` because Doe can only infer access from Doe usage tokens, not arbitrary bitmasks.
|
|
417
|
+
*/
|
|
418
|
+
create(options) {
|
|
419
|
+
return createBuffer(device, options);
|
|
420
|
+
},
|
|
421
|
+
/**
|
|
422
|
+
* Create a buffer from typed-array or ArrayBuffer data and upload it immediately.
|
|
423
|
+
*
|
|
424
|
+
* This allocates a buffer, writes the provided data into it, and
|
|
425
|
+
* remembers Doe usage metadata for later helper inference.
|
|
426
|
+
*
|
|
427
|
+
* - Defaults to `storageRead` usage when none is provided.
|
|
428
|
+
* - Raw numeric usage flags are allowed, but that may disable later access inference if the bitmask does not map cleanly to one Doe access mode.
|
|
429
|
+
*/
|
|
430
|
+
fromData(data, options = {}) {
|
|
431
|
+
return createBufferFromData(device, data, options);
|
|
432
|
+
},
|
|
433
|
+
/**
|
|
434
|
+
* Create a buffer whose size is derived from another buffer or typed-array source.
|
|
435
|
+
*
|
|
436
|
+
* This copies the byte size from `source` unless an explicit
|
|
437
|
+
* `options.size` is provided, which removes common `size: src.size`
|
|
438
|
+
* boilerplate.
|
|
439
|
+
*
|
|
440
|
+
* - `source` may be a Doe buffer, a raw buffer exposing `.size`, a typed array, or an `ArrayBuffer`.
|
|
441
|
+
* - If the source has no byte size, this throws instead of guessing.
|
|
442
|
+
*/
|
|
443
|
+
like(source, options = {}) {
|
|
444
|
+
return createBufferLike(device, source, options);
|
|
445
|
+
},
|
|
446
|
+
/**
|
|
447
|
+
* Read a buffer back into a typed array.
|
|
448
|
+
*
|
|
449
|
+
* This copies the source buffer into a staging buffer, maps it for read,
|
|
450
|
+
* and returns a new typed array instance created from the copied bytes.
|
|
451
|
+
*
|
|
452
|
+
* - `options.offset` and `options.size` let you read a subrange.
|
|
453
|
+
* - The returned typed array constructor must accept a plain `ArrayBuffer`.
|
|
454
|
+
*/
|
|
455
|
+
read(buffer, type, options = {}) {
|
|
456
|
+
return readBuffer(device, buffer, type, options);
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
compute: {
|
|
460
|
+
/**
|
|
461
|
+
* Compile and dispatch a one-off compute job.
|
|
462
|
+
*
|
|
463
|
+
* This builds a compute pipeline for the provided WGSL, dispatches it
|
|
464
|
+
* once with the supplied bindings and workgroups, and waits for submitted
|
|
465
|
+
* work to finish as part of the explicit `Doe API` surface.
|
|
466
|
+
*
|
|
467
|
+
* - `workgroups` may be `number`, `[x, y]`, or `[x, y, z]`.
|
|
468
|
+
* - Bare buffers without Doe helper metadata require `{ buffer, access }`.
|
|
469
|
+
* - This recompiles per call; use `compute.compile(...)` when reusing the kernel.
|
|
470
|
+
*/
|
|
471
|
+
run(options) {
|
|
472
|
+
return runCompute(device, options);
|
|
473
|
+
},
|
|
474
|
+
/**
|
|
475
|
+
* Compile a reusable compute kernel.
|
|
476
|
+
*
|
|
477
|
+
* This creates the shader, bind-group layout, and compute pipeline once
|
|
478
|
+
* and returns a kernel object with `.dispatch(...)`.
|
|
479
|
+
*
|
|
480
|
+
* - Binding access is inferred from the bindings passed at compile time.
|
|
481
|
+
* - Reuse this path when you are dispatching the same WGSL shape repeatedly.
|
|
482
|
+
*/
|
|
483
|
+
compile(options) {
|
|
484
|
+
return compileCompute(device, options);
|
|
485
|
+
},
|
|
486
|
+
/**
|
|
487
|
+
* Run a narrow Doe routines typed-array workflow.
|
|
488
|
+
*
|
|
489
|
+
* This is the first `Doe routines` path. It accepts typed-array or Doe input specs, allocates temporary
|
|
490
|
+
* buffers, dispatches the compute job once, reads the output back, and
|
|
491
|
+
* returns the requested typed array result.
|
|
492
|
+
*
|
|
493
|
+
* - This is intentionally opinionated: it rejects raw numeric WebGPU usage flags and expects Doe usage tokens when usage is specified.
|
|
494
|
+
* - Output size defaults from `likeInput` or the first input when possible; if no size can be derived, it throws instead of guessing.
|
|
495
|
+
* - Temporary buffers created internally are destroyed before the call returns.
|
|
496
|
+
*/
|
|
497
|
+
once(options) {
|
|
498
|
+
return computeOnce(device, options);
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Build the shared Doe API / Doe routines namespace for a package surface.
|
|
506
|
+
*
|
|
507
|
+
* This creates the public `doe` object used by both `@simulatte/webgpu` and
|
|
508
|
+
* `@simulatte/webgpu/compute` for the JS convenience surface layered over the
|
|
509
|
+
* Doe runtime.
|
|
510
|
+
*
|
|
511
|
+
* - If no `requestDevice` implementation is supplied, `doe.requestDevice()` throws, but `doe.bind(device)` and the static helper groups still work.
|
|
512
|
+
* - Both package surfaces share this helper shape; only the underlying raw device contract differs.
|
|
513
|
+
*/
|
|
514
|
+
export function createDoeNamespace({ requestDevice } = {}) {
|
|
515
|
+
return {
|
|
516
|
+
/**
|
|
517
|
+
* Request a device and return the bound Doe API helper object in one step.
|
|
518
|
+
*
|
|
519
|
+
* This calls the package-local `requestDevice(...)` implementation, then
|
|
520
|
+
* wraps the resulting device into the `Doe API` surface.
|
|
521
|
+
*
|
|
522
|
+
* - Throws if this Doe namespace was created without a `requestDevice` implementation.
|
|
523
|
+
* - The returned `gpu.device` is full-surface or compute-only depending on which package created the namespace.
|
|
524
|
+
*/
|
|
525
|
+
async requestDevice(options = {}) {
|
|
526
|
+
if (typeof requestDevice !== 'function') {
|
|
527
|
+
throw new Error('Doe requestDevice() is unavailable in this context.');
|
|
528
|
+
}
|
|
529
|
+
return createBoundDoe(await requestDevice(options));
|
|
530
|
+
},
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Wrap an existing device in the Doe API surface.
|
|
534
|
+
*
|
|
535
|
+
* This turns a previously requested device into the same bound helper
|
|
536
|
+
* object returned by `await doe.requestDevice()`.
|
|
537
|
+
*
|
|
538
|
+
* - Use this when you need the raw device first for non-helper setup.
|
|
539
|
+
* - No async work happens here; it only wraps the device you already have.
|
|
540
|
+
*/
|
|
541
|
+
bind(device) {
|
|
542
|
+
return createBoundDoe(device);
|
|
543
|
+
},
|
|
544
|
+
|
|
545
|
+
buffers: {
|
|
546
|
+
/**
|
|
547
|
+
* Static Doe API buffer creation call for an explicit device.
|
|
548
|
+
*
|
|
549
|
+
* This lets callers use the Doe API buffer surface without first binding
|
|
550
|
+
* a device into a `gpu` helper object.
|
|
551
|
+
*
|
|
552
|
+
* - This is the unbound form of `gpu.buffers.create(...)`.
|
|
553
|
+
*/
|
|
554
|
+
create(device, options) {
|
|
555
|
+
return createBuffer(device, options);
|
|
556
|
+
},
|
|
557
|
+
/**
|
|
558
|
+
* Static data-upload helper for an explicit device.
|
|
559
|
+
*
|
|
560
|
+
* This provides the unbound form of the same upload flow exposed on
|
|
561
|
+
* `gpu.buffers.fromData(...)`.
|
|
562
|
+
*
|
|
563
|
+
* - This is the unbound form of `gpu.buffers.fromData(...)`.
|
|
564
|
+
*/
|
|
565
|
+
fromData(device, data, options = {}) {
|
|
566
|
+
return createBufferFromData(device, data, options);
|
|
567
|
+
},
|
|
568
|
+
/**
|
|
569
|
+
* Static size-copy helper for an explicit device.
|
|
570
|
+
*
|
|
571
|
+
* This keeps the `createBufferLike` convenience available when callers
|
|
572
|
+
* are working with a raw device rather than a bound helper object.
|
|
573
|
+
*
|
|
574
|
+
* - This is the unbound form of `gpu.buffers.like(...)`.
|
|
575
|
+
*/
|
|
576
|
+
like(device, source, options = {}) {
|
|
577
|
+
return createBufferLike(device, source, options);
|
|
578
|
+
},
|
|
579
|
+
/**
|
|
580
|
+
* Static readback helper for an explicit device.
|
|
581
|
+
*
|
|
582
|
+
* This exposes the same staging-copy readback path as
|
|
583
|
+
* `gpu.buffers.read(...)` without requiring a bound helper.
|
|
584
|
+
*
|
|
585
|
+
* - This is the unbound form of `gpu.buffers.read(...)`.
|
|
586
|
+
*/
|
|
587
|
+
read(device, buffer, type, options = {}) {
|
|
588
|
+
return readBuffer(device, buffer, type, options);
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
|
|
592
|
+
compute: {
|
|
593
|
+
/**
|
|
594
|
+
* Static compute dispatch helper for an explicit device.
|
|
595
|
+
*
|
|
596
|
+
* This gives raw-device callers the same one-off compute dispatch helper
|
|
597
|
+
* that bound helpers expose on `gpu.compute.run(...)`.
|
|
598
|
+
*
|
|
599
|
+
* - This is the unbound form of `gpu.compute.run(...)`.
|
|
600
|
+
*/
|
|
601
|
+
run(device, options) {
|
|
602
|
+
return runCompute(device, options);
|
|
603
|
+
},
|
|
604
|
+
/**
|
|
605
|
+
* Static reusable-kernel compiler for an explicit device.
|
|
606
|
+
*
|
|
607
|
+
* This exposes the reusable kernel path without requiring a previously
|
|
608
|
+
* bound `gpu` helper object.
|
|
609
|
+
*
|
|
610
|
+
* - This is the unbound form of `gpu.compute.compile(...)`.
|
|
611
|
+
*/
|
|
612
|
+
compile(device, options) {
|
|
613
|
+
return compileCompute(device, options);
|
|
614
|
+
},
|
|
615
|
+
/**
|
|
616
|
+
* Static Doe routines typed-array compute call for an explicit device.
|
|
617
|
+
*
|
|
618
|
+
* This keeps the narrow `Doe routines` `compute.once(...)` workflow available to
|
|
619
|
+
* callers that are still holding a raw device.
|
|
620
|
+
*
|
|
621
|
+
* - This is the unbound form of `gpu.compute.once(...)`.
|
|
622
|
+
*/
|
|
623
|
+
once(device, options) {
|
|
624
|
+
return computeOnce(device, options);
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Unbound Doe API / Doe routines namespace without a package-local `requestDevice(...)`.
|
|
632
|
+
*
|
|
633
|
+
* This export is primarily for internal composition and advanced consumers who
|
|
634
|
+
* want the shared Doe API and Doe routines groups without choosing the full or compute package entry.
|
|
635
|
+
*
|
|
636
|
+
* - `doe.requestDevice()` throws here because no package-local request function is attached.
|
|
637
|
+
* - Most package consumers should prefer the `doe` export from `@simulatte/webgpu` or `@simulatte/webgpu/compute`.
|
|
638
|
+
*/
|
|
639
|
+
export const doe = createDoeNamespace();
|
|
274
640
|
|
|
275
641
|
export default doe;
|