@simulatte/webgpu-doe 0.1.2 → 0.3.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/src/index.js CHANGED
@@ -1,495 +1,731 @@
1
- import { createRequire } from 'node:module';
2
- import { existsSync } from 'node:fs';
3
- import { resolve, dirname } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
1
+ const DOE_GPU_BUFFER_USAGE = {
2
+ MAP_READ: 0x0001,
3
+ COPY_SRC: 0x0004,
4
+ COPY_DST: 0x0008,
5
+ UNIFORM: 0x0040,
6
+ STORAGE: 0x0080,
7
+ };
5
8
 
6
- const __dirname = dirname(fileURLToPath(import.meta.url));
7
- const require = createRequire(import.meta.url);
9
+ const DOE_GPU_SHADER_STAGE = {
10
+ COMPUTE: 0x4,
11
+ };
8
12
 
9
- const addon = loadAddon();
10
- const DOE_LIB_PATH = resolveDoeLibraryPath();
11
- let libraryLoaded = false;
13
+ const DOE_GPU_MAP_MODE = {
14
+ READ: 0x0001,
15
+ };
12
16
 
13
- function loadAddon() {
14
- const prebuildPath = resolve(__dirname, '..', 'prebuilds', `${process.platform}-${process.arch}`, 'doe_napi.node');
15
- try {
16
- return require(prebuildPath);
17
- } catch {
18
- try {
19
- return require('../build/Release/doe_napi.node');
20
- } catch {
21
- try {
22
- return require('../build/Debug/doe_napi.node');
23
- } catch {
24
- return null;
25
- }
26
- }
17
+ const DOE_BUFFER_META = new WeakMap();
18
+
19
+ function resolveBufferUsageToken(token, combined = false) {
20
+ switch (token) {
21
+ case 'upload':
22
+ return DOE_GPU_BUFFER_USAGE.COPY_DST;
23
+ case 'readback':
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;
27
+ case 'uniform':
28
+ return DOE_GPU_BUFFER_USAGE.UNIFORM | DOE_GPU_BUFFER_USAGE.COPY_DST;
29
+ case 'storageRead':
30
+ return DOE_GPU_BUFFER_USAGE.STORAGE | DOE_GPU_BUFFER_USAGE.COPY_DST;
31
+ case 'storageReadWrite':
32
+ return DOE_GPU_BUFFER_USAGE.STORAGE | DOE_GPU_BUFFER_USAGE.COPY_DST | DOE_GPU_BUFFER_USAGE.COPY_SRC;
33
+ default:
34
+ throw new Error(`Unknown Doe buffer usage token: ${token}`);
27
35
  }
28
36
  }
29
37
 
30
- function resolveDoeLibraryPath() {
31
- const ext = process.platform === 'darwin' ? 'dylib'
32
- : process.platform === 'win32' ? 'dll' : 'so';
33
-
34
- const candidates = [
35
- process.env.DOE_WEBGPU_LIB,
36
- resolve(__dirname, '..', 'prebuilds', `${process.platform}-${process.arch}`, `libdoe_webgpu.${ext}`),
37
- resolve(__dirname, '..', '..', '..', 'zig', 'zig-out', 'lib', `libdoe_webgpu.${ext}`),
38
- resolve(process.cwd(), 'zig', 'zig-out', 'lib', `libdoe_webgpu.${ext}`),
39
- ];
40
-
41
- for (const candidate of candidates) {
42
- if (candidate && existsSync(candidate)) return candidate;
43
- }
44
- return null;
38
+ function resolveBufferUsage(usage) {
39
+ if (typeof usage === 'number') return usage;
40
+ if (typeof usage === 'string') return resolveBufferUsageToken(usage);
41
+ if (Array.isArray(usage)) {
42
+ const combined = usage.length > 1;
43
+ return usage.reduce((mask, token) => mask | (
44
+ typeof token === 'number'
45
+ ? token
46
+ : resolveBufferUsageToken(token, combined)
47
+ ), 0);
48
+ }
49
+ throw new Error('Doe buffer usage must be a number, string, or string array.');
45
50
  }
46
51
 
47
- function ensureLibrary() {
48
- if (libraryLoaded) return;
49
- if (!addon) {
50
- throw new Error(
51
- '@simulatte/webgpu-doe: Native addon not found. Run `npm run build` or `npx node-gyp rebuild`.'
52
- );
52
+ function inferBindingAccessToken(token) {
53
+ switch (token) {
54
+ case 'uniform':
55
+ return 'uniform';
56
+ case 'storageRead':
57
+ return 'storageRead';
58
+ case 'storageReadWrite':
59
+ return 'storageReadWrite';
60
+ default:
61
+ return null;
53
62
  }
54
- if (!DOE_LIB_PATH) {
55
- throw new Error(
56
- '@simulatte/webgpu-doe: libdoe_webgpu not found. Build it with `cd fawn/zig && zig build dropin` or set DOE_WEBGPU_LIB.'
57
- );
58
- }
59
- addon.loadLibrary(DOE_LIB_PATH);
60
- libraryLoaded = true;
61
63
  }
62
64
 
63
- // WebGPU enum constants (standard values).
64
- export const globals = {
65
- GPUBufferUsage: {
66
- MAP_READ: 0x0001,
67
- MAP_WRITE: 0x0002,
68
- COPY_SRC: 0x0004,
69
- COPY_DST: 0x0008,
70
- INDEX: 0x0010,
71
- VERTEX: 0x0020,
72
- UNIFORM: 0x0040,
73
- STORAGE: 0x0080,
74
- INDIRECT: 0x0100,
75
- QUERY_RESOLVE: 0x0200,
76
- },
77
- GPUShaderStage: {
78
- VERTEX: 0x1,
79
- FRAGMENT: 0x2,
80
- COMPUTE: 0x4,
81
- },
82
- GPUMapMode: {
83
- READ: 0x0001,
84
- WRITE: 0x0002,
85
- },
86
- GPUTextureUsage: {
87
- COPY_SRC: 0x01,
88
- COPY_DST: 0x02,
89
- TEXTURE_BINDING: 0x04,
90
- STORAGE_BINDING: 0x08,
91
- RENDER_ATTACHMENT: 0x10,
92
- },
93
- };
65
+ function inferBindingAccess(usage) {
66
+ if (typeof usage === 'number' || usage == null) return null;
67
+ const tokens = typeof usage === 'string'
68
+ ? [usage]
69
+ : Array.isArray(usage)
70
+ ? usage.filter((token) => typeof token !== 'number')
71
+ : null;
72
+ if (!tokens) {
73
+ throw new Error('Doe buffer usage must be a number, string, or string array.');
74
+ }
75
+ const inferred = [...new Set(tokens.map(inferBindingAccessToken).filter(Boolean))];
76
+ if (inferred.length > 1) {
77
+ throw new Error(`Doe buffer usage cannot imply multiple binding access modes: ${inferred.join(', ')}`);
78
+ }
79
+ return inferred[0] ?? null;
80
+ }
94
81
 
95
- class DoeGPUBuffer {
96
- constructor(native, instance, size, usage, queue) {
97
- this._native = native;
98
- this._instance = instance;
99
- this._queue = queue;
100
- this.size = size;
101
- this.usage = usage;
102
- }
82
+ function rememberBufferUsage(buffer, usage) {
83
+ DOE_BUFFER_META.set(buffer, {
84
+ bindingAccess: inferBindingAccess(usage),
85
+ });
86
+ return buffer;
87
+ }
103
88
 
104
- async mapAsync(mode, offset = 0, size = this.size) {
105
- if (this._queue) addon.queueFlush(this._queue);
106
- addon.bufferMapSync(this._instance, this._native, mode, offset, size);
107
- }
89
+ function inferredBindingAccessForBuffer(buffer) {
90
+ return DOE_BUFFER_META.get(buffer)?.bindingAccess ?? null;
91
+ }
108
92
 
109
- getMappedRange(offset = 0, size = this.size) {
110
- return addon.bufferGetMappedRange(this._native, offset, size);
93
+ function normalizeWorkgroups(workgroups) {
94
+ if (typeof workgroups === 'number') {
95
+ return [workgroups, 1, 1];
111
96
  }
112
-
113
- unmap() {
114
- addon.bufferUnmap(this._native);
97
+ if (Array.isArray(workgroups) && workgroups.length === 2) {
98
+ return [workgroups[0], workgroups[1], 1];
115
99
  }
116
-
117
- destroy() {
118
- addon.bufferRelease(this._native);
119
- this._native = null;
100
+ if (Array.isArray(workgroups) && workgroups.length === 3) {
101
+ return workgroups;
120
102
  }
103
+ throw new Error('Doe workgroups must be a number, [x, y], or [x, y, z].');
121
104
  }
122
105
 
123
- class DoeGPUComputePassEncoder {
124
- constructor(native) { this._native = native; }
125
-
126
- setPipeline(pipeline) {
127
- addon.computePassSetPipeline(this._native, pipeline._native);
128
- }
129
-
130
- setBindGroup(index, bindGroup) {
131
- addon.computePassSetBindGroup(this._native, index, bindGroup._native);
132
- }
133
-
134
- dispatchWorkgroups(x, y = 1, z = 1) {
135
- addon.computePassDispatchWorkgroups(this._native, x, y, z);
106
+ function validatePositiveInteger(value, label) {
107
+ if (!Number.isInteger(value) || value < 1) {
108
+ throw new Error(`${label} must be a positive integer.`);
136
109
  }
110
+ }
137
111
 
138
- dispatchWorkgroupsIndirect(indirectBuffer, indirectOffset = 0) {
139
- addon.computePassDispatchWorkgroupsIndirect(this._native, indirectBuffer._native, indirectOffset);
112
+ function validateWorkgroups(device, workgroups) {
113
+ const normalized = normalizeWorkgroups(workgroups);
114
+ const limits = device?.limits ?? {};
115
+ const [x, y, z] = normalized;
116
+
117
+ validatePositiveInteger(x, 'Doe workgroups.x');
118
+ validatePositiveInteger(y, 'Doe workgroups.y');
119
+ validatePositiveInteger(z, 'Doe workgroups.z');
120
+
121
+ if (limits.maxComputeWorkgroupsPerDimension) {
122
+ if (x > limits.maxComputeWorkgroupsPerDimension ||
123
+ y > limits.maxComputeWorkgroupsPerDimension ||
124
+ z > limits.maxComputeWorkgroupsPerDimension) {
125
+ throw new Error(
126
+ `Doe workgroups exceed maxComputeWorkgroupsPerDimension (${limits.maxComputeWorkgroupsPerDimension}).`
127
+ );
128
+ }
140
129
  }
141
-
142
- end() {
143
- addon.computePassEnd(this._native);
130
+ if (limits.maxComputeWorkgroupSizeX && x > limits.maxComputeWorkgroupSizeX) {
131
+ throw new Error(
132
+ `Doe workgroups.x (${x}) exceeds maxComputeWorkgroupSizeX (${limits.maxComputeWorkgroupSizeX}).`
133
+ );
144
134
  }
145
- }
146
-
147
- class DoeGPUCommandEncoder {
148
- constructor(native) { this._native = native; }
149
-
150
- beginComputePass(descriptor) {
151
- const pass = addon.beginComputePass(this._native);
152
- return new DoeGPUComputePassEncoder(pass);
135
+ if (limits.maxComputeWorkgroupSizeY && y > limits.maxComputeWorkgroupSizeY) {
136
+ throw new Error(
137
+ `Doe workgroups.y (${y}) exceeds maxComputeWorkgroupSizeY (${limits.maxComputeWorkgroupSizeY}).`
138
+ );
153
139
  }
154
-
155
- beginRenderPass(descriptor) {
156
- const colorAttachments = (descriptor.colorAttachments || []).map((a) => ({
157
- view: a.view._native,
158
- clearValue: a.clearValue || { r: 0, g: 0, b: 0, a: 1 },
159
- }));
160
- const pass = addon.beginRenderPass(this._native, colorAttachments);
161
- return new DoeGPURenderPassEncoder(pass);
140
+ if (limits.maxComputeWorkgroupSizeZ && z > limits.maxComputeWorkgroupSizeZ) {
141
+ throw new Error(
142
+ `Doe workgroups.z (${z}) exceeds maxComputeWorkgroupSizeZ (${limits.maxComputeWorkgroupSizeZ}).`
143
+ );
162
144
  }
163
-
164
- copyBufferToBuffer(src, srcOffset, dst, dstOffset, size) {
165
- addon.commandEncoderCopyBufferToBuffer(
166
- this._native, src._native, srcOffset, dst._native, dstOffset, size);
145
+ if (limits.maxComputeInvocationsPerWorkgroup) {
146
+ const invocations = x * y * z;
147
+ if (invocations > limits.maxComputeInvocationsPerWorkgroup) {
148
+ throw new Error(
149
+ `Doe workgroups (${invocations} invocations) exceed maxComputeInvocationsPerWorkgroup (${limits.maxComputeInvocationsPerWorkgroup}).`
150
+ );
151
+ }
167
152
  }
168
153
 
169
- finish() {
170
- const cmd = addon.commandEncoderFinish(this._native);
171
- return { _native: cmd };
172
- }
154
+ return normalized;
173
155
  }
174
156
 
175
- class DoeGPUQueue {
176
- constructor(native) { this._native = native; }
177
-
178
- submit(commandBuffers) {
179
- const natives = commandBuffers.map((c) => c._native);
180
- addon.queueSubmit(this._native, natives);
181
- }
182
-
183
- writeBuffer(buffer, bufferOffset, data, dataOffset = 0, size) {
184
- let view = data;
185
- if (dataOffset > 0 || size !== undefined) {
186
- const byteOffset = data.byteOffset + dataOffset * (data.BYTES_PER_ELEMENT || 1);
187
- const byteLength = size !== undefined
188
- ? size * (data.BYTES_PER_ELEMENT || 1)
189
- : data.byteLength - dataOffset * (data.BYTES_PER_ELEMENT || 1);
190
- view = new Uint8Array(data.buffer, byteOffset, byteLength);
191
- }
192
- addon.queueWriteBuffer(this._native, buffer._native, bufferOffset, view);
157
+ function normalizeDataView(data) {
158
+ if (ArrayBuffer.isView(data)) {
159
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
193
160
  }
194
-
195
- async onSubmittedWorkDone() {
196
- addon.queueFlush(this._native);
161
+ if (data instanceof ArrayBuffer) {
162
+ return new Uint8Array(data);
197
163
  }
164
+ throw new Error('Doe buffer data must be an ArrayBuffer or ArrayBufferView.');
198
165
  }
199
166
 
200
- class DoeGPURenderPassEncoder {
201
- constructor(native) { this._native = native; }
202
-
203
- setPipeline(pipeline) {
204
- addon.renderPassSetPipeline(this._native, pipeline._native);
167
+ function resolveBufferSize(source) {
168
+ if (source && typeof source === 'object' && typeof source.size === 'number') {
169
+ return source.size;
205
170
  }
206
-
207
- draw(vertexCount, instanceCount = 1, firstVertex = 0, firstInstance = 0) {
208
- addon.renderPassDraw(this._native, vertexCount, instanceCount, firstVertex, firstInstance);
171
+ if (ArrayBuffer.isView(source)) {
172
+ return source.byteLength;
209
173
  }
210
-
211
- end() {
212
- addon.renderPassEnd(this._native);
174
+ if (source instanceof ArrayBuffer) {
175
+ return source.byteLength;
213
176
  }
177
+ throw new Error('Doe buffer-like source must expose a byte size or be ArrayBuffer-backed data.');
214
178
  }
215
179
 
216
- class DoeGPUTexture {
217
- constructor(native) { this._native = native; }
218
-
219
- createView(descriptor) {
220
- const view = addon.textureCreateView(this._native);
221
- return new DoeGPUTextureView(view);
222
- }
223
-
224
- destroy() {
225
- addon.textureRelease(this._native);
226
- this._native = null;
180
+ function normalizeBinding(binding, index) {
181
+ const entry = binding && typeof binding === 'object' && 'buffer' in binding
182
+ ? binding
183
+ : { buffer: binding };
184
+ const access = entry.access ?? inferredBindingAccessForBuffer(entry.buffer);
185
+ if (!access) {
186
+ throw new Error(
187
+ 'Doe binding access is required for buffers without Doe helper usage metadata. ' +
188
+ 'Pass { buffer, access } or create the buffer through gpu.buffer.* with a bindable usage token.'
189
+ );
227
190
  }
191
+ return {
192
+ binding: index,
193
+ buffer: entry.buffer,
194
+ access,
195
+ };
228
196
  }
229
197
 
230
- class DoeGPUTextureView {
231
- constructor(native) { this._native = native; }
198
+ function bindGroupLayoutEntry(binding) {
199
+ const buffer_type = binding.access === 'uniform'
200
+ ? 'uniform'
201
+ : binding.access === 'storageRead'
202
+ ? 'read-only-storage'
203
+ : 'storage';
204
+ return {
205
+ binding: binding.binding,
206
+ visibility: DOE_GPU_SHADER_STAGE.COMPUTE,
207
+ buffer: { type: buffer_type },
208
+ };
232
209
  }
233
210
 
234
- class DoeGPUSampler {
235
- constructor(native) { this._native = native; }
211
+ function bindGroupEntry(binding) {
212
+ return {
213
+ binding: binding.binding,
214
+ resource: { buffer: binding.buffer },
215
+ };
236
216
  }
237
217
 
238
- class DoeGPURenderPipeline {
239
- constructor(native) { this._native = native; }
218
+ /**
219
+ * Reusable compute kernel compiled by `gpu.kernel.create(...)`.
220
+ *
221
+ * Surface: Doe API `gpu.kernel`.
222
+ * Input: Created from WGSL source, an entry point, and an initial binding shape.
223
+ * Returns: A reusable kernel object with `dispatch(...)`.
224
+ *
225
+ * This object keeps the compiled pipeline and bind-group layout for a repeated
226
+ * WGSL compute shape. Use it when you will dispatch the same shader more than
227
+ * once and want to avoid recompiling on every call.
228
+ *
229
+ * This example shows the API in its basic form.
230
+ *
231
+ * ```js
232
+ * const kernel = gpu.kernel.create({
233
+ * code,
234
+ * bindings: [src, dst],
235
+ * });
236
+ *
237
+ * await kernel.dispatch({
238
+ * bindings: [src, dst],
239
+ * workgroups: 1,
240
+ * });
241
+ * ```
242
+ *
243
+ * - See `gpu.kernel.run(...)` for the one-shot explicit path.
244
+ * - See `gpu.compute(...)` for the narrower typed-array workflow.
245
+ * - Instances are returned through the bound Doe API and are not exported directly.
246
+ */
247
+ class DoeKernel {
248
+ constructor(device, pipeline, layout, entryPoint) {
249
+ this.device = device;
250
+ this.pipeline = pipeline;
251
+ this.layout = layout;
252
+ this.entryPoint = entryPoint;
253
+ }
254
+
255
+ /**
256
+ * Dispatch this compiled kernel once.
257
+ *
258
+ * Surface: Doe API `gpu.kernel`.
259
+ * Input: A binding list, workgroup counts, and an optional label.
260
+ * Returns: A promise that resolves after submission completes.
261
+ *
262
+ * This records one compute pass for the compiled pipeline, submits it, and
263
+ * waits for completion when the underlying queue exposes
264
+ * `onSubmittedWorkDone()`.
265
+ *
266
+ * This example shows the API in its basic form.
267
+ *
268
+ * ```js
269
+ * await kernel.dispatch({
270
+ * bindings: [src, dst],
271
+ * workgroups: [4, 1, 1],
272
+ * });
273
+ * ```
274
+ *
275
+ * - `workgroups` may be `number`, `[x, y]`, or `[x, y, z]`.
276
+ * - Bare buffers without Doe helper metadata require `{ buffer, access }`.
277
+ * - See `gpu.kernel.run(...)` when you do not need reuse.
278
+ */
279
+ async dispatch(options) {
280
+ const bindings = (options.bindings ?? []).map(normalizeBinding);
281
+ const workgroups = validateWorkgroups(this.device, options.workgroups);
282
+ const bindGroup = this.device.createBindGroup({
283
+ label: options.label ?? undefined,
284
+ layout: this.layout,
285
+ entries: bindings.map(bindGroupEntry),
286
+ });
287
+ const encoder = this.device.createCommandEncoder({ label: options.label ?? undefined });
288
+ const pass = encoder.beginComputePass({ label: options.label ?? undefined });
289
+ pass.setPipeline(this.pipeline);
290
+ if (bindings.length > 0) {
291
+ pass.setBindGroup(0, bindGroup);
292
+ }
293
+ pass.dispatchWorkgroups(workgroups[0], workgroups[1], workgroups[2]);
294
+ pass.end();
295
+ this.device.queue.submit([encoder.finish()]);
296
+ if (typeof this.device.queue.onSubmittedWorkDone === 'function') {
297
+ await this.device.queue.onSubmittedWorkDone();
298
+ }
299
+ }
240
300
  }
241
301
 
242
- class DoeGPUShaderModule {
243
- constructor(native) { this._native = native; }
302
+ function createKernel(device, options) {
303
+ const bindings = (options.bindings ?? []).map(normalizeBinding);
304
+ const shader = device.createShaderModule({ code: options.code });
305
+ const bindGroupLayout = device.createBindGroupLayout({
306
+ entries: bindings.map(bindGroupLayoutEntry),
307
+ });
308
+ const pipelineLayout = device.createPipelineLayout({
309
+ bindGroupLayouts: [bindGroupLayout],
310
+ });
311
+ const pipeline = device.createComputePipeline({
312
+ layout: pipelineLayout,
313
+ compute: {
314
+ module: shader,
315
+ entryPoint: options.entryPoint ?? 'main',
316
+ },
317
+ });
318
+ return new DoeKernel(device, pipeline, bindGroupLayout, options.entryPoint ?? 'main');
244
319
  }
245
320
 
246
- class DoeGPUComputePipeline {
247
- constructor(native) { this._native = native; }
248
-
249
- getBindGroupLayout(index) {
250
- const layout = addon.computePipelineGetBindGroupLayout(this._native, index);
251
- return new DoeGPUBindGroupLayout(layout);
252
- }
321
+ function createBuffer(device, options) {
322
+ if (!options || typeof options !== 'object') {
323
+ throw new Error('Doe buffer options must be an object.');
324
+ }
325
+ if (options.data != null) {
326
+ const view = normalizeDataView(options.data);
327
+ const usage = options.usage ?? 'storageRead';
328
+ const buffer = rememberBufferUsage(device.createBuffer({
329
+ label: options.label ?? undefined,
330
+ size: options.size ?? view.byteLength,
331
+ usage: resolveBufferUsage(usage),
332
+ }), usage);
333
+ device.queue.writeBuffer(buffer, 0, view);
334
+ return buffer;
335
+ }
336
+ validatePositiveInteger(options.size, 'Doe buffer size');
337
+ return rememberBufferUsage(device.createBuffer({
338
+ label: options.label ?? undefined,
339
+ size: options.size,
340
+ usage: resolveBufferUsage(options.usage),
341
+ mappedAtCreation: options.mappedAtCreation ?? false,
342
+ }), options.usage);
253
343
  }
254
344
 
255
- class DoeGPUBindGroupLayout {
256
- constructor(native) { this._native = native; }
345
+ function createBufferFromData(device, data, options = {}) {
346
+ const view = normalizeDataView(data);
347
+ const usage = options.usage ?? 'storageRead';
348
+ const buffer = rememberBufferUsage(device.createBuffer({
349
+ label: options.label ?? undefined,
350
+ size: view.byteLength,
351
+ usage: resolveBufferUsage(usage),
352
+ }), usage);
353
+ device.queue.writeBuffer(buffer, 0, view);
354
+ return buffer;
257
355
  }
258
356
 
259
- class DoeGPUBindGroup {
260
- constructor(native) { this._native = native; }
357
+ async function readBuffer(device, buffer, type, options = {}) {
358
+ if (arguments.length === 2 && buffer && typeof buffer === 'object') {
359
+ return readBuffer(device, buffer.buffer, buffer.type, buffer);
360
+ }
361
+ if (!buffer || typeof buffer !== 'object') {
362
+ throw new Error('Doe buffer.read requires a buffer.');
363
+ }
364
+ if (typeof type !== 'function') {
365
+ throw new Error('Doe buffer.read type must be a typed-array constructor.');
366
+ }
367
+ const offset = options.offset ?? 0;
368
+ const size = options.size ?? Math.max(0, (buffer.size ?? 0) - offset);
369
+ if (!Number.isInteger(offset) || offset < 0) {
370
+ throw new Error('Doe buffer.read offset must be a non-negative integer.');
371
+ }
372
+ if (!Number.isInteger(size) || size < 0) {
373
+ throw new Error('Doe buffer.read size must be a non-negative integer.');
374
+ }
375
+ if (((buffer.usage ?? 0) & DOE_GPU_BUFFER_USAGE.MAP_READ) !== 0) {
376
+ await buffer.mapAsync(DOE_GPU_MAP_MODE.READ, offset, size);
377
+ const copy = buffer.getMappedRange(offset, size).slice(0);
378
+ buffer.unmap();
379
+ return new type(copy);
380
+ }
381
+ const staging = device.createBuffer({
382
+ label: options.label ?? undefined,
383
+ size,
384
+ usage: DOE_GPU_BUFFER_USAGE.COPY_DST | DOE_GPU_BUFFER_USAGE.MAP_READ,
385
+ });
386
+ const encoder = device.createCommandEncoder({ label: options.label ?? undefined });
387
+ encoder.copyBufferToBuffer(buffer, offset, staging, 0, size);
388
+ device.queue.submit([encoder.finish()]);
389
+ await staging.mapAsync(DOE_GPU_MAP_MODE.READ);
390
+ const copy = staging.getMappedRange().slice(0);
391
+ staging.unmap();
392
+ if (typeof staging.destroy === 'function') {
393
+ staging.destroy();
394
+ }
395
+ return new type(copy);
261
396
  }
262
397
 
263
- class DoeGPUPipelineLayout {
264
- constructor(native) { this._native = native; }
398
+ async function runKernel(device, options) {
399
+ const kernel = createKernel(device, options);
400
+ await kernel.dispatch({
401
+ bindings: options.bindings ?? [],
402
+ workgroups: options.workgroups,
403
+ label: options.label,
404
+ });
265
405
  }
266
406
 
267
- // Metal defaults for Apple Silicon — matches doe_device_caps.zig METAL_LIMITS.
268
- const DOE_LIMITS = Object.freeze({
269
- maxTextureDimension1D: 16384,
270
- maxTextureDimension2D: 16384,
271
- maxTextureDimension3D: 2048,
272
- maxTextureArrayLayers: 2048,
273
- maxBindGroups: 4,
274
- maxBindGroupsPlusVertexBuffers: 24,
275
- maxBindingsPerBindGroup: 1000,
276
- maxDynamicUniformBuffersPerPipelineLayout: 8,
277
- maxDynamicStorageBuffersPerPipelineLayout: 4,
278
- maxSampledTexturesPerShaderStage: 16,
279
- maxSamplersPerShaderStage: 16,
280
- maxStorageBuffersPerShaderStage: 8,
281
- maxStorageTexturesPerShaderStage: 4,
282
- maxUniformBuffersPerShaderStage: 12,
283
- maxUniformBufferBindingSize: 65536,
284
- maxStorageBufferBindingSize: 134217728,
285
- minUniformBufferOffsetAlignment: 256,
286
- minStorageBufferOffsetAlignment: 32,
287
- maxVertexBuffers: 8,
288
- maxBufferSize: 268435456,
289
- maxVertexAttributes: 16,
290
- maxVertexBufferArrayStride: 2048,
291
- maxInterStageShaderVariables: 16,
292
- maxColorAttachments: 8,
293
- maxColorAttachmentBytesPerSample: 32,
294
- maxComputeWorkgroupStorageSize: 32768,
295
- maxComputeInvocationsPerWorkgroup: 1024,
296
- maxComputeWorkgroupSizeX: 1024,
297
- maxComputeWorkgroupSizeY: 1024,
298
- maxComputeWorkgroupSizeZ: 64,
299
- maxComputeWorkgroupsPerDimension: 65535,
300
- });
301
-
302
- const DOE_FEATURES = Object.freeze(new Set(['shader-f16']));
303
-
304
- class DoeGPUDevice {
305
- constructor(native, instance) {
306
- this._native = native;
307
- this._instance = instance;
308
- const q = addon.deviceGetQueue(native);
309
- this.queue = new DoeGPUQueue(q);
310
- this.limits = DOE_LIMITS;
311
- this.features = DOE_FEATURES;
312
- }
313
-
314
- createBuffer(descriptor) {
315
- const buf = addon.createBuffer(this._native, descriptor);
316
- return new DoeGPUBuffer(buf, this._instance, descriptor.size, descriptor.usage, this.queue._native);
317
- }
318
-
319
- createShaderModule(descriptor) {
320
- const code = descriptor.code || descriptor.source;
321
- if (!code) throw new Error('createShaderModule: descriptor.code is required');
322
- const mod = addon.createShaderModule(this._native, code);
323
- return new DoeGPUShaderModule(mod);
324
- }
325
-
326
- createComputePipeline(descriptor) {
327
- const shader = descriptor.compute?.module;
328
- const entryPoint = descriptor.compute?.entryPoint || 'main';
329
- const layout = descriptor.layout === 'auto' ? null : descriptor.layout;
330
- const native = addon.createComputePipeline(
331
- this._native, shader._native, entryPoint,
332
- layout?._native ?? null);
333
- return new DoeGPUComputePipeline(native);
334
- }
335
-
336
- async createComputePipelineAsync(descriptor) {
337
- return this.createComputePipeline(descriptor);
338
- }
339
-
340
- createBindGroupLayout(descriptor) {
341
- const entries = (descriptor.entries || []).map((e) => ({
342
- binding: e.binding,
343
- visibility: e.visibility,
344
- buffer: e.buffer ? {
345
- type: e.buffer.type || 'uniform',
346
- hasDynamicOffset: e.buffer.hasDynamicOffset || false,
347
- minBindingSize: e.buffer.minBindingSize || 0,
348
- } : undefined,
349
- storageTexture: e.storageTexture,
350
- }));
351
- const native = addon.createBindGroupLayout(this._native, entries);
352
- return new DoeGPUBindGroupLayout(native);
353
- }
354
-
355
- createBindGroup(descriptor) {
356
- const entries = (descriptor.entries || []).map((e) => {
357
- const entry = {
358
- binding: e.binding,
359
- buffer: e.resource?.buffer?._native ?? e.resource?._native ?? null,
360
- offset: e.resource?.offset ?? 0,
361
- };
362
- if (e.resource?.size !== undefined) entry.size = e.resource.size;
363
- return entry;
364
- });
365
- const native = addon.createBindGroup(
366
- this._native, descriptor.layout._native, entries);
367
- return new DoeGPUBindGroup(native);
368
- }
407
+ function usesRawNumericFlags(usage) {
408
+ return typeof usage === 'number' || (Array.isArray(usage) && usage.some((token) => typeof token === 'number'));
409
+ }
369
410
 
370
- createPipelineLayout(descriptor) {
371
- const layouts = (descriptor.bindGroupLayouts || []).map((l) => l._native);
372
- const native = addon.createPipelineLayout(this._native, layouts);
373
- return new DoeGPUPipelineLayout(native);
411
+ function assertLayer3Usage(usage, access, path) {
412
+ if (usesRawNumericFlags(usage) && !access) {
413
+ throw new Error(`Doe ${path} accepts raw numeric usage flags only when explicit access is also provided.`);
374
414
  }
415
+ }
375
416
 
376
- createTexture(descriptor) {
377
- const native = addon.createTexture(this._native, {
378
- format: descriptor.format || 'rgba8unorm',
379
- width: descriptor.size?.[0] ?? descriptor.size?.width ?? descriptor.size ?? 1,
380
- height: descriptor.size?.[1] ?? descriptor.size?.height ?? 1,
381
- depthOrArrayLayers: descriptor.size?.[2] ?? descriptor.size?.depthOrArrayLayers ?? 1,
382
- usage: descriptor.usage || 0,
383
- mipLevelCount: descriptor.mipLevelCount || 1,
417
+ function normalizeOnceInput(device, input, index) {
418
+ if (ArrayBuffer.isView(input) || input instanceof ArrayBuffer) {
419
+ const buffer = createBufferFromData(device, input, {});
420
+ return {
421
+ binding: buffer,
422
+ buffer,
423
+ byte_length: resolveBufferSize(input),
424
+ owned: true,
425
+ };
426
+ }
427
+
428
+ if (input && typeof input === 'object' && 'data' in input) {
429
+ assertLayer3Usage(input.usage, input.access, `compute input ${index} usage`);
430
+ const buffer = createBufferFromData(device, input.data, {
431
+ usage: input.usage ?? 'storageRead',
432
+ label: input.label,
384
433
  });
385
- return new DoeGPUTexture(native);
386
- }
387
-
388
- createSampler(descriptor = {}) {
389
- const native = addon.createSampler(this._native, descriptor);
390
- return new DoeGPUSampler(native);
434
+ return {
435
+ binding: input.access ? { buffer, access: input.access } : buffer,
436
+ buffer,
437
+ byte_length: resolveBufferSize(input.data),
438
+ owned: true,
439
+ };
391
440
  }
392
441
 
393
- createRenderPipeline(descriptor) {
394
- const native = addon.createRenderPipeline(this._native);
395
- return new DoeGPURenderPipeline(native);
442
+ if (input && typeof input === 'object' && 'buffer' in input) {
443
+ return {
444
+ binding: input,
445
+ buffer: input.buffer,
446
+ byte_length: resolveBufferSize(input.buffer),
447
+ owned: false,
448
+ };
396
449
  }
397
450
 
398
- createCommandEncoder(descriptor) {
399
- const native = addon.createCommandEncoder(this._native);
400
- return new DoeGPUCommandEncoder(native);
451
+ if (input && typeof input === 'object' && typeof input.size === 'number') {
452
+ return {
453
+ binding: input,
454
+ buffer: input,
455
+ byte_length: input.size,
456
+ owned: false,
457
+ };
401
458
  }
402
459
 
403
- destroy() {
404
- addon.deviceRelease(this._native);
405
- this._native = null;
406
- }
460
+ throw new Error(`Doe compute input ${index} must be data, a Doe input spec, or a buffer.`);
407
461
  }
408
462
 
409
- class DoeGPUAdapter {
410
- constructor(native, instance) {
411
- this._native = native;
412
- this._instance = instance;
413
- this.features = DOE_FEATURES;
414
- this.limits = DOE_LIMITS;
415
- }
416
-
417
- async requestDevice(descriptor) {
418
- const device = addon.requestDevice(this._instance, this._native);
419
- return new DoeGPUDevice(device, this._instance);
463
+ function normalizeOnceOutput(device, output, inputs) {
464
+ if (!output || typeof output !== 'object') {
465
+ throw new Error('Doe compute output is required.');
420
466
  }
421
-
422
- destroy() {
423
- addon.adapterRelease(this._native);
424
- this._native = null;
467
+ if (typeof output.type !== 'function') {
468
+ throw new Error('Doe compute output.type must be a typed-array constructor.');
425
469
  }
426
- }
427
470
 
428
- class DoeGPU {
429
- constructor(instance) {
430
- this._instance = instance;
471
+ const fallbackInputIndex = inputs.length > 0 ? 0 : null;
472
+ const likeInputIndex = output.likeInput ?? fallbackInputIndex;
473
+ if (likeInputIndex != null && (!Number.isInteger(likeInputIndex) || likeInputIndex < 0 || likeInputIndex >= inputs.length)) {
474
+ throw new Error(`Doe compute output.likeInput must reference an input index in [0, ${Math.max(inputs.length - 1, 0)}].`);
431
475
  }
476
+ const size = output.size ?? (
477
+ likeInputIndex != null && inputs[likeInputIndex]
478
+ ? inputs[likeInputIndex].byte_length
479
+ : null
480
+ );
432
481
 
433
- async requestAdapter(options) {
434
- const adapter = addon.requestAdapter(this._instance);
435
- return new DoeGPUAdapter(adapter, this._instance);
482
+ if (!(size > 0)) {
483
+ throw new Error('Doe compute output size must be provided or derived from likeInput.');
436
484
  }
437
- }
438
485
 
439
- export function create(createArgs = null) {
440
- ensureLibrary();
441
- const instance = addon.createInstance();
442
- return new DoeGPU(instance);
486
+ assertLayer3Usage(output.usage, output.access, 'compute output usage');
487
+ const buffer = createBuffer(device, {
488
+ size,
489
+ usage: output.usage ?? 'storageReadWrite',
490
+ label: output.label,
491
+ });
492
+ return {
493
+ binding: output.access ? { buffer, access: output.access } : buffer,
494
+ buffer,
495
+ type: output.type,
496
+ read_options: output.read ?? {},
497
+ };
443
498
  }
444
499
 
445
- export function setupGlobals(target = globalThis, createArgs = null) {
446
- for (const [name, value] of Object.entries(globals)) {
447
- if (target[name] === undefined) {
448
- Object.defineProperty(target, name, {
449
- value,
450
- writable: true,
451
- configurable: true,
452
- enumerable: false,
453
- });
454
- }
455
- }
456
- const gpu = create(createArgs);
457
- if (typeof target.navigator === 'undefined') {
458
- Object.defineProperty(target, 'navigator', {
459
- value: { gpu },
460
- writable: true,
461
- configurable: true,
462
- enumerable: false,
463
- });
464
- } else if (!target.navigator.gpu) {
465
- Object.defineProperty(target.navigator, 'gpu', {
466
- value: gpu,
467
- writable: true,
468
- configurable: true,
469
- enumerable: false,
500
+ async function computeOnce(device, options) {
501
+ const inputs = (options.inputs ?? []).map((input, index) => normalizeOnceInput(device, input, index));
502
+ const output = normalizeOnceOutput(device, options.output, inputs);
503
+ validateWorkgroups(device, options.workgroups);
504
+ try {
505
+ await runKernel(device, {
506
+ code: options.code,
507
+ entryPoint: options.entryPoint,
508
+ bindings: [...inputs.map((input) => input.binding), output.binding],
509
+ workgroups: options.workgroups,
510
+ label: options.label,
470
511
  });
512
+ return await readBuffer(device, output.buffer, output.type, output.read_options);
513
+ } finally {
514
+ if (typeof output.buffer.destroy === 'function') {
515
+ output.buffer.destroy();
516
+ }
517
+ for (const input of inputs) {
518
+ if (input.owned && typeof input.buffer.destroy === 'function') {
519
+ input.buffer.destroy();
520
+ }
521
+ }
471
522
  }
472
- return gpu;
473
- }
474
-
475
- export async function requestAdapter(adapterOptions = undefined, createArgs = null) {
476
- const gpu = create(createArgs);
477
- return gpu.requestAdapter(adapterOptions);
478
523
  }
479
524
 
480
- export async function requestDevice(options = {}) {
481
- const createArgs = options?.createArgs ?? null;
482
- const adapter = await requestAdapter(options?.adapterOptions, createArgs);
483
- return adapter.requestDevice(options?.deviceDescriptor);
525
+ function createBoundDoe(device) {
526
+ /**
527
+ * Run a one-shot typed-array compute workflow.
528
+ *
529
+ * Surface: Doe API `gpu.compute`.
530
+ * Input: WGSL source, typed-array or buffer inputs, an output spec, and workgroups.
531
+ * Returns: A promise for the requested typed-array output.
532
+ *
533
+ * This is the most opinionated Doe helper. It creates temporary buffers
534
+ * as needed, uploads host data, dispatches the compute shader once,
535
+ * reads back the requested output, and destroys temporary resources
536
+ * before returning.
537
+ *
538
+ * This example shows the API in its basic form.
539
+ *
540
+ * ```js
541
+ * const out = await gpu.compute({
542
+ * code,
543
+ * inputs: [new Float32Array([1, 2, 3, 4])],
544
+ * output: { type: Float32Array },
545
+ * workgroups: 1,
546
+ * });
547
+ * ```
548
+ *
549
+ * - Raw numeric usage flags are accepted only when explicit Doe access is also provided.
550
+ * - Output size defaults from `likeInput` or the first input when possible.
551
+ * - See `gpu.kernel.run(...)` or `gpu.kernel.create(...)` when you need explicit resource ownership.
552
+ */
553
+ const compute = function compute(options) {
554
+ return computeOnce(device, options);
555
+ };
556
+ return {
557
+ device,
558
+ buffer: {
559
+ /**
560
+ * Create a buffer with explicit size and Doe usage tokens.
561
+ *
562
+ * Surface: Doe API `gpu.buffer`.
563
+ * Input: A buffer size, usage, and optional label or mapping flag.
564
+ * Returns: A GPU buffer with Doe usage metadata attached when possible.
565
+ *
566
+ * This is the explicit Doe helper over `device.createBuffer(...)`. It
567
+ * accepts Doe usage tokens such as `storageReadWrite`, and when `data`
568
+ * is provided it allocates and uploads in one step. Doe remembers the
569
+ * resulting binding access so later helper calls can infer how the
570
+ * buffer should be bound.
571
+ *
572
+ * This example shows the API in its basic form.
573
+ *
574
+ * ```js
575
+ * const src = gpu.buffer.create({ data: new Float32Array([1, 2, 3, 4]) });
576
+ * const dst = gpu.buffer.create({ size: src.size, usage: "storageReadWrite" });
577
+ * ```
578
+ *
579
+ * - When `data` is provided, usage defaults to `storageRead`.
580
+ * - Raw numeric usage flags are allowed here for explicit control.
581
+ * - Buffers created with raw numeric flags may later require `{ buffer, access }`.
582
+ */
583
+ create(options) {
584
+ return createBuffer(device, options);
585
+ },
586
+ /**
587
+ * Read a buffer back into a typed array.
588
+ *
589
+ * Surface: Doe API `gpu.buffer`.
590
+ * Input: A source buffer, a typed-array constructor, and optional offset or size.
591
+ * Returns: A promise for a newly allocated typed array.
592
+ *
593
+ * This reads GPU buffer contents back to JS. If the buffer is already
594
+ * mappable for read, Doe maps it directly; otherwise Doe stages the copy
595
+ * through a temporary readback buffer.
596
+ *
597
+ * This example shows the API in its basic form.
598
+ *
599
+ * ```js
600
+ * const out = await gpu.buffer.read(dst, Float32Array);
601
+ * ```
602
+ *
603
+ * - `options.offset` and `options.size` let you read a subrange.
604
+ * - The typed-array constructor must accept a plain `ArrayBuffer`.
605
+ * - See raw `buffer.mapAsync(...)` when you need manual readback control.
606
+ */
607
+ read(options_or_buffer, type, options = {}) {
608
+ if (arguments.length === 1 && options_or_buffer && typeof options_or_buffer === 'object') {
609
+ return readBuffer(device, options_or_buffer);
610
+ }
611
+ return readBuffer(device, options_or_buffer, type, options);
612
+ },
613
+ },
614
+ kernel: {
615
+ /**
616
+ * Compile and dispatch a one-off compute job.
617
+ *
618
+ * Surface: Doe API `gpu.kernel`.
619
+ * Input: WGSL source, bindings, workgroups, and an optional entry point or label.
620
+ * Returns: A promise that resolves after submission completes.
621
+ *
622
+ * This is the explicit one-shot compute path. It builds the pipeline for
623
+ * the provided shader, dispatches once, and waits for completion.
624
+ *
625
+ * This example shows the API in its basic form.
626
+ *
627
+ * ```js
628
+ * await gpu.kernel.run({
629
+ * code,
630
+ * bindings: [src, dst],
631
+ * workgroups: 1,
632
+ * });
633
+ * ```
634
+ *
635
+ * - `workgroups` may be `number`, `[x, y]`, or `[x, y, z]`.
636
+ * - Bare buffers without Doe helper metadata require `{ buffer, access }`.
637
+ * - See `gpu.kernel.create(...)` when you will reuse the shader shape.
638
+ * - See `gpu.compute(...)` for the narrower typed-array workflow.
639
+ */
640
+ run(options) {
641
+ return runKernel(device, options);
642
+ },
643
+ /**
644
+ * Compile a reusable compute kernel.
645
+ *
646
+ * Surface: Doe API `gpu.kernel`.
647
+ * Input: WGSL source, an optional entry point, and an initial binding shape.
648
+ * Returns: A `DoeKernel` object with `dispatch(...)`.
649
+ *
650
+ * This creates the shader module, bind-group layout, and compute
651
+ * pipeline once so the same WGSL shape can be dispatched repeatedly.
652
+ *
653
+ * This example shows the API in its basic form.
654
+ *
655
+ * ```js
656
+ * const kernel = gpu.kernel.create({
657
+ * code,
658
+ * bindings: [src, dst],
659
+ * });
660
+ * ```
661
+ *
662
+ * - Binding access is inferred from the bindings passed at compile time.
663
+ * - See `kernel.dispatch(...)` to run the compiled kernel.
664
+ * - See `gpu.kernel.run(...)` when reuse does not matter.
665
+ */
666
+ create(options) {
667
+ return createKernel(device, options);
668
+ },
669
+ },
670
+ compute,
671
+ };
484
672
  }
485
673
 
486
- export function providerInfo() {
674
+ export function createDoeNamespace({ requestDevice } = {}) {
487
675
  return {
488
- module: '@simulatte/webgpu-doe',
489
- loaded: !!addon && !!DOE_LIB_PATH,
490
- loadError: !addon ? 'native addon not found' : !DOE_LIB_PATH ? 'libdoe_webgpu not found' : '',
491
- defaultCreateArgs: [],
492
- doeNative: true,
493
- doeLibraryPath: DOE_LIB_PATH ?? '',
676
+ /**
677
+ * Request a device and return the bound Doe API in one step.
678
+ *
679
+ * Surface: Doe API namespace.
680
+ * Input: Optional package-local request options.
681
+ * Returns: A promise for the bound `gpu` helper object.
682
+ *
683
+ * This calls the package-local `requestDevice(...)` implementation and
684
+ * then wraps the resulting raw device in the bound Doe API.
685
+ *
686
+ * This example shows the API in its basic form.
687
+ *
688
+ * ```js
689
+ * const gpu = await doe.requestDevice();
690
+ * ```
691
+ *
692
+ * - Throws if this namespace was created without a `requestDevice` implementation.
693
+ * - `gpu.device` exposes the underlying raw device when you need lower-level control.
694
+ * - See `doe.bind(device)` when you already have a raw device.
695
+ */
696
+ async requestDevice(options = {}) {
697
+ if (typeof requestDevice !== 'function') {
698
+ throw new Error('Doe requestDevice() is unavailable in this context.');
699
+ }
700
+ return createBoundDoe(await requestDevice(options));
701
+ },
702
+
703
+ /**
704
+ * Wrap an existing device in the bound Doe API.
705
+ *
706
+ * Surface: Doe API namespace.
707
+ * Input: A raw device returned by the package surface.
708
+ * Returns: The bound `gpu` helper object for that device.
709
+ *
710
+ * Use this when you need the raw device first, but still want to opt into
711
+ * Doe helpers afterward.
712
+ *
713
+ * This example shows the API in its basic form.
714
+ *
715
+ * ```js
716
+ * const device = await requestDevice();
717
+ * const gpu = doe.bind(device);
718
+ * ```
719
+ *
720
+ * - No async work happens here; it only wraps the device you already have.
721
+ * - See `doe.requestDevice(...)` for the one-step helper entrypoint.
722
+ */
723
+ bind(device) {
724
+ return createBoundDoe(device);
725
+ },
494
726
  };
495
727
  }
728
+
729
+ export const doe = createDoeNamespace();
730
+
731
+ export default doe;