@simulatte/webgpu 0.3.0 → 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.
Files changed (50) hide show
  1. package/CHANGELOG.md +37 -10
  2. package/LICENSE +191 -0
  3. package/README.md +62 -48
  4. package/api-contract.md +67 -49
  5. package/architecture.md +317 -0
  6. package/assets/package-layers.svg +3 -3
  7. package/docs/doe-api-reference.html +1842 -0
  8. package/doe-api-design.md +237 -0
  9. package/examples/doe-api/README.md +19 -0
  10. package/examples/doe-api/buffers-readback.js +3 -2
  11. package/examples/{doe-routines/compute-once-like-input.js → doe-api/compute-one-shot-like-input.js} +1 -1
  12. package/examples/{doe-routines/compute-once-matmul.js → doe-api/compute-one-shot-matmul.js} +2 -2
  13. package/examples/{doe-routines/compute-once-multiple-inputs.js → doe-api/compute-one-shot-multiple-inputs.js} +1 -1
  14. package/examples/{doe-routines/compute-once.js → doe-api/compute-one-shot.js} +1 -1
  15. package/examples/doe-api/{compile-and-dispatch.js → kernel-create-and-dispatch.js} +4 -6
  16. package/examples/doe-api/{compute-dispatch.js → kernel-run.js} +4 -6
  17. package/headless-webgpu-comparison.md +3 -3
  18. package/jsdoc-style-guide.md +435 -0
  19. package/native/doe_napi.c +1481 -84
  20. package/package.json +18 -6
  21. package/prebuilds/darwin-arm64/doe_napi.node +0 -0
  22. package/prebuilds/darwin-arm64/libwebgpu_doe.dylib +0 -0
  23. package/prebuilds/darwin-arm64/metadata.json +5 -5
  24. package/prebuilds/linux-x64/metadata.json +1 -1
  25. package/scripts/generate-doe-api-docs.js +1607 -0
  26. package/scripts/generate-readme-assets.js +3 -3
  27. package/src/build_metadata.js +7 -4
  28. package/src/bun-ffi.js +1229 -474
  29. package/src/bun.js +5 -1
  30. package/src/compute.d.ts +16 -7
  31. package/src/compute.js +84 -53
  32. package/src/full.d.ts +16 -7
  33. package/src/full.js +12 -10
  34. package/src/index.js +679 -1324
  35. package/src/runtime_cli.js +17 -17
  36. package/src/shared/capabilities.js +144 -0
  37. package/src/shared/compiler-errors.js +78 -0
  38. package/src/shared/encoder-surface.js +295 -0
  39. package/src/shared/full-surface.js +514 -0
  40. package/src/shared/public-surface.js +82 -0
  41. package/src/shared/resource-lifecycle.js +120 -0
  42. package/src/shared/validation.js +495 -0
  43. package/src/webgpu_constants.js +30 -0
  44. package/support-contracts.md +2 -2
  45. package/compat-scope.md +0 -46
  46. package/layering-plan.md +0 -259
  47. package/src/auto_bind_group_layout.js +0 -32
  48. package/src/doe.d.ts +0 -184
  49. package/src/doe.js +0 -641
  50. package/zig-source-inventory.md +0 -468
package/src/index.js CHANGED
@@ -2,15 +2,66 @@ import { createRequire } from 'node:module';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { resolve, dirname } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
+ import { globals } from './webgpu_constants.js';
5
6
  import {
6
7
  createDoeRuntime as createDoeRuntimeCli,
7
8
  runDawnVsDoeCompare as runDawnVsDoeCompareCli,
8
9
  } from './runtime_cli.js';
9
10
  import { loadDoeBuildMetadata } from './build_metadata.js';
10
- import { inferAutoBindGroupLayouts } from './auto_bind_group_layout.js';
11
+ import {
12
+ UINT32_MAX,
13
+ failValidation,
14
+ describeResourceLabel,
15
+ initResource,
16
+ assertObject,
17
+ assertArray,
18
+ assertBoolean,
19
+ assertNonEmptyString,
20
+ assertIntegerInRange,
21
+ assertOptionalIntegerInRange,
22
+ validatePositiveInteger,
23
+ assertLiveResource,
24
+ destroyResource,
25
+ } from './shared/resource-lifecycle.js';
26
+ import {
27
+ publishLimits,
28
+ publishFeatures,
29
+ } from './shared/capabilities.js';
30
+ import {
31
+ assertBufferDescriptor,
32
+ assertTextureSize,
33
+ assertBindGroupResource,
34
+ normalizeSamplerLayout,
35
+ normalizeTextureLayout,
36
+ normalizeStorageTextureLayout,
37
+ autoLayoutEntriesFromNativeBindings,
38
+ } from './shared/validation.js';
39
+ import {
40
+ setupGlobalsOnTarget,
41
+ requestAdapterFromCreate,
42
+ requestDeviceFromRequestAdapter,
43
+ buildProviderInfo,
44
+ libraryFlavor,
45
+ } from './shared/public-surface.js';
46
+ import {
47
+ shaderCheckFailure,
48
+ enrichNativeCompilerError,
49
+ compilerErrorFromMessage,
50
+ } from './shared/compiler-errors.js';
51
+ import {
52
+ createFullSurfaceClasses,
53
+ } from './shared/full-surface.js';
54
+ import {
55
+ createEncoderClasses,
56
+ } from './shared/encoder-surface.js';
11
57
 
12
58
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
59
  const require = createRequire(import.meta.url);
60
+ const TEXTURE_DIMENSION_MAP = Object.freeze({
61
+ '1d': 1,
62
+ '2d': 2,
63
+ '3d': 3,
64
+ });
14
65
 
15
66
  const addon = loadAddon();
16
67
  const DOE_LIB_PATH = resolveDoeLibraryPath();
@@ -20,6 +71,9 @@ const DOE_BUILD_METADATA = loadDoeBuildMetadata({
20
71
  });
21
72
  let libraryLoaded = false;
22
73
 
74
+ export { globals, preflightShaderSource };
75
+
76
+
23
77
  function loadAddon() {
24
78
  const prebuildPath = resolve(__dirname, '..', 'prebuilds', `${process.platform}-${process.arch}`, 'doe_napi.node');
25
79
  try {
@@ -44,8 +98,8 @@ function resolveDoeLibraryPath() {
44
98
  const candidates = [
45
99
  process.env.DOE_WEBGPU_LIB,
46
100
  process.env.FAWN_DOE_LIB,
47
- resolve(__dirname, '..', 'prebuilds', `${process.platform}-${process.arch}`, `libwebgpu_doe.${ext}`),
48
101
  resolve(__dirname, '..', '..', '..', 'zig', 'zig-out', 'lib', `libwebgpu_doe.${ext}`),
102
+ resolve(__dirname, '..', 'prebuilds', `${process.platform}-${process.arch}`, `libwebgpu_doe.${ext}`),
49
103
  resolve(process.cwd(), 'zig', 'zig-out', 'lib', `libwebgpu_doe.${ext}`),
50
104
  ];
51
105
 
@@ -55,17 +109,6 @@ function resolveDoeLibraryPath() {
55
109
  return null;
56
110
  }
57
111
 
58
- function libraryFlavor(libraryPath) {
59
- if (!libraryPath) return 'missing';
60
- if (libraryPath.endsWith('libwebgpu_doe.so') || libraryPath.endsWith('libwebgpu_doe.dylib') || libraryPath.endsWith('libwebgpu_doe.dll')) {
61
- return 'doe-dropin';
62
- }
63
- if (libraryPath.endsWith('libwebgpu.so') || libraryPath.endsWith('libwebgpu.dylib') || libraryPath.endsWith('libwebgpu_dawn.so') || libraryPath.endsWith('libwgpu_native.so') || libraryPath.endsWith('libwgpu_native.so.0')) {
64
- return 'delegate';
65
- }
66
- return 'unknown';
67
- }
68
-
69
112
  function ensureLibrary() {
70
113
  if (libraryLoaded) return;
71
114
  if (!addon) {
@@ -82,524 +125,434 @@ function ensureLibrary() {
82
125
  libraryLoaded = true;
83
126
  }
84
127
 
85
- /**
86
- * Standard WebGPU enum objects exposed by the Doe package runtime.
87
- *
88
- * This is a package-local copy of the enum tables commonly needed by Node and
89
- * Bun callers that want WebGPU constants without relying on browser globals.
90
- *
91
- * This example shows the API in its basic form.
92
- *
93
- * ```js
94
- * import { globals } from "@simulatte/webgpu";
95
- *
96
- * const usage = globals.GPUBufferUsage.STORAGE | globals.GPUBufferUsage.COPY_DST;
97
- * ```
98
- *
99
- * - These values mirror the standard WebGPU numeric constants.
100
- * - They do not install themselves on `globalThis`; use `setupGlobals(...)` if needed.
101
- * - `@simulatte/webgpu/compute` shares the same constants even though its device facade is narrower.
102
- */
103
- export const globals = {
104
- GPUBufferUsage: {
105
- MAP_READ: 0x0001,
106
- MAP_WRITE: 0x0002,
107
- COPY_SRC: 0x0004,
108
- COPY_DST: 0x0008,
109
- INDEX: 0x0010,
110
- VERTEX: 0x0020,
111
- UNIFORM: 0x0040,
112
- STORAGE: 0x0080,
113
- INDIRECT: 0x0100,
114
- QUERY_RESOLVE: 0x0200,
115
- },
116
- GPUShaderStage: {
117
- VERTEX: 0x1,
118
- FRAGMENT: 0x2,
119
- COMPUTE: 0x4,
120
- },
121
- GPUMapMode: {
122
- READ: 0x0001,
123
- WRITE: 0x0002,
124
- },
125
- GPUTextureUsage: {
126
- COPY_SRC: 0x01,
127
- COPY_DST: 0x02,
128
- TEXTURE_BINDING: 0x04,
129
- STORAGE_BINDING: 0x08,
130
- RENDER_ATTACHMENT: 0x10,
131
- },
132
- };
128
+ function validateBufferDescriptor(descriptor) {
129
+ return assertBufferDescriptor(descriptor, 'GPUDevice.createBuffer');
130
+ }
133
131
 
134
132
  /**
135
- * WebGPU buffer returned by the Doe full package surface.
136
- *
137
- * Instances come from `device.createBuffer(...)` and expose buffer metadata,
138
- * mapping, and destruction operations for headless workflows.
139
- *
140
- * This example shows the API in its basic form.
141
- *
142
- * ```js
143
- * const buffer = device.createBuffer({
144
- * size: 16,
145
- * usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
146
- * });
147
- * ```
148
- *
149
- * - `size` and `usage` are copied onto the JS object for convenience.
150
- * - Destroying the buffer releases the native handle but does not remove the JS object itself.
133
+ * Read structured error fields from the native N-API addon's last-error ABI.
134
+ * Uses `addon.getLastErrorLine` / `addon.getLastErrorColumn` when available
135
+ * (requires native build that exports `doeNativeGetLastErrorLine/Column`).
136
+ * Returns null when the addon does not expose these functions.
151
137
  */
152
- class DoeGPUBuffer {
153
- constructor(native, instance, size, usage, queue) {
154
- this._native = native;
155
- this._instance = instance;
156
- this._queue = queue;
157
- this.size = size;
158
- this.usage = usage;
159
- }
138
+ function readLastErrorFields() {
139
+ if (typeof addon?.getLastErrorStage !== 'function' && typeof addon?.getLastErrorKind !== 'function') {
140
+ return null;
141
+ }
142
+ const stage = typeof addon?.getLastErrorStage === 'function' ? (addon.getLastErrorStage() ?? '') : '';
143
+ const kind = typeof addon?.getLastErrorKind === 'function' ? (addon.getLastErrorKind() ?? '') : '';
144
+ const line = typeof addon?.getLastErrorLine === 'function' ? Number(addon.getLastErrorLine()) : 0;
145
+ const column = typeof addon?.getLastErrorColumn === 'function' ? Number(addon.getLastErrorColumn()) : 0;
146
+ return {
147
+ stage: stage || undefined,
148
+ kind: kind || undefined,
149
+ line: line > 0 ? line : undefined,
150
+ column: column > 0 ? column : undefined,
151
+ };
152
+ }
160
153
 
161
- /**
162
- * Map the buffer for host access.
163
- *
164
- * This resolves after Doe has flushed any pending queue work needed to make
165
- * the requested range readable or writable from JS.
166
- *
167
- * This example shows the API in its basic form.
168
- *
169
- * ```js
170
- * await buffer.mapAsync(GPUMapMode.READ);
171
- * ```
172
- *
173
- * - When `size` is omitted, Doe maps the remaining bytes from `offset` to the end of the buffer.
174
- * - When the queue still has pending submissions, Doe flushes them before mapping.
175
- */
176
- async mapAsync(mode, offset = 0, size = Math.max(0, this.size - offset)) {
177
- if (this._queue) {
178
- if (this._queue.hasPendingSubmissions()) {
179
- addon.flushAndMapSync(this._instance, this._queue._native, this._native, mode, offset, size);
180
- this._queue.markSubmittedWorkDone();
181
- } else {
182
- addon.bufferMapSync(this._instance, this._native, mode, offset, size);
183
- }
184
- } else {
185
- addon.bufferMapSync(this._instance, this._native, mode, offset, size);
186
- }
154
+ function adapterLimits(native) {
155
+ if (typeof addon?.adapterGetLimits !== 'function') {
156
+ return publishLimits(null);
187
157
  }
158
+ return publishLimits(addon.adapterGetLimits(native));
159
+ }
188
160
 
189
- /**
190
- * Return the currently mapped byte range.
191
- *
192
- * This exposes the mapped bytes as an `ArrayBuffer`-backed view after a
193
- * successful `mapAsync(...)` call.
194
- *
195
- * This example shows the API in its basic form.
196
- *
197
- * ```js
198
- * const bytes = buffer.getMappedRange();
199
- * ```
200
- *
201
- * - Call this only while the buffer is mapped.
202
- * - When `size` is omitted, Doe returns the remaining bytes from `offset` to the end of the buffer.
203
- */
204
- getMappedRange(offset = 0, size = Math.max(0, this.size - offset)) {
205
- return addon.bufferGetMappedRange(this._native, offset, size);
161
+ function deviceLimits(native) {
162
+ if (typeof addon?.deviceGetLimits !== 'function') {
163
+ return publishLimits(null);
206
164
  }
165
+ return publishLimits(addon.deviceGetLimits(native));
166
+ }
207
167
 
208
- /**
209
- * Compare a mapped `f32` prefix against expected values.
210
- *
211
- * This is a small assertion helper used by smoke tests and quick validation
212
- * flows after mapping a buffer for read.
213
- *
214
- * This example shows the API in its basic form.
215
- *
216
- * ```js
217
- * buffer.assertMappedPrefixF32([1, 2, 3, 4], 4);
218
- * ```
219
- *
220
- * - The buffer must already be mapped.
221
- * - This checks only the requested prefix rather than the whole buffer.
222
- */
223
- assertMappedPrefixF32(expected, count) {
224
- return addon.bufferAssertMappedPrefixF32(this._native, expected, count);
225
- }
168
+ function adapterFeatures(native) {
169
+ return publishFeatures(
170
+ typeof addon?.adapterHasFeature === 'function'
171
+ ? (feature) => addon.adapterHasFeature(native, feature)
172
+ : null,
173
+ );
174
+ }
226
175
 
227
- /**
228
- * Release the current mapping.
229
- *
230
- * This returns the buffer to normal GPU ownership after `mapAsync(...)`.
231
- *
232
- * This example shows the API in its basic form.
233
- *
234
- * ```js
235
- * buffer.unmap();
236
- * ```
237
- *
238
- * - Call this after reading or writing mapped bytes.
239
- * - `getMappedRange(...)` is not valid again until the buffer is remapped.
240
- */
241
- unmap() {
242
- addon.bufferUnmap(this._native);
176
+ function deviceFeatures(native) {
177
+ return publishFeatures(
178
+ typeof addon?.deviceHasFeature === 'function'
179
+ ? (feature) => addon.deviceHasFeature(native, feature)
180
+ : null,
181
+ );
182
+ }
183
+
184
+ function preflightShaderSource(code) {
185
+ ensureLibrary();
186
+ if (typeof addon?.checkShaderSource === 'function') {
187
+ const result = addon.checkShaderSource(code);
188
+ if (result && typeof result === 'object') {
189
+ const out = {
190
+ ok: result.ok !== false,
191
+ stage: result.stage ?? '',
192
+ kind: result.kind ?? '',
193
+ message: result.message ?? '',
194
+ reasons: result.ok === false && result.message ? [result.message] : [],
195
+ };
196
+ if (typeof result.line === 'number' && result.line > 0) out.line = result.line;
197
+ if (typeof result.column === 'number' && result.column > 0) out.column = result.column;
198
+ return out;
199
+ }
243
200
  }
201
+ return { ok: true, stage: '', kind: '', message: '', reasons: [] };
202
+ }
244
203
 
245
- /**
246
- * Release the native buffer.
247
- *
248
- * This tears down the underlying Doe buffer and marks the JS wrapper as
249
- * released.
250
- *
251
- * This example shows the API in its basic form.
252
- *
253
- * ```js
254
- * buffer.destroy();
255
- * ```
256
- *
257
- * - Reusing a destroyed buffer is unsupported.
258
- * - The wrapper remains reachable in JS but no longer owns a live native handle.
259
- */
260
- destroy() {
261
- addon.bufferRelease(this._native);
262
- this._native = null;
204
+ function requireAutoLayoutEntriesFromNative(shaderNative, visibility, path) {
205
+ if (typeof addon?.shaderModuleGetBindings !== 'function') {
206
+ failValidation(
207
+ path,
208
+ 'layout: "auto" requires native shader binding metadata on this package surface'
209
+ );
263
210
  }
211
+ const bindings = addon.shaderModuleGetBindings(shaderNative);
212
+ if (!Array.isArray(bindings)) {
213
+ failValidation(
214
+ path,
215
+ 'layout: "auto" could not read native shader binding metadata'
216
+ );
217
+ }
218
+ return autoLayoutEntriesFromNativeBindings(bindings, visibility);
264
219
  }
265
220
 
221
+
266
222
  /**
267
- * Compute pass encoder returned by `commandEncoder.beginComputePass(...)`.
223
+ * Standard WebGPU enum objects exposed by the Doe package runtime.
268
224
  *
269
- * This records a compute pass on the full package surface.
225
+ * These package-local shared enum tables are commonly needed by Node and Bun
226
+ * callers that want WebGPU constants without relying on browser globals.
270
227
  *
271
228
  * This example shows the API in its basic form.
272
229
  *
273
230
  * ```js
274
- * const pass = encoder.beginComputePass();
275
- * pass.setPipeline(pipeline);
231
+ * import { globals } from "@simulatte/webgpu";
232
+ *
233
+ * const usage = globals.GPUBufferUsage.STORAGE | globals.GPUBufferUsage.COPY_DST;
276
234
  * ```
277
235
  *
278
- * - Dispatches may be batched until the command encoder is finalized.
279
- * - The encoder only supports the compute commands exposed by Doe here.
236
+ * - These values mirror the standard WebGPU numeric constants.
237
+ * - They do not install themselves on `globalThis`; use `setupGlobals(...)` if needed.
238
+ * - `@simulatte/webgpu/compute` shares the same constants even though its device facade is narrower.
280
239
  */
281
- class DoeGPUComputePassEncoder {
282
- constructor(encoder) {
283
- this._encoder = encoder;
284
- this._pipeline = null;
285
- this._bindGroups = [];
286
- }
287
-
288
- /**
289
- * Set the compute pipeline used by later dispatch calls.
290
- *
291
- * This stores the pipeline handle on the pass so later dispatches use the
292
- * expected compiled shader and layout.
293
- *
294
- * This example shows the API in its basic form.
295
- *
296
- * ```js
297
- * pass.setPipeline(pipeline);
298
- * ```
299
- *
300
- * - Call this before dispatching workgroups.
301
- * - The pipeline object must come from the same device.
302
- */
303
- setPipeline(pipeline) { this._pipeline = pipeline._native; }
304
-
305
- /**
306
- * Bind a bind group for the compute pass.
307
- *
308
- * This records the resource bindings that the next dispatches should see.
309
- *
310
- * This example shows the API in its basic form.
311
- *
312
- * ```js
313
- * pass.setBindGroup(0, bindGroup);
314
- * ```
315
- *
316
- * - Later calls for the same index replace the previous bind group.
317
- * - Sparse indices are allowed, but the shader layout still has to match.
318
- */
319
- setBindGroup(index, bindGroup) { this._bindGroups[index] = bindGroup._native; }
320
-
321
- /**
322
- * Record a direct compute dispatch.
323
- *
324
- * This queues an explicit workgroup dispatch on the current pass.
325
- *
326
- * This example shows the API in its basic form.
327
- *
328
- * ```js
329
- * pass.dispatchWorkgroups(4, 1, 1);
330
- * ```
331
- *
332
- * - Omitted `y` and `z` default to `1`.
333
- * - The pipeline and required bind groups should already be set.
334
- */
335
- dispatchWorkgroups(x, y = 1, z = 1) {
336
- this._encoder._commands.push({
337
- t: 0, p: this._pipeline, bg: [...this._bindGroups], x, y, z,
338
- });
339
- }
340
-
341
- /**
342
- * Dispatch compute workgroups using counts stored in a buffer.
343
- *
344
- * This switches to the native encoder path and forwards the indirect dispatch
345
- * parameters from the supplied buffer.
346
- *
347
- * This example shows the API in its basic form.
348
- *
349
- * ```js
350
- * pass.dispatchWorkgroupsIndirect(indirectBuffer, 0);
351
- * ```
352
- *
353
- * - This forces the command encoder to materialize a native encoder immediately.
354
- * - The indirect buffer must contain the expected dispatch layout.
355
- */
356
- dispatchWorkgroupsIndirect(indirectBuffer, indirectOffset = 0) {
357
- this._encoder._ensureNative();
358
- const pass = addon.beginComputePass(this._encoder._native);
359
- addon.computePassSetPipeline(pass, this._pipeline);
360
- for (let i = 0; i < this._bindGroups.length; i++) {
361
- if (this._bindGroups[i]) addon.computePassSetBindGroup(pass, i, this._bindGroups[i]);
362
- }
363
- addon.computePassDispatchWorkgroupsIndirect(pass, indirectBuffer._native, indirectOffset);
364
- addon.computePassEnd(pass);
365
- addon.computePassRelease(pass);
366
- }
367
-
368
- /**
369
- * Finish the compute pass.
370
- *
371
- * This closes the pass so the surrounding command encoder can continue or
372
- * be finalized.
373
- *
374
- * This example shows the API in its basic form.
375
- *
376
- * ```js
377
- * pass.end();
378
- * ```
379
- *
380
- * - Doe records most work on the surrounding command encoder, so this is lightweight.
381
- * - Finishing the pass does not submit it; submit the finished command buffer on the queue.
382
- */
383
- end() {}
384
- }
385
-
386
240
  /**
387
- * Command encoder returned by `device.createCommandEncoder(...)`.
241
+ * Compute pass encoder returned by `commandEncoder.beginComputePass(...)`.
388
242
  *
389
- * This records compute, render, and buffer-copy commands before they are
390
- * turned into a command buffer for queue submission.
243
+ * This records a compute pass on the full package surface.
391
244
  *
392
245
  * This example shows the API in its basic form.
393
246
  *
394
247
  * ```js
395
- * const encoder = device.createCommandEncoder();
248
+ * const pass = encoder.beginComputePass();
249
+ * pass.setPipeline(pipeline);
396
250
  * ```
397
251
  *
398
- * - Doe may batch simple command sequences before a native encoder is required.
399
- * - Submission still happens through `device.queue.submit(...)`.
252
+ * - Dispatches may be batched until the command encoder is finalized.
253
+ * - The encoder only supports the compute commands exposed by Doe here.
400
254
  */
401
- class DoeGPUCommandEncoder {
402
- constructor(device) {
403
- this._device = device;
404
- this._commands = [];
405
- this._native = null;
406
- }
407
-
408
- _ensureNative() {
409
- if (this._native) return;
410
- this._native = addon.createCommandEncoder(this._device);
411
- for (const cmd of this._commands) {
412
- if (cmd.t === 0) {
413
- const pass = addon.beginComputePass(this._native);
414
- addon.computePassSetPipeline(pass, cmd.p);
415
- for (let i = 0; i < cmd.bg.length; i++) {
416
- if (cmd.bg[i]) addon.computePassSetBindGroup(pass, i, cmd.bg[i]);
255
+ function ensureNodeCommandEncoderNative(encoder) {
256
+ encoder._assertOpen('GPUCommandEncoder');
257
+ if (encoder._native) {
258
+ return;
259
+ }
260
+ encoder._native = addon.createCommandEncoder(assertLiveResource(encoder._device, 'GPUCommandEncoder', 'GPUDevice'));
261
+ for (const cmd of encoder._commands) {
262
+ if (cmd.t === 0) {
263
+ const pass = addon.beginComputePass(encoder._native);
264
+ addon.computePassSetPipeline(pass, cmd.p);
265
+ for (let index = 0; index < cmd.bg.length; index += 1) {
266
+ if (cmd.bg[index]) {
267
+ addon.computePassSetBindGroup(pass, index, cmd.bg[index]);
417
268
  }
418
- addon.computePassDispatchWorkgroups(pass, cmd.x, cmd.y, cmd.z);
419
- addon.computePassEnd(pass);
420
- addon.computePassRelease(pass);
421
- } else if (cmd.t === 1) {
422
- addon.commandEncoderCopyBufferToBuffer(this._native, cmd.s, cmd.so, cmd.d, cmd.do, cmd.sz);
423
269
  }
270
+ addon.computePassDispatchWorkgroups(pass, cmd.x, cmd.y, cmd.z);
271
+ addon.computePassEnd(pass);
272
+ addon.computePassRelease(pass);
273
+ } else if (cmd.t === 1) {
274
+ addon.commandEncoderCopyBufferToBuffer(encoder._native, cmd.s, cmd.so, cmd.d, cmd.do, cmd.sz);
424
275
  }
425
- this._commands = [];
426
- }
427
-
428
- /**
429
- * Begin a compute pass.
430
- *
431
- * This creates a pass encoder that records compute state and dispatches on
432
- * this command encoder.
433
- *
434
- * This example shows the API in its basic form.
435
- *
436
- * ```js
437
- * const pass = encoder.beginComputePass();
438
- * ```
439
- *
440
- * - The descriptor is accepted for WebGPU shape compatibility.
441
- * - The returned pass is valid until `pass.end()`.
442
- */
443
- beginComputePass(descriptor) {
444
- return new DoeGPUComputePassEncoder(this);
445
- }
446
-
447
- /**
448
- * Begin a render pass.
449
- *
450
- * This starts a headless render pass with the provided attachments on the
451
- * underlying native command encoder.
452
- *
453
- * This example shows the API in its basic form.
454
- *
455
- * ```js
456
- * const pass = encoder.beginRenderPass({
457
- * colorAttachments: [{ view }],
458
- * });
459
- * ```
460
- *
461
- * - Doe materializes the native encoder before starting the render pass.
462
- * - Color attachments default their clear color when one is not provided.
463
- */
464
- beginRenderPass(descriptor) {
465
- this._ensureNative();
466
- const colorAttachments = (descriptor.colorAttachments || []).map((a) => ({
467
- view: a.view._native,
468
- clearValue: a.clearValue || { r: 0, g: 0, b: 0, a: 1 },
469
- }));
470
- const pass = addon.beginRenderPass(this._native, colorAttachments);
471
- return new DoeGPURenderPassEncoder(pass);
472
276
  }
277
+ encoder._commands = [];
278
+ }
473
279
 
474
- /**
475
- * Record a buffer-to-buffer copy.
476
- *
477
- * This schedules a transfer from one buffer range into another on the
478
- * command encoder.
479
- *
480
- * This example shows the API in its basic form.
481
- *
482
- * ```js
483
- * encoder.copyBufferToBuffer(src, 0, dst, 0, src.size);
484
- * ```
485
- *
486
- * - Copies can be batched until the encoder is finalized.
487
- * - Buffer ranges still need to be valid for the underlying WebGPU rules.
488
- */
489
- copyBufferToBuffer(src, srcOffset, dst, dstOffset, size) {
490
- if (this._native) {
491
- addon.commandEncoderCopyBufferToBuffer(this._native, src._native, srcOffset, dst._native, dstOffset, size);
492
- } else {
493
- this._commands.push({ t: 1, s: src._native, so: srcOffset, d: dst._native, do: dstOffset, sz: size });
280
+ const nodeEncoderBackend = {
281
+ computePassInit(pass) {
282
+ pass._pipeline = null;
283
+ pass._bindGroups = [];
284
+ pass._ended = false;
285
+ },
286
+ computePassAssertOpen(pass, path) {
287
+ if (pass._ended) {
288
+ failValidation(path, 'compute pass is already ended');
494
289
  }
495
- }
496
-
497
- /**
498
- * Finish command recording and return a command buffer.
499
- *
500
- * This seals the recorded commands so they can be submitted on a queue.
501
- *
502
- * This example shows the API in its basic form.
503
- *
504
- * ```js
505
- * const commands = encoder.finish();
506
- * device.queue.submit([commands]);
507
- * ```
508
- *
509
- * - Doe may return a lightweight batched command buffer representation.
510
- * - The returned object is meant for queue submission, not direct inspection.
511
- */
512
- finish() {
513
- if (this._native) {
514
- const cmd = addon.commandEncoderFinish(this._native);
515
- return { _native: cmd, _batched: false };
290
+ if (pass._encoder._finished) {
291
+ failValidation(path, 'command encoder is already finished');
516
292
  }
517
- return { _commands: this._commands, _batched: true };
518
- }
519
- }
293
+ },
294
+ computePassSetPipeline(pass, pipelineNative) {
295
+ pass._pipeline = pipelineNative;
296
+ },
297
+ computePassSetBindGroup(pass, index, bindGroupNative) {
298
+ pass._bindGroups[index] = bindGroupNative;
299
+ },
300
+ computePassDispatchWorkgroups(pass, x, y, z) {
301
+ if (pass._pipeline == null) {
302
+ failValidation('GPUComputePassEncoder.dispatchWorkgroups', 'setPipeline() must be called before dispatch');
303
+ }
304
+ pass._encoder._commands.push({ t: 0, p: pass._pipeline, bg: [...pass._bindGroups], x, y, z });
305
+ },
306
+ computePassDispatchWorkgroupsIndirect(pass, indirectBufferNative, indirectOffset) {
307
+ if (pass._pipeline == null) {
308
+ failValidation('GPUComputePassEncoder.dispatchWorkgroupsIndirect', 'setPipeline() must be called before dispatch');
309
+ }
310
+ if (typeof addon.bufferReadIndirectCounts === 'function') {
311
+ const counts = addon.bufferReadIndirectCounts(indirectBufferNative, indirectOffset);
312
+ pass._encoder._commands.push({
313
+ t: 0,
314
+ p: pass._pipeline,
315
+ bg: [...pass._bindGroups],
316
+ x: counts.x,
317
+ y: counts.y,
318
+ z: counts.z,
319
+ });
320
+ return;
321
+ }
322
+ ensureNodeCommandEncoderNative(pass._encoder);
323
+ const nativePass = addon.beginComputePass(pass._encoder._native);
324
+ addon.computePassSetPipeline(nativePass, pass._pipeline);
325
+ for (let index = 0; index < pass._bindGroups.length; index += 1) {
326
+ if (pass._bindGroups[index]) {
327
+ addon.computePassSetBindGroup(nativePass, index, pass._bindGroups[index]);
328
+ }
329
+ }
330
+ addon.computePassDispatchWorkgroupsIndirect(nativePass, indirectBufferNative, indirectOffset);
331
+ addon.computePassEnd(nativePass);
332
+ addon.computePassRelease(nativePass);
333
+ },
334
+ computePassEnd(pass) {
335
+ pass._ended = true;
336
+ },
337
+ renderPassInit(pass, native) {
338
+ pass._native = native;
339
+ pass._ended = false;
340
+ },
341
+ renderPassAssertOpen(pass, path) {
342
+ if (pass._ended) {
343
+ failValidation(path, 'render pass is already ended');
344
+ }
345
+ if (pass._encoder._finished) {
346
+ failValidation(path, 'command encoder is already finished');
347
+ }
348
+ },
349
+ renderPassSetPipeline(pass, pipelineNative) {
350
+ addon.renderPassSetPipeline(
351
+ assertLiveResource(pass, 'GPURenderPassEncoder.setPipeline', 'GPURenderPassEncoder'),
352
+ pipelineNative,
353
+ );
354
+ },
355
+ renderPassSetBindGroup(pass, index, bindGroupNative) {
356
+ addon.renderPassSetBindGroup(
357
+ assertLiveResource(pass, 'GPURenderPassEncoder.setBindGroup', 'GPURenderPassEncoder'),
358
+ index,
359
+ bindGroupNative,
360
+ );
361
+ },
362
+ renderPassSetVertexBuffer(pass, slot, bufferNative, offset, size) {
363
+ addon.renderPassSetVertexBuffer(
364
+ assertLiveResource(pass, 'GPURenderPassEncoder.setVertexBuffer', 'GPURenderPassEncoder'),
365
+ slot,
366
+ bufferNative,
367
+ offset,
368
+ size ?? 0,
369
+ );
370
+ },
371
+ renderPassSetIndexBuffer(pass, bufferNative, format, offset, size) {
372
+ addon.renderPassSetIndexBuffer(
373
+ assertLiveResource(pass, 'GPURenderPassEncoder.setIndexBuffer', 'GPURenderPassEncoder'),
374
+ bufferNative,
375
+ format,
376
+ offset,
377
+ size ?? 0,
378
+ );
379
+ },
380
+ renderPassDraw(pass, vertexCount, instanceCount, firstVertex, firstInstance) {
381
+ addon.renderPassDraw(pass._native, vertexCount, instanceCount, firstVertex, firstInstance);
382
+ },
383
+ renderPassDrawIndexed(pass, indexCount, instanceCount, firstIndex, baseVertex, firstInstance) {
384
+ addon.renderPassDrawIndexed(pass._native, indexCount, instanceCount, firstIndex, baseVertex, firstInstance);
385
+ },
386
+ renderPassEnd(pass) {
387
+ addon.renderPassEnd(pass._native);
388
+ pass._ended = true;
389
+ },
390
+ commandEncoderInit(encoder) {
391
+ encoder._commands = [];
392
+ encoder._native = null;
393
+ encoder._finished = false;
394
+ },
395
+ commandEncoderAssertOpen(encoder, path) {
396
+ if (encoder._finished) {
397
+ failValidation(path, 'command encoder is already finished');
398
+ }
399
+ },
400
+ commandEncoderBeginComputePass(encoder, _descriptor, classes) {
401
+ return new classes.DoeGPUComputePassEncoder(null, encoder);
402
+ },
403
+ commandEncoderBeginRenderPass(encoder, passDescriptor, classes) {
404
+ const attachments = assertArray(passDescriptor.colorAttachments ?? [], 'GPUCommandEncoder.beginRenderPass', 'descriptor.colorAttachments');
405
+ if (attachments.length === 0) {
406
+ failValidation('GPUCommandEncoder.beginRenderPass', 'descriptor.colorAttachments must contain at least one attachment');
407
+ }
408
+ ensureNodeCommandEncoderNative(encoder);
409
+ const colorAttachments = attachments.map((attachment, index) => {
410
+ const entry = assertObject(attachment, 'GPUCommandEncoder.beginRenderPass', `descriptor.colorAttachments[${index}]`);
411
+ return {
412
+ view: assertLiveResource(entry.view, 'GPUCommandEncoder.beginRenderPass', 'GPUTextureView'),
413
+ clearValue: entry.clearValue || { r: 0, g: 0, b: 0, a: 1 },
414
+ };
415
+ });
416
+ let depthStencilAttachment = undefined;
417
+ if (passDescriptor.depthStencilAttachment !== undefined) {
418
+ const depthAttachment = assertObject(passDescriptor.depthStencilAttachment, 'GPUCommandEncoder.beginRenderPass', 'descriptor.depthStencilAttachment');
419
+ depthStencilAttachment = {
420
+ view: assertLiveResource(depthAttachment.view, 'GPUCommandEncoder.beginRenderPass', 'GPUTextureView'),
421
+ depthClearValue: depthAttachment.depthClearValue ?? 1,
422
+ depthReadOnly: depthAttachment.depthReadOnly ?? false,
423
+ stencilClearValue: depthAttachment.stencilClearValue ?? 0,
424
+ stencilReadOnly: depthAttachment.stencilReadOnly ?? false,
425
+ };
426
+ }
427
+ const pass = addon.beginRenderPass(encoder._native, {
428
+ colorAttachments,
429
+ depthStencilAttachment,
430
+ });
431
+ return new classes.DoeGPURenderPassEncoder(pass, encoder);
432
+ },
433
+ commandEncoderCopyBufferToBuffer(encoder, srcNative, srcOffset, dstNative, dstOffset, size) {
434
+ if (encoder._native) {
435
+ addon.commandEncoderCopyBufferToBuffer(encoder._native, srcNative, srcOffset, dstNative, dstOffset, size);
436
+ return;
437
+ }
438
+ encoder._commands.push({ t: 1, s: srcNative, so: srcOffset, d: dstNative, do: dstOffset, sz: size });
439
+ },
440
+ commandEncoderWriteTimestamp(encoder, querySetNative, queryIndex) {
441
+ ensureNodeCommandEncoderNative(encoder);
442
+ addon.commandEncoderWriteTimestamp(encoder._native, querySetNative, queryIndex);
443
+ },
444
+ commandEncoderResolveQuerySet(encoder, querySetNative, firstQuery, queryCount, destinationNative, destinationOffset) {
445
+ ensureNodeCommandEncoderNative(encoder);
446
+ addon.commandEncoderResolveQuerySet(encoder._native, querySetNative, firstQuery, queryCount, destinationNative, destinationOffset);
447
+ },
448
+ commandEncoderCopyTextureToBuffer(encoder, source, destination, copySize) {
449
+ ensureNodeCommandEncoderNative(encoder);
450
+ addon.commandEncoderCopyTextureToBuffer(
451
+ encoder._native,
452
+ source.texture,
453
+ source.mipLevel ?? 0,
454
+ source.origin?.x ?? 0,
455
+ source.origin?.y ?? 0,
456
+ source.origin?.z ?? 0,
457
+ source.aspect ?? 1,
458
+ destination.buffer,
459
+ destination.offset ?? 0,
460
+ destination.bytesPerRow ?? 0,
461
+ destination.rowsPerImage ?? 0,
462
+ copySize.width,
463
+ copySize.height,
464
+ copySize.depthOrArrayLayers ?? 1,
465
+ );
466
+ },
467
+ commandEncoderFinish(encoder) {
468
+ ensureNodeCommandEncoderNative(encoder);
469
+ encoder._finished = true;
470
+ const cmd = addon.commandEncoderFinish(encoder._native);
471
+ encoder._native = null;
472
+ return { _native: cmd, _batched: false };
473
+ },
474
+ };
475
+
476
+ const {
477
+ DoeGPUComputePassEncoder,
478
+ DoeGPUCommandEncoder,
479
+ DoeGPURenderPassEncoder,
480
+ } = createEncoderClasses(nodeEncoderBackend);
520
481
 
521
482
  /**
522
- * Queue exposed on `device.queue`.
483
+ * Texture returned by `device.createTexture(...)`.
523
484
  *
524
- * This submits finished command buffers, uploads host data into buffers, and
525
- * lets callers wait for queued work to drain.
485
+ * This represents a headless Doe texture resource and can create default views
486
+ * for render or sampling usage.
526
487
  *
527
488
  * This example shows the API in its basic form.
528
489
  *
529
490
  * ```js
530
- * device.queue.submit([encoder.finish()]);
491
+ * const texture = device.createTexture({
492
+ * size: [64, 64, 1],
493
+ * format: "rgba8unorm",
494
+ * usage: GPUTextureUsage.RENDER_ATTACHMENT,
495
+ * });
531
496
  * ```
532
497
  *
533
- * - Queue writes and submissions stay package-local and headless.
534
- * - The queue also tracks lightweight submission state used by Doe's sync mapping path.
498
+ * - The package currently exposes the texture operations needed by its headless surface.
499
+ * - Texture views are created through `createView(...)`.
535
500
  */
536
- class DoeGPUQueue {
537
- constructor(native, instance, device) {
538
- this._native = native;
539
- this._instance = instance;
540
- this._device = device;
541
- this._submittedSerial = 0;
542
- this._completedSerial = 0;
543
- }
544
-
545
- /**
546
- * Report whether this queue still has unflushed submitted work.
547
- *
548
- * This exposes Doe's lightweight submission bookkeeping for callers that
549
- * need to understand queue state.
550
- *
551
- * This example shows the API in its basic form.
552
- *
553
- * ```js
554
- * const busy = device.queue.hasPendingSubmissions();
555
- * ```
556
- *
557
- * - This is a Doe queue-state helper rather than a standard WebGPU method.
558
- * - It reflects Doe's tracked submission serials, not a browser event model.
559
- */
560
- hasPendingSubmissions() {
561
- return this._completedSerial < this._submittedSerial;
562
- }
563
-
564
- /**
565
- * Mark the current tracked submissions as completed.
566
- *
567
- * This updates Doe's internal queue bookkeeping without waiting on any
568
- * external event source.
569
- *
570
- * This example shows the API in its basic form.
571
- *
572
- * ```js
573
- * device.queue.markSubmittedWorkDone();
574
- * ```
575
- *
576
- * - This is primarily useful for Doe's own queue bookkeeping.
577
- * - Most callers should prefer `await queue.onSubmittedWorkDone()`.
578
- */
579
- markSubmittedWorkDone() {
580
- this._completedSerial = this._submittedSerial;
581
- }
582
-
583
- /**
584
- * Submit command buffers to the queue.
585
- *
586
- * This forwards one or more finished command buffers to the Doe queue for
587
- * execution.
588
- *
589
- * This example shows the API in its basic form.
590
- *
591
- * ```js
592
- * device.queue.submit([encoder.finish()]);
593
- * ```
594
- *
595
- * - Empty submissions are ignored.
596
- * - Simple batched compute-copy sequences may take a Doe fast path.
597
- */
598
- submit(commandBuffers) {
599
- if (commandBuffers.length === 0) return;
600
- this._submittedSerial += 1;
601
- if (commandBuffers.length === 1 && commandBuffers[0]?._batched) {
602
- const cmds = commandBuffers[0]._commands;
501
+ const fullSurfaceBackend = {
502
+ initBufferState(buffer) {
503
+ buffer._mapMode = 0;
504
+ },
505
+ bufferMarkMappedAtCreation(buffer) {
506
+ buffer._mapMode = globals.GPUMapMode.WRITE;
507
+ },
508
+ bufferMapAsync(wrapper, native, mode, offset, size) {
509
+ if (wrapper._queue) {
510
+ if (wrapper._queue.hasPendingSubmissions()) {
511
+ addon.flushAndMapSync(
512
+ wrapper._instance,
513
+ assertLiveResource(wrapper._queue, 'GPUBuffer.mapAsync', 'GPUQueue'),
514
+ native,
515
+ mode,
516
+ offset,
517
+ size,
518
+ );
519
+ wrapper._queue.markSubmittedWorkDone();
520
+ } else {
521
+ addon.bufferMapSync(wrapper._instance, native, mode, offset, size);
522
+ }
523
+ } else {
524
+ addon.bufferMapSync(wrapper._instance, native, mode, offset, size);
525
+ }
526
+ wrapper._mapMode = mode;
527
+ },
528
+ bufferGetMappedRange(wrapper, native, offset, size) {
529
+ return addon.bufferGetMappedRange(native, offset, size);
530
+ },
531
+ bufferAssertMappedPrefixF32(_wrapper, native, expected, count) {
532
+ return addon.bufferAssertMappedPrefixF32(native, expected, count);
533
+ },
534
+ bufferUnmap(native, wrapper) {
535
+ wrapper._mapMode = 0;
536
+ addon.bufferUnmap(native);
537
+ },
538
+ bufferDestroy(native) {
539
+ addon.bufferRelease(native);
540
+ },
541
+ initQueueState(queue) {
542
+ queue._submittedSerial = 0;
543
+ queue._completedSerial = 0;
544
+ },
545
+ queueHasPendingSubmissions(queue) {
546
+ return queue._completedSerial < queue._submittedSerial;
547
+ },
548
+ queueMarkSubmittedWorkDone(queue) {
549
+ queue._completedSerial = queue._submittedSerial;
550
+ },
551
+ queueSubmit(queue, queueNative, buffers) {
552
+ const deviceNative = assertLiveResource(queue._device, 'GPUQueue.submit', 'GPUDevice');
553
+ queue._submittedSerial += 1;
554
+ if (buffers.length === 1 && buffers[0]?._batched) {
555
+ const cmds = buffers[0]._commands;
603
556
  if (
604
557
  cmds.length === 2
605
558
  && cmds[0]?.t === 0
@@ -607,8 +560,8 @@ class DoeGPUQueue {
607
560
  && typeof addon.submitComputeDispatchCopy === 'function'
608
561
  ) {
609
562
  addon.submitComputeDispatchCopy(
610
- this._device,
611
- this._native,
563
+ deviceNative,
564
+ queueNative,
612
565
  cmds[0].p,
613
566
  cmds[0].bg,
614
567
  cmds[0].x,
@@ -623,831 +576,253 @@ class DoeGPUQueue {
623
576
  return;
624
577
  }
625
578
  }
626
- if (commandBuffers.length > 0 && commandBuffers.every((c) => c._batched)) {
579
+ if (buffers.every((commandBuffer) => commandBuffer?._batched && Array.isArray(commandBuffer._commands))) {
627
580
  const allCommands = [];
628
- for (const cb of commandBuffers) allCommands.push(...cb._commands);
629
- addon.submitBatched(this._device, this._native, allCommands);
581
+ for (const cb of buffers) {
582
+ allCommands.push(...cb._commands);
583
+ }
584
+ addon.submitBatched(deviceNative, queueNative, allCommands);
630
585
  if (
631
586
  allCommands.length === 2
632
587
  && allCommands[0]?.t === 0
633
588
  && allCommands[1]?.t === 1
634
589
  ) {
635
- this.markSubmittedWorkDone();
590
+ queue.markSubmittedWorkDone();
636
591
  }
637
- } else {
638
- const natives = commandBuffers.map((c) => c._native);
639
- addon.queueSubmit(this._native, natives);
592
+ return;
640
593
  }
641
- }
642
-
643
- /**
644
- * Write host data into a GPU buffer.
645
- *
646
- * This copies bytes from JS-owned memory into the destination GPU buffer
647
- * range on the queue.
648
- *
649
- * This example shows the API in its basic form.
650
- *
651
- * ```js
652
- * device.queue.writeBuffer(buffer, 0, new Float32Array([1, 2, 3, 4]));
653
- * ```
654
- *
655
- * - `dataOffset` and `size` are interpreted in element units for typed arrays.
656
- * - Doe converts the requested range into bytes before writing it.
657
- */
658
- writeBuffer(buffer, bufferOffset, data, dataOffset = 0, size) {
659
- let view = data;
660
- if (dataOffset > 0 || size !== undefined) {
661
- const byteOffset = data.byteOffset + dataOffset * (data.BYTES_PER_ELEMENT || 1);
662
- const byteLength = size !== undefined
663
- ? size * (data.BYTES_PER_ELEMENT || 1)
664
- : data.byteLength - dataOffset * (data.BYTES_PER_ELEMENT || 1);
665
- view = new Uint8Array(data.buffer, byteOffset, byteLength);
666
- }
667
- addon.queueWriteBuffer(this._native, buffer._native, bufferOffset, view);
668
- }
669
-
670
- /**
671
- * Resolve after submitted work has been flushed.
672
- *
673
- * This gives callers a simple way to wait until Doe has drained the tracked
674
- * queue work relevant to this device.
675
- *
676
- * This example shows the API in its basic form.
677
- *
678
- * ```js
679
- * await device.queue.onSubmittedWorkDone();
680
- * ```
681
- *
682
- * - If no submissions are pending, this resolves immediately.
683
- * - Doe flushes the native queue before marking the tracked work complete.
684
- */
685
- async onSubmittedWorkDone() {
686
- if (!this.hasPendingSubmissions()) return;
594
+ const natives = buffers.map((commandBuffer, index) => {
595
+ if (!commandBuffer || typeof commandBuffer !== 'object' || commandBuffer._native == null) {
596
+ failValidation('GPUQueue.submit', `commandBuffers[${index}] must be a finished command buffer`);
597
+ }
598
+ return commandBuffer._native;
599
+ });
600
+ addon.queueSubmit(queueNative, natives);
601
+ },
602
+ queueWriteBuffer(_queue, queueNative, bufferNative, bufferOffset, view) {
603
+ addon.queueWriteBuffer(queueNative, bufferNative, bufferOffset, view);
604
+ },
605
+ async queueOnSubmittedWorkDone(queue, queueNative) {
687
606
  try {
688
- addon.queueFlush(this._instance, this._native);
607
+ addon.queueFlush(queue._instance, queueNative);
689
608
  } catch (error) {
690
- if (/queueFlush: wgpuInstanceWaitAny failed|queueFlush: doeNativeQueueFlush not available/.test(String(error?.message ?? error))) {
609
+ if (error?.code === 'DOE_QUEUE_UNAVAILABLE') {
691
610
  return;
692
611
  }
693
612
  throw error;
694
613
  }
695
- this.markSubmittedWorkDone();
696
- }
697
- }
698
-
699
- /**
700
- * Render pass encoder returned by `commandEncoder.beginRenderPass(...)`.
701
- *
702
- * This provides the subset of render-pass methods currently surfaced by the
703
- * full headless package.
704
- *
705
- * This example shows the API in its basic form.
706
- *
707
- * ```js
708
- * const pass = encoder.beginRenderPass({ colorAttachments: [{ view }] });
709
- * ```
710
- *
711
- * - The exposed render API is intentionally narrower than a browser implementation.
712
- * - Submission still happens through the command encoder and queue.
713
- */
714
- class DoeGPURenderPassEncoder {
715
- constructor(native) { this._native = native; }
716
-
717
- /**
718
- * Set the render pipeline used by later draw calls.
719
- *
720
- * This records the pipeline state that subsequent draw calls in the pass
721
- * should use.
722
- *
723
- * This example shows the API in its basic form.
724
- *
725
- * ```js
726
- * pass.setPipeline(pipeline);
727
- * ```
728
- *
729
- * - The pipeline must come from the same device.
730
- * - Call this before `draw(...)`.
731
- */
732
- setPipeline(pipeline) {
733
- addon.renderPassSetPipeline(this._native, pipeline._native);
734
- }
735
-
736
- /**
737
- * Record a non-indexed draw.
738
- *
739
- * This queues a draw call using the current render pipeline and bound
740
- * attachments.
741
- *
742
- * This example shows the API in its basic form.
743
- *
744
- * ```js
745
- * pass.draw(3);
746
- * ```
747
- *
748
- * - Omitted instance and offset arguments default to the WebGPU-style values.
749
- * - Draw calls only become visible after the command buffer is submitted.
750
- */
751
- draw(vertexCount, instanceCount = 1, firstVertex = 0, firstInstance = 0) {
752
- addon.renderPassDraw(this._native, vertexCount, instanceCount, firstVertex, firstInstance);
753
- }
754
-
755
- /**
756
- * Finish the render pass.
757
- *
758
- * This closes the native render-pass encoder so the command encoder can
759
- * continue recording.
760
- *
761
- * This example shows the API in its basic form.
762
- *
763
- * ```js
764
- * pass.end();
765
- * ```
766
- *
767
- * - This closes the native render pass encoder.
768
- * - It does not submit work by itself.
769
- */
770
- end() {
771
- addon.renderPassEnd(this._native);
772
- }
773
- }
774
-
775
- /**
776
- * Texture returned by `device.createTexture(...)`.
777
- *
778
- * This represents a headless Doe texture resource and can create default views
779
- * for render or sampling usage.
780
- *
781
- * This example shows the API in its basic form.
782
- *
783
- * ```js
784
- * const texture = device.createTexture({
785
- * size: [64, 64, 1],
786
- * format: "rgba8unorm",
787
- * usage: GPUTextureUsage.RENDER_ATTACHMENT,
788
- * });
789
- * ```
790
- *
791
- * - The package currently exposes the texture operations needed by its headless surface.
792
- * - Texture views are created through `createView(...)`.
793
- */
794
- class DoeGPUTexture {
795
- constructor(native) { this._native = native; }
796
-
797
- /**
798
- * Create a texture view.
799
- *
800
- * This returns a default texture view wrapper for the texture so it can be
801
- * used in render or sampling APIs.
802
- *
803
- * This example shows the API in its basic form.
804
- *
805
- * ```js
806
- * const view = texture.createView();
807
- * ```
808
- *
809
- * - Doe currently ignores most descriptor variation here and creates a default view.
810
- * - The returned view is suitable for the package's headless render paths.
811
- */
812
- createView(descriptor) {
813
- const view = addon.textureCreateView(this._native);
814
- return new DoeGPUTextureView(view);
815
- }
816
-
817
- /**
818
- * Release the native texture.
819
- *
820
- * This tears down the underlying Doe texture allocation associated with the
821
- * wrapper.
822
- *
823
- * This example shows the API in its basic form.
824
- *
825
- * ```js
826
- * texture.destroy();
827
- * ```
828
- *
829
- * - Reusing the texture after destruction is unsupported.
830
- * - Views already created are plain JS wrappers and do not keep the texture alive.
831
- */
832
- destroy() {
833
- addon.textureRelease(this._native);
834
- this._native = null;
835
- }
836
- }
837
-
838
- /**
839
- * Texture view wrapper returned by `texture.createView()`.
840
- *
841
- * This example shows the API in its basic form.
842
- *
843
- * ```js
844
- * const view = texture.createView();
845
- * ```
846
- *
847
- * - This package currently treats the view as a lightweight opaque handle.
848
- */
849
- class DoeGPUTextureView {
850
- constructor(native) { this._native = native; }
851
- }
852
-
853
- /**
854
- * Sampler wrapper returned by `device.createSampler(...)`.
855
- *
856
- * This example shows the API in its basic form.
857
- *
858
- * ```js
859
- * const sampler = device.createSampler();
860
- * ```
861
- *
862
- * - The sampler is currently an opaque handle on the JS side.
863
- */
864
- class DoeGPUSampler {
865
- constructor(native) { this._native = native; }
866
- }
867
-
868
- /**
869
- * Render pipeline returned by `device.createRenderPipeline(...)`.
870
- *
871
- * This example shows the API in its basic form.
872
- *
873
- * ```js
874
- * const pipeline = device.createRenderPipeline(descriptor);
875
- * ```
876
- *
877
- * - The JS wrapper is currently an opaque handle used by render passes.
878
- */
879
- class DoeGPURenderPipeline {
880
- constructor(native) { this._native = native; }
881
- }
882
-
883
- /**
884
- * Shader module returned by `device.createShaderModule(...)`.
885
- *
886
- * This example shows the API in its basic form.
887
- *
888
- * ```js
889
- * const shader = device.createShaderModule({ code: WGSL });
890
- * ```
891
- *
892
- * - Doe keeps the WGSL source on the wrapper for pipeline creation and auto-layout work.
893
- */
894
- class DoeGPUShaderModule {
895
- constructor(native, code) {
896
- this._native = native;
897
- this._code = code;
898
- }
899
- }
900
-
901
- /**
902
- * Compute pipeline returned by `device.createComputePipeline(...)`.
903
- *
904
- * This wrapper exposes pipeline layout lookup for bind-group creation and
905
- * dispatch setup.
906
- *
907
- * This example shows the API in its basic form.
908
- *
909
- * ```js
910
- * const pipeline = device.createComputePipeline({
911
- * layout: "auto",
912
- * compute: { module: shader, entryPoint: "main" },
913
- * });
914
- * ```
915
- *
916
- * - Auto-layout pipelines derive bind-group layouts from the shader source.
917
- * - Explicit-layout pipelines return the layout they were created with.
918
- */
919
- class DoeGPUComputePipeline {
920
- constructor(native, device, explicitLayout, autoLayoutEntriesByGroup) {
921
- this._native = native;
922
- this._device = device;
923
- this._explicitLayout = explicitLayout;
924
- this._autoLayoutEntriesByGroup = autoLayoutEntriesByGroup;
925
- this._cachedLayouts = new Map();
926
- }
927
-
928
- /**
929
- * Return the bind-group layout for a given group index.
930
- *
931
- * This gives callers the layout object needed to construct compatible bind
932
- * groups for the pipeline.
933
- *
934
- * This example shows the API in its basic form.
935
- *
936
- * ```js
937
- * const layout = pipeline.getBindGroupLayout(0);
938
- * ```
939
- *
940
- * - Auto-layout pipelines lazily build and cache layouts by group index.
941
- * - Explicit-layout pipelines return their original layout for any requested index.
942
- */
943
- getBindGroupLayout(index) {
944
- if (this._explicitLayout) return this._explicitLayout;
945
- if (this._cachedLayouts.has(index)) return this._cachedLayouts.get(index);
946
- let layout;
947
- if (this._autoLayoutEntriesByGroup && process.platform === 'darwin') {
948
- const entries = this._autoLayoutEntriesByGroup.get(index) ?? [];
949
- layout = this._device.createBindGroupLayout({ entries });
950
- } else if (typeof addon.computePipelineGetBindGroupLayout === 'function') {
951
- layout = new DoeGPUBindGroupLayout(
952
- addon.computePipelineGetBindGroupLayout(this._native, index),
614
+ },
615
+ textureCreateView(_texture, native) {
616
+ return addon.textureCreateView(native);
617
+ },
618
+ textureDestroy(native) {
619
+ addon.textureRelease(native);
620
+ },
621
+ shaderModuleDestroy(native) {
622
+ addon.shaderModuleRelease(native);
623
+ },
624
+ computePipelineGetBindGroupLayout(pipeline, index, classes) {
625
+ if (pipeline._autoLayoutEntriesByGroup && process.platform === 'darwin') {
626
+ const entries = pipeline._autoLayoutEntriesByGroup.get(index) ?? [];
627
+ return pipeline._device.createBindGroupLayout({ entries });
628
+ }
629
+ if (typeof addon.computePipelineGetBindGroupLayout === 'function') {
630
+ return new classes.DoeGPUBindGroupLayout(
631
+ addon.computePipelineGetBindGroupLayout(pipeline._native, index),
632
+ pipeline._device,
953
633
  );
954
- } else if (this._autoLayoutEntriesByGroup) {
955
- const entries = this._autoLayoutEntriesByGroup.get(index) ?? [];
956
- layout = this._device.createBindGroupLayout({ entries });
957
- } else {
958
- layout = this._device.createBindGroupLayout({ entries: [] });
959
634
  }
960
- this._cachedLayouts.set(index, layout);
961
- return layout;
962
- }
963
- }
964
-
965
- /**
966
- * Bind-group layout returned by `device.createBindGroupLayout(...)`.
967
- *
968
- * This example shows the API in its basic form.
969
- *
970
- * ```js
971
- * const layout = device.createBindGroupLayout({ entries });
972
- * ```
973
- *
974
- * - The JS wrapper is an opaque handle used when creating bind groups and pipelines.
975
- */
976
- class DoeGPUBindGroupLayout {
977
- constructor(native) { this._native = native; }
978
- }
979
-
980
- /**
981
- * Bind group returned by `device.createBindGroup(...)`.
982
- *
983
- * This example shows the API in its basic form.
984
- *
985
- * ```js
986
- * const bindGroup = device.createBindGroup({ layout, entries });
987
- * ```
988
- *
989
- * - The JS wrapper is an opaque handle consumed by pass encoders.
990
- */
991
- class DoeGPUBindGroup {
992
- constructor(native) { this._native = native; }
993
- }
994
-
995
- /**
996
- * Pipeline layout returned by `device.createPipelineLayout(...)`.
997
- *
998
- * This example shows the API in its basic form.
999
- *
1000
- * ```js
1001
- * const layout = device.createPipelineLayout({ bindGroupLayouts: [group0] });
1002
- * ```
1003
- *
1004
- * - The JS wrapper is an opaque handle passed into pipeline creation.
1005
- */
1006
- class DoeGPUPipelineLayout {
1007
- constructor(native) { this._native = native; }
1008
- }
1009
-
1010
- const DOE_LIMITS = Object.freeze({
1011
- maxTextureDimension1D: 16384,
1012
- maxTextureDimension2D: 16384,
1013
- maxTextureDimension3D: 2048,
1014
- maxTextureArrayLayers: 2048,
1015
- maxBindGroups: 4,
1016
- maxBindGroupsPlusVertexBuffers: 24,
1017
- maxBindingsPerBindGroup: 1000,
1018
- maxDynamicUniformBuffersPerPipelineLayout: 8,
1019
- maxDynamicStorageBuffersPerPipelineLayout: 4,
1020
- maxSampledTexturesPerShaderStage: 16,
1021
- maxSamplersPerShaderStage: 16,
1022
- maxStorageBuffersPerShaderStage: 8,
1023
- maxStorageTexturesPerShaderStage: 4,
1024
- maxUniformBuffersPerShaderStage: 12,
1025
- maxUniformBufferBindingSize: 65536,
1026
- maxStorageBufferBindingSize: 134217728,
1027
- minUniformBufferOffsetAlignment: 256,
1028
- minStorageBufferOffsetAlignment: 32,
1029
- maxVertexBuffers: 8,
1030
- maxBufferSize: 268435456,
1031
- maxVertexAttributes: 16,
1032
- maxVertexBufferArrayStride: 2048,
1033
- maxInterStageShaderVariables: 16,
1034
- maxColorAttachments: 8,
1035
- maxColorAttachmentBytesPerSample: 32,
1036
- maxComputeWorkgroupStorageSize: 32768,
1037
- maxComputeInvocationsPerWorkgroup: 1024,
1038
- maxComputeWorkgroupSizeX: 1024,
1039
- maxComputeWorkgroupSizeY: 1024,
1040
- maxComputeWorkgroupSizeZ: 64,
1041
- maxComputeWorkgroupsPerDimension: 65535,
1042
- });
1043
-
1044
- const DOE_FEATURES = Object.freeze(new Set(['shader-f16']));
1045
-
1046
- /**
1047
- * Device returned by `adapter.requestDevice()`.
1048
- *
1049
- * This is the main full-surface headless WebGPU object exposed by the package.
1050
- *
1051
- * This example shows the API in its basic form.
1052
- *
1053
- * ```js
1054
- * const device = await adapter.requestDevice();
1055
- * ```
1056
- *
1057
- * - `queue`, `limits`, and `features` are available as data properties.
1058
- * - The full package keeps render, texture, sampler, and command APIs on this object.
1059
- */
1060
- class DoeGPUDevice {
1061
- constructor(native, instance) {
1062
- this._native = native;
1063
- this._instance = instance;
1064
- const q = addon.deviceGetQueue(native);
1065
- this.queue = new DoeGPUQueue(q, instance, native);
1066
- this.limits = DOE_LIMITS;
1067
- this.features = DOE_FEATURES;
1068
- }
1069
-
1070
- /**
1071
- * Create a buffer.
1072
- *
1073
- * This allocates a Doe buffer using the supplied WebGPU-shaped descriptor and
1074
- * returns the package wrapper for it.
1075
- *
1076
- * This example shows the API in its basic form.
1077
- *
1078
- * ```js
1079
- * const buffer = device.createBuffer({
1080
- * size: 16,
1081
- * usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
1082
- * });
1083
- * ```
1084
- *
1085
- * - The descriptor follows the standard WebGPU buffer shape.
1086
- * - The returned wrapper exposes `size`, `usage`, mapping, and destruction helpers.
1087
- */
1088
- createBuffer(descriptor) {
1089
- const buf = addon.createBuffer(this._native, descriptor);
1090
- return new DoeGPUBuffer(buf, this._instance, descriptor.size, descriptor.usage, this.queue);
1091
- }
1092
-
1093
- /**
1094
- * Create a shader module from WGSL source.
1095
- *
1096
- * This compiles WGSL into a shader module wrapper that can be used by
1097
- * compute or render pipeline creation.
1098
- *
1099
- * This example shows the API in its basic form.
1100
- *
1101
- * ```js
1102
- * const shader = device.createShaderModule({ code: WGSL });
1103
- * ```
1104
- *
1105
- * - `descriptor.code` is required on this surface.
1106
- * - The package also accepts `descriptor.source` as a convenience alias.
1107
- */
1108
- createShaderModule(descriptor) {
1109
- const code = descriptor.code || descriptor.source;
1110
- if (!code) throw new Error('createShaderModule: descriptor.code is required');
1111
- const mod = addon.createShaderModule(this._native, code);
1112
- return new DoeGPUShaderModule(mod, code);
1113
- }
1114
-
1115
- /**
1116
- * Create a compute pipeline.
1117
- *
1118
- * This builds a pipeline wrapper from a shader module, entry point, and
1119
- * optional explicit layout information.
1120
- *
1121
- * This example shows the API in its basic form.
1122
- *
1123
- * ```js
1124
- * const pipeline = device.createComputePipeline({
1125
- * layout: "auto",
1126
- * compute: { module: shader, entryPoint: "main" },
1127
- * });
1128
- * ```
1129
- *
1130
- * - `layout: "auto"` derives bind-group layouts from the WGSL.
1131
- * - Explicit pipeline layouts are passed through directly.
1132
- */
1133
- createComputePipeline(descriptor) {
1134
- const shader = descriptor.compute?.module;
1135
- const entryPoint = descriptor.compute?.entryPoint || 'main';
1136
- const layout = descriptor.layout === 'auto' ? null : descriptor.layout;
1137
- const autoLayoutEntriesByGroup = layout ? null : inferAutoBindGroupLayouts(
1138
- shader?._code || '',
1139
- globals.GPUShaderStage.COMPUTE,
635
+ if (pipeline._autoLayoutEntriesByGroup) {
636
+ const entries = pipeline._autoLayoutEntriesByGroup.get(index) ?? [];
637
+ return pipeline._device.createBindGroupLayout({ entries });
638
+ }
639
+ return pipeline._device.createBindGroupLayout({ entries: [] });
640
+ },
641
+ deviceLimits,
642
+ deviceFeatures,
643
+ adapterLimits,
644
+ adapterFeatures,
645
+ preflightShaderSource,
646
+ requireAutoLayoutEntriesFromNative(shader, visibility, path) {
647
+ return requireAutoLayoutEntriesFromNative(
648
+ assertLiveResource(shader, path, 'GPUShaderModule'),
649
+ visibility,
650
+ path,
1140
651
  );
1141
- const native = addon.createComputePipeline(
1142
- this._native, shader._native, entryPoint,
1143
- layout?._native ?? null);
1144
- return new DoeGPUComputePipeline(native, this, layout, autoLayoutEntriesByGroup);
1145
- }
1146
-
1147
- /**
1148
- * Create a compute pipeline through an async-shaped API.
1149
- *
1150
- * This preserves the async WebGPU API shape while using Doe's current
1151
- * synchronous pipeline creation underneath.
1152
- *
1153
- * This example shows the API in its basic form.
1154
- *
1155
- * ```js
1156
- * const pipeline = await device.createComputePipelineAsync(descriptor);
1157
- * ```
1158
- *
1159
- * - Doe currently resolves this by calling the synchronous pipeline creation path.
1160
- * - The async shape exists for WebGPU API compatibility.
1161
- */
1162
- async createComputePipelineAsync(descriptor) {
1163
- return this.createComputePipeline(descriptor);
1164
- }
1165
-
1166
- /**
1167
- * Create a bind-group layout.
1168
- *
1169
- * This normalizes the descriptor into the shape expected by Doe and returns
1170
- * a layout wrapper for later resource binding.
1171
- *
1172
- * This example shows the API in its basic form.
1173
- *
1174
- * ```js
1175
- * const layout = device.createBindGroupLayout({ entries });
1176
- * ```
1177
- *
1178
- * - Missing buffer entry fields are normalized to WebGPU-style defaults.
1179
- * - Storage-texture entries are forwarded when present.
1180
- */
1181
- createBindGroupLayout(descriptor) {
1182
- const entries = (descriptor.entries || []).map((e) => ({
1183
- binding: e.binding,
1184
- visibility: e.visibility,
1185
- buffer: e.buffer ? {
1186
- type: e.buffer.type || 'uniform',
1187
- hasDynamicOffset: e.buffer.hasDynamicOffset || false,
1188
- minBindingSize: e.buffer.minBindingSize || 0,
1189
- } : undefined,
1190
- storageTexture: e.storageTexture,
1191
- }));
1192
- const native = addon.createBindGroupLayout(this._native, entries);
1193
- return new DoeGPUBindGroupLayout(native);
1194
- }
1195
-
1196
- /**
1197
- * Create a bind group.
1198
- *
1199
- * This binds resources to a previously created layout and returns the bind
1200
- * group wrapper used by pass encoders.
1201
- *
1202
- * This example shows the API in its basic form.
1203
- *
1204
- * ```js
1205
- * const bindGroup = device.createBindGroup({ layout, entries });
1206
- * ```
1207
- *
1208
- * - Resource buffers may be passed either as `{ buffer, offset, size }` or as bare buffer wrappers.
1209
- * - Layout and buffer wrappers must come from the same device.
1210
- */
1211
- createBindGroup(descriptor) {
1212
- const entries = (descriptor.entries || []).map((e) => {
1213
- const entry = {
1214
- binding: e.binding,
1215
- buffer: e.resource?.buffer?._native ?? e.resource?._native ?? null,
1216
- offset: e.resource?.offset ?? 0,
1217
- };
1218
- if (e.resource?.size !== undefined) entry.size = e.resource.size;
1219
- return entry;
1220
- });
1221
- const native = addon.createBindGroup(
1222
- this._native, descriptor.layout._native, entries);
1223
- return new DoeGPUBindGroup(native);
1224
- }
1225
-
1226
- /**
1227
- * Create a pipeline layout.
1228
- *
1229
- * This combines one or more bind-group layouts into the pipeline layout
1230
- * wrapper used during pipeline creation.
1231
- *
1232
- * This example shows the API in its basic form.
1233
- *
1234
- * ```js
1235
- * const layout = device.createPipelineLayout({ bindGroupLayouts: [group0] });
1236
- * ```
1237
- *
1238
- * - Bind-group layouts are unwrapped to their native handles before creation.
1239
- * - The returned wrapper is opaque on the JS side.
1240
- */
1241
- createPipelineLayout(descriptor) {
1242
- const layouts = (descriptor.bindGroupLayouts || []).map((l) => l._native);
1243
- const native = addon.createPipelineLayout(this._native, layouts);
1244
- return new DoeGPUPipelineLayout(native);
1245
- }
1246
-
1247
- /**
1248
- * Create a texture.
1249
- *
1250
- * This allocates a Doe texture resource from a WebGPU-shaped descriptor and
1251
- * returns the package wrapper for it.
1252
- *
1253
- * This example shows the API in its basic form.
1254
- *
1255
- * ```js
1256
- * const texture = device.createTexture({
1257
- * size: [64, 64, 1],
1258
- * format: "rgba8unorm",
1259
- * usage: GPUTextureUsage.RENDER_ATTACHMENT,
1260
- * });
1261
- * ```
1262
- *
1263
- * - `descriptor.size` may be a scalar, tuple, or width/height object.
1264
- * - Omitted format and mip-count fields fall back to package defaults.
1265
- */
1266
- createTexture(descriptor) {
1267
- const native = addon.createTexture(this._native, {
1268
- format: descriptor.format || 'rgba8unorm',
1269
- width: descriptor.size?.[0] ?? descriptor.size?.width ?? descriptor.size ?? 1,
1270
- height: descriptor.size?.[1] ?? descriptor.size?.height ?? 1,
1271
- depthOrArrayLayers: descriptor.size?.[2] ?? descriptor.size?.depthOrArrayLayers ?? 1,
1272
- usage: descriptor.usage || 0,
1273
- mipLevelCount: descriptor.mipLevelCount || 1,
652
+ },
653
+ deviceGetQueue(native) {
654
+ return addon.deviceGetQueue(native);
655
+ },
656
+ deviceCreateBuffer(device, validated) {
657
+ return addon.createBuffer(assertLiveResource(device, 'GPUDevice.createBuffer', 'GPUDevice'), validated);
658
+ },
659
+ deviceCreateShaderModule(device, code) {
660
+ try {
661
+ return addon.createShaderModule(assertLiveResource(device, 'GPUDevice.createShaderModule', 'GPUDevice'), code);
662
+ } catch (error) {
663
+ throw enrichNativeCompilerError(error, 'GPUDevice.createShaderModule', readLastErrorFields());
664
+ }
665
+ },
666
+ deviceCreateComputePipeline(device, shaderNative, entryPoint, layoutNative) {
667
+ try {
668
+ return addon.createComputePipeline(
669
+ assertLiveResource(device, 'GPUDevice.createComputePipeline', 'GPUDevice'),
670
+ shaderNative,
671
+ entryPoint,
672
+ layoutNative,
673
+ );
674
+ } catch (error) {
675
+ throw enrichNativeCompilerError(error, 'GPUDevice.createComputePipeline', readLastErrorFields());
676
+ }
677
+ },
678
+ deviceCreateBindGroupLayout(device, entries) {
679
+ return addon.createBindGroupLayout(assertLiveResource(device, 'GPUDevice.createBindGroupLayout', 'GPUDevice'), entries);
680
+ },
681
+ deviceCreateBindGroup(device, layoutNative, entries) {
682
+ return addon.createBindGroup(
683
+ assertLiveResource(device, 'GPUDevice.createBindGroup', 'GPUDevice'),
684
+ layoutNative,
685
+ entries,
686
+ );
687
+ },
688
+ deviceCreatePipelineLayout(device, layouts) {
689
+ return addon.createPipelineLayout(assertLiveResource(device, 'GPUDevice.createPipelineLayout', 'GPUDevice'), layouts);
690
+ },
691
+ deviceCreateTexture(device, textureDescriptor, size, usage) {
692
+ return addon.createTexture(assertLiveResource(device, 'GPUDevice.createTexture', 'GPUDevice'), {
693
+ format: textureDescriptor.format || 'rgba8unorm',
694
+ width: size.width,
695
+ height: size.height,
696
+ depthOrArrayLayers: size.depthOrArrayLayers,
697
+ dimension: TEXTURE_DIMENSION_MAP[textureDescriptor.dimension ?? '2d'] ?? 2,
698
+ usage,
699
+ mipLevelCount: assertIntegerInRange(textureDescriptor.mipLevelCount ?? 1, 'GPUDevice.createTexture', 'descriptor.mipLevelCount', { min: 1, max: UINT32_MAX }),
1274
700
  });
1275
- return new DoeGPUTexture(native);
1276
- }
1277
-
1278
- /**
1279
- * Create a sampler.
1280
- *
1281
- * This allocates a sampler wrapper that can be used by the package's render
1282
- * and texture-binding paths.
1283
- *
1284
- * This example shows the API in its basic form.
1285
- *
1286
- * ```js
1287
- * const sampler = device.createSampler();
1288
- * ```
1289
- *
1290
- * - An empty descriptor is allowed.
1291
- * - The returned wrapper is currently an opaque handle on the JS side.
1292
- */
1293
- createSampler(descriptor = {}) {
1294
- const native = addon.createSampler(this._native, descriptor);
1295
- return new DoeGPUSampler(native);
1296
- }
1297
-
1298
- /**
1299
- * Create a render pipeline.
1300
- *
1301
- * This builds the package's render-pipeline wrapper for use with render-pass
1302
- * encoders on the full surface.
1303
- *
1304
- * This example shows the API in its basic form.
1305
- *
1306
- * ```js
1307
- * const pipeline = device.createRenderPipeline(descriptor);
1308
- * ```
1309
- *
1310
- * - The returned wrapper is consumed by render-pass encoders.
1311
- * - Descriptor handling on this package surface is intentionally narrower than browser engines.
1312
- */
1313
- createRenderPipeline(descriptor) {
1314
- const native = addon.createRenderPipeline(this._native);
1315
- return new DoeGPURenderPipeline(native);
1316
- }
1317
-
1318
- /**
1319
- * Create a command encoder.
1320
- *
1321
- * This creates the object that records compute, render, and copy commands
1322
- * before queue submission.
1323
- *
1324
- * This example shows the API in its basic form.
1325
- *
1326
- * ```js
1327
- * const encoder = device.createCommandEncoder();
1328
- * ```
1329
- *
1330
- * - The descriptor is accepted for API shape compatibility.
1331
- * - The returned encoder records work until `finish()` is called.
1332
- */
1333
- createCommandEncoder(descriptor) {
1334
- return new DoeGPUCommandEncoder(this._native);
1335
- }
1336
-
1337
- /**
1338
- * Release the native device.
1339
- *
1340
- * This tears down the underlying Doe device associated with the wrapper.
1341
- *
1342
- * This example shows the API in its basic form.
1343
- *
1344
- * ```js
1345
- * device.destroy();
1346
- * ```
1347
- *
1348
- * - Reusing the device after destruction is unsupported.
1349
- * - Existing wrappers created from the device do not regain validity afterward.
1350
- */
1351
- destroy() {
1352
- addon.deviceRelease(this._native);
1353
- this._native = null;
1354
- }
1355
- }
1356
-
1357
- /**
1358
- * Adapter returned by `gpu.requestAdapter()`.
1359
- *
1360
- * This example shows the API in its basic form.
1361
- *
1362
- * ```js
1363
- * const adapter = await gpu.requestAdapter();
1364
- * ```
1365
- *
1366
- * - `features` and `limits` are exposed as data properties.
1367
- * - The adapter produces full-surface devices on this package entrypoint.
1368
- */
1369
- class DoeGPUAdapter {
1370
- constructor(native, instance) {
1371
- this._native = native;
1372
- this._instance = instance;
1373
- this.features = DOE_FEATURES;
1374
- this.limits = DOE_LIMITS;
1375
- }
1376
-
1377
- /**
1378
- * Request a device from this adapter.
1379
- *
1380
- * This creates the full-surface Doe device associated with the adapter.
1381
- *
1382
- * This example shows the API in its basic form.
1383
- *
1384
- * ```js
1385
- * const device = await adapter.requestDevice();
1386
- * ```
1387
- *
1388
- * - The descriptor is accepted for WebGPU API shape compatibility.
1389
- * - The returned device includes the full package surface.
1390
- */
1391
- async requestDevice(descriptor) {
1392
- const device = addon.requestDevice(this._instance, this._native);
1393
- return new DoeGPUDevice(device, this._instance);
1394
- }
1395
-
1396
- /**
1397
- * Release the native adapter.
1398
- *
1399
- * This tears down the adapter handle that was returned by Doe for this GPU.
1400
- *
1401
- * This example shows the API in its basic form.
1402
- *
1403
- * ```js
1404
- * adapter.destroy();
1405
- * ```
1406
- *
1407
- * - Reusing the adapter after destruction is unsupported.
1408
- */
1409
- destroy() {
1410
- addon.adapterRelease(this._native);
1411
- this._native = null;
1412
- }
1413
- }
1414
-
1415
- /**
1416
- * GPU root object returned by `create()` or installed at `navigator.gpu`.
1417
- *
1418
- * This example shows the API in its basic form.
1419
- *
1420
- * ```js
1421
- * const gpu = create();
1422
- * ```
1423
- *
1424
- * - This is a headless package-owned GPU object, not a browser-owned DOM object.
1425
- */
1426
- class DoeGPU {
1427
- constructor(instance) {
1428
- this._instance = instance;
1429
- }
701
+ },
702
+ deviceCreateSampler(device, descriptor) {
703
+ return addon.createSampler(assertLiveResource(device, 'GPUDevice.createSampler', 'GPUDevice'), descriptor);
704
+ },
705
+ deviceCreateRenderPipeline(device, descriptor) {
706
+ return addon.createRenderPipeline(
707
+ assertLiveResource(device, 'GPUDevice.createRenderPipeline', 'GPUDevice'),
708
+ {
709
+ layout: descriptor.layout,
710
+ vertex: {
711
+ module: descriptor.vertexModule,
712
+ entryPoint: descriptor.vertexEntryPoint,
713
+ buffers: descriptor.vertexBuffers ?? [],
714
+ },
715
+ fragment: {
716
+ module: descriptor.fragmentModule,
717
+ entryPoint: descriptor.fragmentEntryPoint,
718
+ targets: [{ format: descriptor.colorFormat }],
719
+ },
720
+ primitive: descriptor.primitive ? {
721
+ topology: descriptor.primitive.topology ?? 'triangle-list',
722
+ frontFace: descriptor.primitive.frontFace ?? 'ccw',
723
+ cullMode: descriptor.primitive.cullMode ?? 'none',
724
+ unclippedDepth: descriptor.primitive.unclippedDepth ?? false,
725
+ } : undefined,
726
+ multisample: descriptor.multisample ? {
727
+ count: descriptor.multisample.count ?? 1,
728
+ mask: descriptor.multisample.mask ?? 0xFFFF_FFFF,
729
+ alphaToCoverageEnabled: descriptor.multisample.alphaToCoverageEnabled ?? false,
730
+ } : undefined,
731
+ depthStencil: descriptor.depthStencil ? {
732
+ format: descriptor.depthStencil.format,
733
+ depthWriteEnabled: descriptor.depthStencil.depthWriteEnabled ?? false,
734
+ depthCompare: descriptor.depthStencil.depthCompare ?? 'always',
735
+ } : undefined,
736
+ },
737
+ );
738
+ },
739
+ deviceCreateQuerySet(device, descriptor) {
740
+ const QUERY_TYPE_TIMESTAMP = 2;
741
+ return addon.createQuerySet(
742
+ assertLiveResource(device, 'GPUDevice.createQuerySet', 'GPUDevice'),
743
+ QUERY_TYPE_TIMESTAMP,
744
+ descriptor.count,
745
+ );
746
+ },
747
+ querySetDestroy(native) {
748
+ addon.querySetDestroy(native);
749
+ },
750
+ deviceCreateCommandEncoder(device) {
751
+ return new DoeGPUCommandEncoder(null, device);
752
+ },
753
+ deviceDestroy(native) {
754
+ addon.deviceRelease(native);
755
+ },
756
+ adapterRequestDevice(adapter, _descriptor, classes) {
757
+ assertLiveResource(adapter, 'GPUAdapter.requestDevice', 'GPUAdapter');
758
+ const native = addon.requestDevice(adapter._instance, adapter._native);
759
+ const device = {
760
+ _destroyed: false,
761
+ _resourceLabel: 'GPUDevice',
762
+ _resourceOwner: null,
763
+ createBuffer: classes.DoeGPUDevice.prototype.createBuffer,
764
+ createShaderModule: classes.DoeGPUDevice.prototype.createShaderModule,
765
+ createComputePipeline: classes.DoeGPUDevice.prototype.createComputePipeline,
766
+ createComputePipelineAsync: classes.DoeGPUDevice.prototype.createComputePipelineAsync,
767
+ createBindGroupLayout: classes.DoeGPUDevice.prototype.createBindGroupLayout,
768
+ createBindGroup: classes.DoeGPUDevice.prototype.createBindGroup,
769
+ createPipelineLayout: classes.DoeGPUDevice.prototype.createPipelineLayout,
770
+ createTexture: classes.DoeGPUDevice.prototype.createTexture,
771
+ createSampler: classes.DoeGPUDevice.prototype.createSampler,
772
+ createRenderPipeline: classes.DoeGPUDevice.prototype.createRenderPipeline,
773
+ createQuerySet: classes.DoeGPUDevice.prototype.createQuerySet,
774
+ createCommandEncoder: classes.DoeGPUDevice.prototype.createCommandEncoder,
775
+ destroy: classes.DoeGPUDevice.prototype.destroy,
776
+ };
777
+ device._native = native;
778
+ device._instance = adapter._instance;
779
+ device.limits = deviceLimits(native);
780
+ device.features = deviceFeatures(native);
781
+ const queue = {
782
+ _destroyed: false,
783
+ _resourceLabel: 'GPUQueue',
784
+ _resourceOwner: device,
785
+ hasPendingSubmissions: classes.DoeGPUQueue.prototype.hasPendingSubmissions,
786
+ markSubmittedWorkDone: classes.DoeGPUQueue.prototype.markSubmittedWorkDone,
787
+ submit: classes.DoeGPUQueue.prototype.submit,
788
+ writeBuffer: classes.DoeGPUQueue.prototype.writeBuffer,
789
+ onSubmittedWorkDone: classes.DoeGPUQueue.prototype.onSubmittedWorkDone,
790
+ };
791
+ queue._native = addon.deviceGetQueue(native);
792
+ queue._instance = adapter._instance;
793
+ queue._device = device;
794
+ this.initQueueState(queue);
795
+ device.queue = queue;
796
+ return device;
797
+ },
798
+ adapterDestroy(native) {
799
+ addon.adapterRelease(native);
800
+ },
801
+ gpuRequestAdapter(gpu, _options, classes) {
802
+ const adapter = addon.requestAdapter(gpu._instance);
803
+ return new classes.DoeGPUAdapter(adapter, gpu._instance);
804
+ },
805
+ };
1430
806
 
1431
- /**
1432
- * Request an adapter from the Doe runtime.
1433
- *
1434
- * This asks the package-owned GPU object for an adapter wrapper that can
1435
- * later create full-surface devices.
1436
- *
1437
- * This example shows the API in its basic form.
1438
- *
1439
- * ```js
1440
- * const adapter = await gpu.requestAdapter();
1441
- * ```
1442
- *
1443
- * - The current Doe package path ignores adapter filtering options.
1444
- * - The returned adapter exposes full-surface device creation.
1445
- */
1446
- async requestAdapter(options) {
1447
- const adapter = addon.requestAdapter(this._instance);
1448
- return new DoeGPUAdapter(adapter, this._instance);
1449
- }
1450
- }
807
+ const {
808
+ DoeGPUBuffer,
809
+ DoeGPUQueue,
810
+ DoeGPUTexture,
811
+ DoeGPUTextureView,
812
+ DoeGPUSampler,
813
+ DoeGPURenderPipeline,
814
+ DoeGPUShaderModule,
815
+ DoeGPUComputePipeline,
816
+ DoeGPUBindGroupLayout,
817
+ DoeGPUBindGroup,
818
+ DoeGPUPipelineLayout,
819
+ DoeGPUDevice,
820
+ DoeGPUAdapter,
821
+ DoeGPU,
822
+ } = createFullSurfaceClasses({
823
+ globals,
824
+ backend: fullSurfaceBackend,
825
+ });
1451
826
 
1452
827
  /**
1453
828
  * Create a package-local `GPU` object backed by the Doe native runtime.
@@ -1473,6 +848,15 @@ export function create(createArgs = null) {
1473
848
  return new DoeGPU(instance);
1474
849
  }
1475
850
 
851
+ export function setNativeTimeoutMs(timeoutMs) {
852
+ ensureLibrary();
853
+ validatePositiveInteger(timeoutMs, 'native timeout');
854
+ if (typeof addon.setTimeoutMs !== 'function') {
855
+ throw new Error('setNativeTimeoutMs is not supported by the loaded addon.');
856
+ }
857
+ addon.setTimeoutMs(timeoutMs);
858
+ }
859
+
1476
860
  /**
1477
861
  * Install the package WebGPU globals onto a target object and return its GPU.
1478
862
  *
@@ -1493,33 +877,8 @@ export function create(createArgs = null) {
1493
877
  * - The returned GPU is still headless/package-owned, not browser DOM ownership or browser-process parity.
1494
878
  */
1495
879
  export function setupGlobals(target = globalThis, createArgs = null) {
1496
- for (const [name, value] of Object.entries(globals)) {
1497
- if (target[name] === undefined) {
1498
- Object.defineProperty(target, name, {
1499
- value,
1500
- writable: true,
1501
- configurable: true,
1502
- enumerable: false,
1503
- });
1504
- }
1505
- }
1506
880
  const gpu = create(createArgs);
1507
- if (typeof target.navigator === 'undefined') {
1508
- Object.defineProperty(target, 'navigator', {
1509
- value: { gpu },
1510
- writable: true,
1511
- configurable: true,
1512
- enumerable: false,
1513
- });
1514
- } else if (!target.navigator.gpu) {
1515
- Object.defineProperty(target.navigator, 'gpu', {
1516
- value: gpu,
1517
- writable: true,
1518
- configurable: true,
1519
- enumerable: false,
1520
- });
1521
- }
1522
- return gpu;
881
+ return setupGlobalsOnTarget(target, gpu, globals);
1523
882
  }
1524
883
 
1525
884
  /**
@@ -1539,8 +898,7 @@ export function setupGlobals(target = globalThis, createArgs = null) {
1539
898
  * - `adapterOptions` are accepted for WebGPU shape compatibility; the current Doe package path does not use them for adapter filtering.
1540
899
  */
1541
900
  export async function requestAdapter(adapterOptions = undefined, createArgs = null) {
1542
- const gpu = create(createArgs);
1543
- return gpu.requestAdapter(adapterOptions);
901
+ return requestAdapterFromCreate(create, adapterOptions, createArgs);
1544
902
  }
1545
903
 
1546
904
  /**
@@ -1565,9 +923,7 @@ export async function requestAdapter(adapterOptions = undefined, createArgs = nu
1565
923
  * - Missing runtime prerequisites still fail at request time through the same addon/library checks as `create()`.
1566
924
  */
1567
925
  export async function requestDevice(options = {}) {
1568
- const createArgs = options?.createArgs ?? null;
1569
- const adapter = await requestAdapter(options?.adapterOptions, createArgs);
1570
- return adapter.requestDevice(options?.deviceDescriptor);
926
+ return requestDeviceFromRequestAdapter(requestAdapter, options);
1571
927
  }
1572
928
 
1573
929
  /**
@@ -1590,8 +946,7 @@ export async function requestDevice(options = {}) {
1590
946
  */
1591
947
  export function providerInfo() {
1592
948
  const flavor = libraryFlavor(DOE_LIB_PATH);
1593
- return {
1594
- module: '@simulatte/webgpu',
949
+ return buildProviderInfo({
1595
950
  loaded: !!addon && !!DOE_LIB_PATH,
1596
951
  loadError: !addon ? 'native addon not found' : !DOE_LIB_PATH ? 'libwebgpu_doe not found' : '',
1597
952
  defaultCreateArgs: [],
@@ -1602,7 +957,7 @@ export function providerInfo() {
1602
957
  buildMetadataPath: DOE_BUILD_METADATA.path,
1603
958
  leanVerifiedBuild: DOE_BUILD_METADATA.leanVerifiedBuild,
1604
959
  proofArtifactSha256: DOE_BUILD_METADATA.proofArtifactSha256,
1605
- };
960
+ });
1606
961
  }
1607
962
 
1608
963
  /**