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