@simulatte/webgpu-doe 0.1.3 → 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 ADDED
@@ -0,0 +1,731 @@
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
+ };
8
+
9
+ const DOE_GPU_SHADER_STAGE = {
10
+ COMPUTE: 0x4,
11
+ };
12
+
13
+ const DOE_GPU_MAP_MODE = {
14
+ READ: 0x0001,
15
+ };
16
+
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}`);
35
+ }
36
+ }
37
+
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.');
50
+ }
51
+
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;
62
+ }
63
+ }
64
+
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
+ }
81
+
82
+ function rememberBufferUsage(buffer, usage) {
83
+ DOE_BUFFER_META.set(buffer, {
84
+ bindingAccess: inferBindingAccess(usage),
85
+ });
86
+ return buffer;
87
+ }
88
+
89
+ function inferredBindingAccessForBuffer(buffer) {
90
+ return DOE_BUFFER_META.get(buffer)?.bindingAccess ?? null;
91
+ }
92
+
93
+ function normalizeWorkgroups(workgroups) {
94
+ if (typeof workgroups === 'number') {
95
+ return [workgroups, 1, 1];
96
+ }
97
+ if (Array.isArray(workgroups) && workgroups.length === 2) {
98
+ return [workgroups[0], workgroups[1], 1];
99
+ }
100
+ if (Array.isArray(workgroups) && workgroups.length === 3) {
101
+ return workgroups;
102
+ }
103
+ throw new Error('Doe workgroups must be a number, [x, y], or [x, y, z].');
104
+ }
105
+
106
+ function validatePositiveInteger(value, label) {
107
+ if (!Number.isInteger(value) || value < 1) {
108
+ throw new Error(`${label} must be a positive integer.`);
109
+ }
110
+ }
111
+
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
+ }
129
+ }
130
+ if (limits.maxComputeWorkgroupSizeX && x > limits.maxComputeWorkgroupSizeX) {
131
+ throw new Error(
132
+ `Doe workgroups.x (${x}) exceeds maxComputeWorkgroupSizeX (${limits.maxComputeWorkgroupSizeX}).`
133
+ );
134
+ }
135
+ if (limits.maxComputeWorkgroupSizeY && y > limits.maxComputeWorkgroupSizeY) {
136
+ throw new Error(
137
+ `Doe workgroups.y (${y}) exceeds maxComputeWorkgroupSizeY (${limits.maxComputeWorkgroupSizeY}).`
138
+ );
139
+ }
140
+ if (limits.maxComputeWorkgroupSizeZ && z > limits.maxComputeWorkgroupSizeZ) {
141
+ throw new Error(
142
+ `Doe workgroups.z (${z}) exceeds maxComputeWorkgroupSizeZ (${limits.maxComputeWorkgroupSizeZ}).`
143
+ );
144
+ }
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
+ }
152
+ }
153
+
154
+ return normalized;
155
+ }
156
+
157
+ function normalizeDataView(data) {
158
+ if (ArrayBuffer.isView(data)) {
159
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
160
+ }
161
+ if (data instanceof ArrayBuffer) {
162
+ return new Uint8Array(data);
163
+ }
164
+ throw new Error('Doe buffer data must be an ArrayBuffer or ArrayBufferView.');
165
+ }
166
+
167
+ function resolveBufferSize(source) {
168
+ if (source && typeof source === 'object' && typeof source.size === 'number') {
169
+ return source.size;
170
+ }
171
+ if (ArrayBuffer.isView(source)) {
172
+ return source.byteLength;
173
+ }
174
+ if (source instanceof ArrayBuffer) {
175
+ return source.byteLength;
176
+ }
177
+ throw new Error('Doe buffer-like source must expose a byte size or be ArrayBuffer-backed data.');
178
+ }
179
+
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
+ );
190
+ }
191
+ return {
192
+ binding: index,
193
+ buffer: entry.buffer,
194
+ access,
195
+ };
196
+ }
197
+
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
+ };
209
+ }
210
+
211
+ function bindGroupEntry(binding) {
212
+ return {
213
+ binding: binding.binding,
214
+ resource: { buffer: binding.buffer },
215
+ };
216
+ }
217
+
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
+ }
300
+ }
301
+
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');
319
+ }
320
+
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);
343
+ }
344
+
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;
355
+ }
356
+
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);
396
+ }
397
+
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
+ });
405
+ }
406
+
407
+ function usesRawNumericFlags(usage) {
408
+ return typeof usage === 'number' || (Array.isArray(usage) && usage.some((token) => typeof token === 'number'));
409
+ }
410
+
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.`);
414
+ }
415
+ }
416
+
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,
433
+ });
434
+ return {
435
+ binding: input.access ? { buffer, access: input.access } : buffer,
436
+ buffer,
437
+ byte_length: resolveBufferSize(input.data),
438
+ owned: true,
439
+ };
440
+ }
441
+
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
+ };
449
+ }
450
+
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
+ };
458
+ }
459
+
460
+ throw new Error(`Doe compute input ${index} must be data, a Doe input spec, or a buffer.`);
461
+ }
462
+
463
+ function normalizeOnceOutput(device, output, inputs) {
464
+ if (!output || typeof output !== 'object') {
465
+ throw new Error('Doe compute output is required.');
466
+ }
467
+ if (typeof output.type !== 'function') {
468
+ throw new Error('Doe compute output.type must be a typed-array constructor.');
469
+ }
470
+
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)}].`);
475
+ }
476
+ const size = output.size ?? (
477
+ likeInputIndex != null && inputs[likeInputIndex]
478
+ ? inputs[likeInputIndex].byte_length
479
+ : null
480
+ );
481
+
482
+ if (!(size > 0)) {
483
+ throw new Error('Doe compute output size must be provided or derived from likeInput.');
484
+ }
485
+
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
+ };
498
+ }
499
+
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,
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
+ }
522
+ }
523
+ }
524
+
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
+ };
672
+ }
673
+
674
+ export function createDoeNamespace({ requestDevice } = {}) {
675
+ return {
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
+ },
726
+ };
727
+ }
728
+
729
+ export const doe = createDoeNamespace();
730
+
731
+ export default doe;