@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
@@ -0,0 +1,120 @@
1
+ const MAX_SAFE_U64 = Number.MAX_SAFE_INTEGER;
2
+ const UINT32_MAX = 0xFFFF_FFFF;
3
+
4
+ function failValidation(path, message) {
5
+ throw new Error(`${path}: ${message}`);
6
+ }
7
+
8
+ function describeResourceLabel(value, fallback = 'resource') {
9
+ return value?._resourceLabel ?? fallback;
10
+ }
11
+
12
+ function initResource(target, label, owner = null) {
13
+ target._resourceLabel = label;
14
+ target._resourceOwner = owner;
15
+ target._destroyed = false;
16
+ return target;
17
+ }
18
+
19
+ function assertObject(value, path, label) {
20
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
21
+ failValidation(path, `${label} must be an object`);
22
+ }
23
+ return value;
24
+ }
25
+
26
+ function assertArray(value, path, label) {
27
+ if (!Array.isArray(value)) {
28
+ failValidation(path, `${label} must be an array`);
29
+ }
30
+ return value;
31
+ }
32
+
33
+ function assertBoolean(value, path, label) {
34
+ if (typeof value !== 'boolean') {
35
+ failValidation(path, `${label} must be a boolean`);
36
+ }
37
+ return value;
38
+ }
39
+
40
+ function assertString(value, path, label) {
41
+ if (typeof value !== 'string') {
42
+ failValidation(path, `${label} must be a string`);
43
+ }
44
+ return value;
45
+ }
46
+
47
+ function assertNonEmptyString(value, path, label) {
48
+ if (assertString(value, path, label).length === 0) {
49
+ failValidation(path, `${label} must not be empty`);
50
+ }
51
+ return value;
52
+ }
53
+
54
+ function assertIntegerInRange(value, path, label, {
55
+ min = 0,
56
+ max = MAX_SAFE_U64,
57
+ } = {}) {
58
+ if (!Number.isInteger(value) || value < min || value > max) {
59
+ failValidation(path, `${label} must be an integer in [${min}, ${max}]`);
60
+ }
61
+ return value;
62
+ }
63
+
64
+ function assertOptionalIntegerInRange(value, path, label, options) {
65
+ if (value === undefined) {
66
+ return value;
67
+ }
68
+ return assertIntegerInRange(value, path, label, options);
69
+ }
70
+
71
+ function validatePositiveInteger(value, label) {
72
+ if (!Number.isInteger(value) || value < 1) {
73
+ throw new Error(`${label} must be a positive integer.`);
74
+ }
75
+ }
76
+
77
+ function assertLiveResource(resource, path, label = null) {
78
+ const resourceLabel = label ?? describeResourceLabel(resource);
79
+ if (!resource || typeof resource !== 'object' || !('_native' in resource)) {
80
+ failValidation(path, `${resourceLabel} must be a Doe WebGPU object`);
81
+ }
82
+ const owner = resource._resourceOwner;
83
+ if (owner?._destroyed) {
84
+ failValidation(
85
+ path,
86
+ `${resourceLabel} cannot be used after ${describeResourceLabel(owner, 'owning resource')} was destroyed`,
87
+ );
88
+ }
89
+ if (resource._destroyed || resource._native == null) {
90
+ failValidation(path, `${resourceLabel} was destroyed`);
91
+ }
92
+ return resource._native;
93
+ }
94
+
95
+ function destroyResource(resource, release) {
96
+ if (resource._destroyed || resource._native == null) {
97
+ return;
98
+ }
99
+ release(resource._native);
100
+ resource._native = null;
101
+ resource._destroyed = true;
102
+ }
103
+
104
+ export {
105
+ MAX_SAFE_U64,
106
+ UINT32_MAX,
107
+ failValidation,
108
+ describeResourceLabel,
109
+ initResource,
110
+ assertObject,
111
+ assertArray,
112
+ assertBoolean,
113
+ assertString,
114
+ assertNonEmptyString,
115
+ assertIntegerInRange,
116
+ assertOptionalIntegerInRange,
117
+ validatePositiveInteger,
118
+ assertLiveResource,
119
+ destroyResource,
120
+ };
@@ -0,0 +1,495 @@
1
+ import { globals } from '../webgpu_constants.js';
2
+ import {
3
+ UINT32_MAX,
4
+ failValidation,
5
+ describeResourceLabel,
6
+ assertObject,
7
+ assertBoolean,
8
+ assertNonEmptyString,
9
+ assertIntegerInRange,
10
+ assertOptionalIntegerInRange,
11
+ assertLiveResource,
12
+ } from './resource-lifecycle.js';
13
+
14
+ const SAMPLER_BINDING_TYPES = Object.freeze({
15
+ filtering: 'filtering',
16
+ 'non-filtering': 'non-filtering',
17
+ comparison: 'comparison',
18
+ });
19
+
20
+ const TEXTURE_SAMPLE_TYPES = Object.freeze({
21
+ float: 'float',
22
+ 'unfilterable-float': 'unfilterable-float',
23
+ depth: 'depth',
24
+ sint: 'sint',
25
+ uint: 'uint',
26
+ });
27
+
28
+ const TEXTURE_VIEW_DIMENSIONS = Object.freeze({
29
+ '1d': '1d',
30
+ '2d': '2d',
31
+ '2d-array': '2d-array',
32
+ cube: 'cube',
33
+ 'cube-array': 'cube-array',
34
+ '3d': '3d',
35
+ });
36
+
37
+ const TEXTURE_DIMENSIONS = Object.freeze({
38
+ '1d': '1d',
39
+ '2d': '2d',
40
+ '3d': '3d',
41
+ });
42
+
43
+ const STORAGE_TEXTURE_ACCESS = Object.freeze({
44
+ 'write-only': 'write-only',
45
+ 'read-only': 'read-only',
46
+ 'read-write': 'read-write',
47
+ });
48
+
49
+ const ALL_BUFFER_USAGE_BITS = Object.values(globals.GPUBufferUsage)
50
+ .reduce((mask, bit) => mask | bit, 0);
51
+
52
+ // ============================================================
53
+ // Texture format capability tables
54
+ // ============================================================
55
+
56
+ // Depth/stencil formats recognized without any feature gate
57
+ const DEPTH_STENCIL_FORMATS_BASE = Object.freeze(new Set([
58
+ 'stencil8',
59
+ 'depth16unorm',
60
+ 'depth24plus',
61
+ 'depth24plus-stencil8',
62
+ 'depth32float',
63
+ ]));
64
+
65
+ // depth32float-stencil8 requires the depth32float-stencil8 feature
66
+ const DEPTH_STENCIL_FORMAT_DEPTH32FLOAT_STENCIL8 = 'depth32float-stencil8';
67
+
68
+ function isDepthStencilFormat(format, features) {
69
+ if (DEPTH_STENCIL_FORMATS_BASE.has(format)) return true;
70
+ if (format === DEPTH_STENCIL_FORMAT_DEPTH32FLOAT_STENCIL8) {
71
+ return features != null && features.has('depth32float-stencil8');
72
+ }
73
+ return false;
74
+ }
75
+
76
+ // Formats that can have stencil aspect (stencilLoadOp / stencilStoreOp)
77
+ const STENCIL_FORMATS = Object.freeze(new Set([
78
+ 'stencil8',
79
+ 'depth24plus-stencil8',
80
+ 'depth32float-stencil8',
81
+ ]));
82
+
83
+ function hasStencilAspect(format) {
84
+ return STENCIL_FORMATS.has(format);
85
+ }
86
+
87
+ // Formats valid for STORAGE_BINDING usage (without any feature)
88
+ const STORAGE_TEXTURE_FORMATS_BASE = Object.freeze(new Set([
89
+ 'rgba8unorm',
90
+ 'rgba8snorm',
91
+ 'rgba8uint',
92
+ 'rgba8sint',
93
+ 'rgba16uint',
94
+ 'rgba16sint',
95
+ 'rgba16float',
96
+ 'r32float',
97
+ 'r32uint',
98
+ 'r32sint',
99
+ 'rg32float',
100
+ 'rg32uint',
101
+ 'rg32sint',
102
+ 'rgba32float',
103
+ 'rgba32uint',
104
+ 'rgba32sint',
105
+ ]));
106
+
107
+ // bgra8unorm is valid for storage only with bgra8unorm-storage feature
108
+ function isStorageTextureFormat(format, features) {
109
+ if (STORAGE_TEXTURE_FORMATS_BASE.has(format)) return true;
110
+ if (format === 'bgra8unorm') {
111
+ return features != null && features.has('bgra8unorm-storage');
112
+ }
113
+ return false;
114
+ }
115
+
116
+ // Float32 formats: default sample type is unfilterable-float.
117
+ // With float32-filterable feature, they become filterable (sample type = float).
118
+ const FLOAT32_FORMATS = Object.freeze(new Set([
119
+ 'r32float',
120
+ 'rg32float',
121
+ 'rgba32float',
122
+ ]));
123
+
124
+ function float32SampleType(format, features) {
125
+ if (!FLOAT32_FORMATS.has(format)) return null;
126
+ if (features != null && features.has('float32-filterable')) {
127
+ return 'float';
128
+ }
129
+ return 'unfilterable-float';
130
+ }
131
+
132
+ function isFloat32Filterable(format, features) {
133
+ return FLOAT32_FORMATS.has(format) && features != null && features.has('float32-filterable');
134
+ }
135
+
136
+ // Float32 formats: default is not blendable as render target.
137
+ // With float32-blendable feature, blend state is allowed on float32 color targets.
138
+ function isFloat32Blendable(format, features) {
139
+ return FLOAT32_FORMATS.has(format) && features != null && features.has('float32-blendable');
140
+ }
141
+
142
+ // Formats that are always blendable (non-integer, non-depth, non-float32)
143
+ const ALWAYS_BLENDABLE_FORMATS = Object.freeze(new Set([
144
+ 'r8unorm', 'r8snorm',
145
+ 'r16float',
146
+ 'rg8unorm', 'rg8snorm',
147
+ 'rg16float',
148
+ 'rgba8unorm', 'rgba8unorm-srgb', 'rgba8snorm',
149
+ 'bgra8unorm', 'bgra8unorm-srgb',
150
+ 'rgb10a2unorm',
151
+ 'rgba16float',
152
+ ]));
153
+
154
+ function isBlendableFormat(format, features) {
155
+ if (ALWAYS_BLENDABLE_FORMATS.has(format)) return true;
156
+ return isFloat32Blendable(format, features);
157
+ }
158
+
159
+ // BC (S3TC/DXT) compressed formats — require texture-compression-bc feature.
160
+ // Read-only compressed: valid for TEXTURE_BINDING and CopySrc/CopyDst,
161
+ // not valid for RENDER_ATTACHMENT or STORAGE_BINDING.
162
+ const BC_FORMATS = Object.freeze(new Set([
163
+ 'bc1-rgba-unorm', 'bc1-rgba-unorm-srgb',
164
+ 'bc2-rgba-unorm', 'bc2-rgba-unorm-srgb',
165
+ 'bc3-rgba-unorm', 'bc3-rgba-unorm-srgb',
166
+ 'bc4-r-unorm', 'bc4-r-snorm',
167
+ 'bc5-rg-unorm', 'bc5-rg-snorm',
168
+ 'bc6h-rgb-ufloat', 'bc6h-rgb-float',
169
+ 'bc7-rgba-unorm', 'bc7-rgba-unorm-srgb',
170
+ ]));
171
+
172
+ function isBCFormat(format) {
173
+ return BC_FORMATS.has(format);
174
+ }
175
+
176
+ // ASTC compressed formats — require texture-compression-astc feature.
177
+ // Read-only compressed: valid for TEXTURE_BINDING and CopySrc/CopyDst,
178
+ // not valid for RENDER_ATTACHMENT or STORAGE_BINDING.
179
+ const ASTC_FORMATS = Object.freeze(new Set([
180
+ 'astc-4x4-unorm', 'astc-4x4-unorm-srgb',
181
+ 'astc-5x4-unorm', 'astc-5x4-unorm-srgb',
182
+ 'astc-5x5-unorm', 'astc-5x5-unorm-srgb',
183
+ 'astc-6x5-unorm', 'astc-6x5-unorm-srgb',
184
+ 'astc-6x6-unorm', 'astc-6x6-unorm-srgb',
185
+ 'astc-8x5-unorm', 'astc-8x5-unorm-srgb',
186
+ 'astc-8x6-unorm', 'astc-8x6-unorm-srgb',
187
+ 'astc-8x8-unorm', 'astc-8x8-unorm-srgb',
188
+ 'astc-10x5-unorm', 'astc-10x5-unorm-srgb',
189
+ 'astc-10x6-unorm', 'astc-10x6-unorm-srgb',
190
+ 'astc-10x8-unorm', 'astc-10x8-unorm-srgb',
191
+ 'astc-10x10-unorm', 'astc-10x10-unorm-srgb',
192
+ 'astc-12x10-unorm', 'astc-12x10-unorm-srgb',
193
+ 'astc-12x12-unorm', 'astc-12x12-unorm-srgb',
194
+ ]));
195
+
196
+ function isASTCFormat(format) {
197
+ return ASTC_FORMATS.has(format);
198
+ }
199
+
200
+ // ETC2/EAC compressed formats — require texture-compression-etc2 feature.
201
+ // Read-only compressed: valid for TEXTURE_BINDING and CopySrc/CopyDst,
202
+ // not valid for RENDER_ATTACHMENT or STORAGE_BINDING.
203
+ const ETC2_FORMATS = Object.freeze(new Set([
204
+ 'etc2-rgb8unorm', 'etc2-rgb8unorm-srgb',
205
+ 'etc2-rgb8a1unorm', 'etc2-rgb8a1unorm-srgb',
206
+ 'etc2-rgba8unorm', 'etc2-rgba8unorm-srgb',
207
+ 'eac-r11unorm', 'eac-r11snorm',
208
+ 'eac-rg11unorm', 'eac-rg11snorm',
209
+ ]));
210
+
211
+ function isETC2Format(format) {
212
+ return ETC2_FORMATS.has(format);
213
+ }
214
+
215
+ // ASTC block sizes for validation (width, height) keyed by format prefix
216
+ const ASTC_BLOCK_SIZES = Object.freeze({
217
+ 'astc-4x4': [4, 4], 'astc-5x4': [5, 4], 'astc-5x5': [5, 5],
218
+ 'astc-6x5': [6, 5], 'astc-6x6': [6, 6], 'astc-8x5': [8, 5],
219
+ 'astc-8x6': [8, 6], 'astc-8x8': [8, 8], 'astc-10x5': [10, 5],
220
+ 'astc-10x6': [10, 6], 'astc-10x8': [10, 8], 'astc-10x10': [10, 10],
221
+ 'astc-12x10': [12, 10], 'astc-12x12': [12, 12],
222
+ });
223
+
224
+ function astcBlockSize(format) {
225
+ const prefix = format.replace(/-unorm(-srgb)?$/, '');
226
+ return ASTC_BLOCK_SIZES[prefix] ?? null;
227
+ }
228
+
229
+ function assertBufferDescriptor(descriptor, path) {
230
+ assertObject(descriptor, path, 'descriptor');
231
+ assertIntegerInRange(descriptor.size, path, 'descriptor.size', { min: 1 });
232
+ const usage = assertIntegerInRange(descriptor.usage, path, 'descriptor.usage', { min: 1 });
233
+ if ((usage & ~ALL_BUFFER_USAGE_BITS) !== 0) {
234
+ failValidation(path, `descriptor.usage contains unknown flag bits (0x${(usage & ~ALL_BUFFER_USAGE_BITS).toString(16)})`);
235
+ }
236
+ if (descriptor.mappedAtCreation !== undefined) {
237
+ assertBoolean(descriptor.mappedAtCreation, path, 'descriptor.mappedAtCreation');
238
+ if (descriptor.mappedAtCreation && (descriptor.size % 4) !== 0) {
239
+ failValidation(path, 'descriptor.size must be a multiple of 4 when mappedAtCreation is true');
240
+ }
241
+ }
242
+ return descriptor;
243
+ }
244
+
245
+ function normalizeEnumKey(value, path, label) {
246
+ return assertNonEmptyString(value, path, label).trim().toLowerCase().replaceAll('_', '-');
247
+ }
248
+
249
+ function normalizeSamplerLayout(binding, path, label) {
250
+ const sampler = assertObject(binding, path, label);
251
+ const type = normalizeEnumKey(sampler.type ?? 'filtering', path, `${label}.type`);
252
+ if (!(type in SAMPLER_BINDING_TYPES)) {
253
+ failValidation(path, `${label}.type must be one of: ${Object.keys(SAMPLER_BINDING_TYPES).join(', ')}`);
254
+ }
255
+ return { type };
256
+ }
257
+
258
+ function normalizeTextureLayout(binding, path, label) {
259
+ const texture = assertObject(binding, path, label);
260
+ const sampleType = normalizeEnumKey(texture.sampleType ?? 'float', path, `${label}.sampleType`);
261
+ const viewDimension = normalizeEnumKey(texture.viewDimension ?? '2d', path, `${label}.viewDimension`);
262
+ if (!(sampleType in TEXTURE_SAMPLE_TYPES)) {
263
+ failValidation(path, `${label}.sampleType must be one of: ${Object.keys(TEXTURE_SAMPLE_TYPES).join(', ')}`);
264
+ }
265
+ if (!(viewDimension in TEXTURE_VIEW_DIMENSIONS)) {
266
+ failValidation(path, `${label}.viewDimension must be one of: ${Object.keys(TEXTURE_VIEW_DIMENSIONS).join(', ')}`);
267
+ }
268
+ return {
269
+ sampleType,
270
+ viewDimension,
271
+ multisampled: texture.multisampled === undefined
272
+ ? false
273
+ : assertBoolean(texture.multisampled, path, `${label}.multisampled`),
274
+ };
275
+ }
276
+
277
+ function normalizeStorageTextureLayout(binding, path, label) {
278
+ const storageTexture = assertObject(binding, path, label);
279
+ const access = normalizeEnumKey(storageTexture.access ?? 'write-only', path, `${label}.access`);
280
+ const viewDimension = normalizeEnumKey(storageTexture.viewDimension ?? '2d', path, `${label}.viewDimension`);
281
+ if (!(access in STORAGE_TEXTURE_ACCESS)) {
282
+ failValidation(path, `${label}.access must be one of: ${Object.keys(STORAGE_TEXTURE_ACCESS).join(', ')}`);
283
+ }
284
+ if (!(viewDimension in TEXTURE_VIEW_DIMENSIONS)) {
285
+ failValidation(path, `${label}.viewDimension must be one of: ${Object.keys(TEXTURE_VIEW_DIMENSIONS).join(', ')}`);
286
+ }
287
+ return {
288
+ access,
289
+ format: assertNonEmptyString(storageTexture.format, path, `${label}.format`),
290
+ viewDimension,
291
+ };
292
+ }
293
+
294
+ function normalizeTextureDimension(value, path, label = 'descriptor.dimension') {
295
+ const dimension = normalizeEnumKey(value ?? '2d', path, label);
296
+ if (!(dimension in TEXTURE_DIMENSIONS)) {
297
+ failValidation(path, `${label} must be one of: ${Object.keys(TEXTURE_DIMENSIONS).join(', ')}`);
298
+ }
299
+ return dimension;
300
+ }
301
+
302
+ function assertTextureSize(size, path) {
303
+ if (typeof size === 'number') {
304
+ return {
305
+ width: assertIntegerInRange(size, path, 'descriptor.size', { min: 1 }),
306
+ height: 1,
307
+ depthOrArrayLayers: 1,
308
+ };
309
+ }
310
+ if (Array.isArray(size)) {
311
+ if (size.length < 1 || size.length > 3) {
312
+ failValidation(path, 'descriptor.size array must have 1 to 3 entries');
313
+ }
314
+ return {
315
+ width: assertIntegerInRange(size[0], path, 'descriptor.size[0]', { min: 1, max: UINT32_MAX }),
316
+ height: assertIntegerInRange(size[1] ?? 1, path, 'descriptor.size[1]', { min: 1, max: UINT32_MAX }),
317
+ depthOrArrayLayers: assertIntegerInRange(size[2] ?? 1, path, 'descriptor.size[2]', { min: 1, max: UINT32_MAX }),
318
+ };
319
+ }
320
+ const objectSize = assertObject(size, path, 'descriptor.size');
321
+ return {
322
+ width: assertIntegerInRange(objectSize.width ?? 1, path, 'descriptor.size.width', { min: 1, max: UINT32_MAX }),
323
+ height: assertIntegerInRange(objectSize.height ?? 1, path, 'descriptor.size.height', { min: 1, max: UINT32_MAX }),
324
+ depthOrArrayLayers: assertIntegerInRange(
325
+ objectSize.depthOrArrayLayers ?? 1,
326
+ path,
327
+ 'descriptor.size.depthOrArrayLayers',
328
+ { min: 1, max: UINT32_MAX },
329
+ ),
330
+ };
331
+ }
332
+
333
+ function assertBindGroupResource(resource, path) {
334
+ if (!resource || typeof resource !== 'object') {
335
+ failValidation(path, 'entry.resource must be an object');
336
+ }
337
+ if ('buffer' in resource) {
338
+ return {
339
+ buffer: assertLiveResource(resource.buffer, path, 'GPUBuffer'),
340
+ offset: assertOptionalIntegerInRange(resource.offset ?? 0, path, 'entry.resource.offset', { min: 0 }),
341
+ size: resource.size === undefined
342
+ ? undefined
343
+ : assertIntegerInRange(resource.size, path, 'entry.resource.size', { min: 1 }),
344
+ };
345
+ }
346
+ if ('_native' in resource) {
347
+ if (describeResourceLabel(resource) === 'GPUSampler') {
348
+ return { sampler: assertLiveResource(resource, path, 'GPUSampler') };
349
+ }
350
+ if (describeResourceLabel(resource) === 'GPUTextureView') {
351
+ return { textureView: assertLiveResource(resource, path, 'GPUTextureView') };
352
+ }
353
+ return {
354
+ buffer: assertLiveResource(resource, path, 'GPUBuffer'),
355
+ offset: 0,
356
+ size: undefined,
357
+ };
358
+ }
359
+ if ('sampler' in resource) {
360
+ return { sampler: assertLiveResource(resource.sampler, path, 'GPUSampler') };
361
+ }
362
+ if ('textureView' in resource) {
363
+ return { textureView: assertLiveResource(resource.textureView, path, 'GPUTextureView') };
364
+ }
365
+ failValidation(path, 'entry.resource must be a GPUBuffer, GPUTextureView, GPUSampler, or { buffer|textureView|sampler, ... }');
366
+ }
367
+
368
+ function normalizeBindGroupLayoutEntry(entry, index, path) {
369
+ const binding = assertObject(entry, path, `descriptor.entries[${index}]`);
370
+ const normalized = {
371
+ binding: assertIntegerInRange(binding.binding, path, `descriptor.entries[${index}].binding`, { min: 0, max: UINT32_MAX }),
372
+ visibility: assertIntegerInRange(binding.visibility, path, `descriptor.entries[${index}].visibility`, { min: 0 }),
373
+ };
374
+ if (binding.buffer) {
375
+ const buffer = assertObject(binding.buffer, path, `descriptor.entries[${index}].buffer`);
376
+ normalized.buffer = {
377
+ type: buffer.type || 'uniform',
378
+ hasDynamicOffset: buffer.hasDynamicOffset === undefined
379
+ ? false
380
+ : assertBoolean(buffer.hasDynamicOffset, path, `descriptor.entries[${index}].buffer.hasDynamicOffset`),
381
+ minBindingSize: assertOptionalIntegerInRange(
382
+ buffer.minBindingSize ?? 0,
383
+ path,
384
+ `descriptor.entries[${index}].buffer.minBindingSize`,
385
+ { min: 0 },
386
+ ) ?? 0,
387
+ };
388
+ }
389
+ if (binding.sampler) {
390
+ normalized.sampler = normalizeSamplerLayout(binding.sampler, path, `descriptor.entries[${index}].sampler`);
391
+ }
392
+ if (binding.texture) {
393
+ normalized.texture = normalizeTextureLayout(binding.texture, path, `descriptor.entries[${index}].texture`);
394
+ }
395
+ if (binding.storageTexture) {
396
+ normalized.storageTexture = normalizeStorageTextureLayout(
397
+ binding.storageTexture,
398
+ path,
399
+ `descriptor.entries[${index}].storageTexture`,
400
+ );
401
+ }
402
+ return normalized;
403
+ }
404
+
405
+ function autoLayoutEntriesFromNativeBindings(bindings, visibility) {
406
+ const groups = new Map();
407
+ for (const binding of bindings ?? []) {
408
+ const entry = (() => {
409
+ if (binding.type === 'buffer') {
410
+ const type = binding.space === 'uniform'
411
+ ? 'uniform'
412
+ : binding.access === 'read'
413
+ ? 'read-only-storage'
414
+ : 'storage';
415
+ return {
416
+ binding: binding.binding,
417
+ visibility,
418
+ buffer: { type },
419
+ };
420
+ }
421
+ if (binding.type === 'sampler') {
422
+ return {
423
+ binding: binding.binding,
424
+ visibility,
425
+ sampler: { type: 'filtering' },
426
+ };
427
+ }
428
+ if (binding.type === 'texture') {
429
+ return {
430
+ binding: binding.binding,
431
+ visibility,
432
+ texture: { sampleType: 'float', viewDimension: '2d', multisampled: false },
433
+ };
434
+ }
435
+ if (binding.type === 'storage_texture') {
436
+ return {
437
+ binding: binding.binding,
438
+ visibility,
439
+ storageTexture: {
440
+ access: binding.access === 'read' ? 'read-only' : 'write-only',
441
+ format: 'rgba8unorm',
442
+ viewDimension: '2d',
443
+ },
444
+ };
445
+ }
446
+ return null;
447
+ })();
448
+ if (!entry) continue;
449
+ const entries = groups.get(binding.group) ?? [];
450
+ entries.push(entry);
451
+ groups.set(binding.group, entries);
452
+ }
453
+ for (const entries of groups.values()) {
454
+ entries.sort((left, right) => left.binding - right.binding);
455
+ }
456
+ return groups;
457
+ }
458
+
459
+ export {
460
+ ALL_BUFFER_USAGE_BITS,
461
+ SAMPLER_BINDING_TYPES,
462
+ TEXTURE_SAMPLE_TYPES,
463
+ TEXTURE_DIMENSIONS,
464
+ TEXTURE_VIEW_DIMENSIONS,
465
+ STORAGE_TEXTURE_ACCESS,
466
+ DEPTH_STENCIL_FORMATS_BASE,
467
+ STORAGE_TEXTURE_FORMATS_BASE,
468
+ FLOAT32_FORMATS,
469
+ ALWAYS_BLENDABLE_FORMATS,
470
+ BC_FORMATS,
471
+ ASTC_FORMATS,
472
+ ASTC_BLOCK_SIZES,
473
+ ETC2_FORMATS,
474
+ isBCFormat,
475
+ isETC2Format,
476
+ isDepthStencilFormat,
477
+ hasStencilAspect,
478
+ isStorageTextureFormat,
479
+ float32SampleType,
480
+ isFloat32Filterable,
481
+ isFloat32Blendable,
482
+ isBlendableFormat,
483
+ isASTCFormat,
484
+ astcBlockSize,
485
+ assertBufferDescriptor,
486
+ normalizeEnumKey,
487
+ normalizeTextureDimension,
488
+ normalizeSamplerLayout,
489
+ normalizeTextureLayout,
490
+ normalizeStorageTextureLayout,
491
+ assertTextureSize,
492
+ assertBindGroupResource,
493
+ normalizeBindGroupLayoutEntry,
494
+ autoLayoutEntriesFromNativeBindings,
495
+ };
@@ -0,0 +1,30 @@
1
+ export const globals = {
2
+ GPUBufferUsage: {
3
+ MAP_READ: 0x0001,
4
+ MAP_WRITE: 0x0002,
5
+ COPY_SRC: 0x0004,
6
+ COPY_DST: 0x0008,
7
+ INDEX: 0x0010,
8
+ VERTEX: 0x0020,
9
+ UNIFORM: 0x0040,
10
+ STORAGE: 0x0080,
11
+ INDIRECT: 0x0100,
12
+ QUERY_RESOLVE: 0x0200,
13
+ },
14
+ GPUShaderStage: {
15
+ VERTEX: 0x1,
16
+ FRAGMENT: 0x2,
17
+ COMPUTE: 0x4,
18
+ },
19
+ GPUMapMode: {
20
+ READ: 0x0001,
21
+ WRITE: 0x0002,
22
+ },
23
+ GPUTextureUsage: {
24
+ COPY_SRC: 0x01,
25
+ COPY_DST: 0x02,
26
+ TEXTURE_BINDING: 0x04,
27
+ STORAGE_BINDING: 0x08,
28
+ RENDER_ATTACHMENT: 0x10,
29
+ },
30
+ };
@@ -29,8 +29,8 @@ This split is intentionally separate from Chromium Track A. Chromium integration
29
29
  depends on the full runtime artifact plus browser-specific gates; it must not
30
30
  depend on npm packaging shape.
31
31
 
32
- Boundary-enforcement and refactor-order details are defined in
33
- [`./layering-plan.md`](./layering-plan.md).
32
+ Boundary-enforcement and refactor-order details are defined in the core/full
33
+ runtime split section of [`./architecture.md`](./architecture.md).
34
34
 
35
35
  ## Dependency contract
36
36
 
package/compat-scope.md DELETED
@@ -1,46 +0,0 @@
1
- # Compatibility Scope: what we actually need
2
-
3
- This note narrows compatibility work to concrete headless integration value for
4
- the current package surface.
5
-
6
- ## Required now
7
-
8
- 1. Stable headless Node/Bun provider behavior for real Doe-native execution.
9
- 2. Stable command/trace orchestration for benchmark and CI pipelines.
10
- 3. Reliable wrappers for:
11
- - Doe native bench runs
12
- - Dawn-vs-Doe compare runs
13
- 4. Deterministic artifact paths and non-zero exit-code propagation.
14
- 5. Minimal convenience entrypoints for Node consumers:
15
- - `create(args?)`
16
- - `globals`
17
- - `requestAdapter`/`requestDevice` convenience helpers
18
- - `setupGlobals` for `navigator.gpu` + enum bootstrap
19
-
20
- The point of this package is headless GPU work in Node/Bun: compute, offscreen
21
- execution, benchmarking, and CI. Compatibility work should serve those
22
- surfaces first.
23
-
24
- ## Optional later (only when demanded by integrations)
25
-
26
- 1. Minimal constants compatibility:
27
- - only constants required by real integrations, not full WebGPU enum surface.
28
- 2. Provider-module swap support for non-default backends beyond `webgpu`.
29
-
30
- ## Not planned by default
31
-
32
- 1. Full `navigator.gpu` browser-parity behavior in Node.
33
- 2. Full object lifetime/event parity (`device lost`, full error scopes, full mapping semantics).
34
- 3. Broad drop-in support for arbitrary npm packages expecting complete `webgpu` behavior.
35
-
36
- Decision rule:
37
-
38
- - Add parity features only after a concrete headless integration is blocked by
39
- a missing capability and cannot be addressed by the existing package,
40
- bridge, or CLI contract.
41
-
42
- Layering note:
43
-
44
- - this file describes the current package surface and its present non-goals
45
- - proposed future `core` vs `full` support contracts are defined separately in
46
- [`./support-contracts.md`](./support-contracts.md)