@daugasauron/dolly-host-gpu 0.1.5 → 0.1.6

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 (5) hide show
  1. package/abi.mjs +33 -1
  2. package/bridge.mjs +2 -2
  3. package/gpu.mjs +22 -9
  4. package/package.json +2 -2
  5. package/worker.mjs +1446 -559
package/worker.mjs CHANGED
@@ -1,55 +1,171 @@
1
+ // The gpu@0 provider (docs/gpu.md): a Worker that turns a process's packets into WebGPU calls.
2
+ // A batch is checked in two steps. Each record's decode reads its bytes into a record and checks
3
+ // what the bytes alone decide, for every record before any runs (records). Its run then checks
4
+ // the record against the scope's objects, quotas and open pass, and makes the WebGPU calls.
1
5
  import * as A from "./abi.mjs";
2
6
  import { DOLLY_ERRNO as E } from "./abi.mjs";
3
7
 
4
- const decode = new TextDecoder("utf-8", { fatal: true });
8
+ const utf8 = new TextDecoder("utf-8", { fatal: true });
5
9
  const encode = new TextEncoder();
6
10
  const fail = (code, message) => { throw Object.assign(new Error(message), { errno: code }); };
7
11
  const ensure = (condition, message, code = E.EINVAL) => { if (!condition) fail(code, message); };
8
12
  const textureFormats = [null, "rgba8unorm", "rgba8unorm-srgb", "r8unorm", "rg8unorm", "depth24plus-stencil8", "depth32float",
9
- "bc1-rgba-unorm", "bc2-rgba-unorm", "bc3-rgba-unorm"];
10
- const blockBytes = [0, 4, 4, 1, 2, 4, 4, 8, 16, 16];
13
+ "bc1-rgba-unorm", "bc2-rgba-unorm", "bc3-rgba-unorm",
14
+ "r8snorm", "r8uint", "r8sint", "r16uint", "r16sint", "r16float", "rg8snorm", "rg8uint", "rg8sint", "r32uint", "r32sint",
15
+ "r32float", "rg16uint", "rg16sint", "rg16float", "rgba8snorm", "rgba8uint", "rgba8sint", "bgra8unorm", "bgra8unorm-srgb",
16
+ "rgb9e5ufloat", "rgb10a2uint", "rgb10a2unorm", "rg11b10ufloat", "rg32uint", "rg32sint", "rg32float", "rgba16uint",
17
+ "rgba16sint", "rgba16float", "rgba32uint", "rgba32sint", "rgba32float", "stencil8", "depth16unorm", "depth32float-stencil8",
18
+ "bc1-rgba-unorm-srgb", "bc2-rgba-unorm-srgb", "bc3-rgba-unorm-srgb", "bc4-r-unorm", "bc4-r-snorm", "bc5-rg-unorm",
19
+ "bc5-rg-snorm", "bc6h-rgb-ufloat", "bc6h-rgb-float", "bc7-rgba-unorm", "bc7-rgba-unorm-srgb",
20
+ "r16unorm", "r16snorm", "rg16unorm", "rg16snorm", "rgba16unorm", "rgba16snorm"];
21
+ // Bytes of a texel, or of a 4x4 block for the block-compressed formats.
22
+ const blockBytes = [0, 4, 4, 1, 2, 4, 4, 8, 16, 16, 1, 1, 1, 2, 2, 2, 2, 2, 2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
23
+ 8, 8, 8, 8, 8, 8, 16, 16, 16, 1, 2, 8, 8, 16, 16, 8, 8, 16, 16, 16, 16, 16, 16, 2, 2, 4, 4, 8, 8];
24
+ const compressedFormat = type => (type >= 7 && type <= 9) || (type >= 46 && type <= 56);
25
+ // The sRGB formats, which views of a texture take that has none of their usages (storage).
26
+ const srgbFormat = type => type === 2 || type === 29 || (type >= 46 && type <= 48) || type === 56;
27
+ const depthFormat = type => type === 5 || type === 6 || type === 44 || type === 45;
28
+ const stencilFormat = type => type === 5 || type === 43 || type === 45;
29
+ const viewDimensions = [undefined, "1d", "2d", "2d-array", "cube", "cube-array", "3d"];
30
+ const aspects = ["all", "stencil-only", "depth-only"];
31
+ const stencilOps = ["keep", "zero", "replace", "invert", "increment-clamp", "decrement-clamp", "increment-wrap", "decrement-wrap"];
11
32
  const compareOps = [undefined, "never", "less", "equal", "less-equal", "greater", "not-equal", "greater-equal", "always"];
12
33
  const blendOps = ["add", "subtract", "reverse-subtract", "min", "max"];
13
- const blendFactors = ["zero", "one", "src", "one-minus-src", "dst", "one-minus-dst", "src-alpha", "one-minus-src-alpha", "dst-alpha", "one-minus-dst-alpha", "src-alpha-saturated"];
14
- const vertexFormats = [null, "float32", "float32x2", "float32x3", "float32x4", "unorm8x4", "snorm8x4", "uint8x4", "sint8x4", "unorm16x2", "unorm16x4", "uint16x2", "uint16x4", "float16x2", "float16x4"];
15
- const vertexBytes = [0, 4, 8, 12, 16, 4, 4, 4, 4, 4, 8, 4, 8, 4, 8];
16
- const maxBytes = 4 * 1024 ** 3, bufferCeiling = 1024 ** 3, maxObjects = 4096;
34
+ const blendFactors = ["zero", "one", "src", "one-minus-src", "dst", "one-minus-dst", "src-alpha", "one-minus-src-alpha",
35
+ "dst-alpha", "one-minus-dst-alpha", "src-alpha-saturated", "constant", "one-minus-constant"];
36
+ const vertexFormats = [null, "float32", "float32x2", "float32x3", "float32x4", "unorm8x4", "snorm8x4", "uint8x4", "sint8x4",
37
+ "unorm16x2", "unorm16x4", "uint16x2", "uint16x4", "float16x2", "float16x4",
38
+ "uint8x2", "sint8x2", "unorm8x2", "snorm8x2", "sint16x2", "sint16x4", "snorm16x2", "snorm16x4", "uint32", "uint32x2", "uint32x3",
39
+ "uint32x4", "sint32", "sint32x2", "sint32x3", "sint32x4", "unorm10-10-10-2"];
40
+ const vertexBytes = [0, 4, 8, 12, 16, 4, 4, 4, 4, 4, 8, 4, 8, 4, 8, 2, 2, 2, 2, 4, 8, 4, 8, 4, 8, 12, 16, 4, 8, 12, 16, 4];
41
+ const maxBytes = 4 * 1024 ** 3, bufferCeiling = 1024 ** 3, maxObjects = 65536;
42
+ // A surface shows one presented frame per animation frame, as a display does, and a frame
43
+ // retires once shown and finished by the GPU. Chrome's canvas also waits for its compositor;
44
+ // Firefox's does not, so without this its programs draw unpaced.
45
+ function present(scope, completed) {
46
+ const shown = new Promise(resolve => scope.shows.push(resolve));
47
+ const frame = Promise.all([completed, shown]).finally(() => scope.frames.splice(scope.frames.indexOf(frame), 1));
48
+ scope.frames.push(frame);
49
+ if (scope.shows.length === 1) void show(scope);
50
+ }
51
+ // The texture a surface scope draws a frame into: the canvas's, or, while another scope's surface
52
+ // covers it, a texture of its own, which the page never shows, charged to the quota.
53
+ function surfaceTexture(scope, width, height) {
54
+ if (surfaces.at(-1) === scope) {
55
+ if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; }
56
+ return context.getCurrentTexture();
57
+ }
58
+ if (scope.covered?.width !== width || scope.covered?.height !== height) {
59
+ dropCovered(scope);
60
+ ensure(width * height * 4 <= maxBytes - usedBytes, "Texture allocation quota", E.ENOMEM);
61
+ scope.covered = device.createTexture({ size: [width, height], format,
62
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC });
63
+ usedBytes += width * height * 4;
64
+ }
65
+ return scope.covered;
66
+ }
67
+ function dropCovered(scope) {
68
+ if (!scope.covered) return;
69
+ usedBytes -= scope.covered.width * scope.covered.height * 4;
70
+ scope.covered.destroy();
71
+ scope.covered = null;
72
+ }
73
+ // One frame per animation frame of the runtime Worker; a canvas no page shows waits for none.
74
+ async function show(scope) {
75
+ while (scope.shows.length) {
76
+ const wait = pageFrames && Atomics.waitAsync(pageFrames, 0, Atomics.load(pageFrames, 0));
77
+ if (wait?.async) await wait.value;
78
+ scope.shows.shift()();
79
+ }
80
+ }
81
+ // Firefox settles onSubmittedWorkDone and mapAsync when its queue next submits, or on a 100 ms
82
+ // timer (bug 1870699): while a frame, a WAIT or a map is pending, empty submissions settle it.
83
+ function settled(device, promise) {
84
+ let done = false;
85
+ promise.then(() => { done = true; }, () => { done = true; });
86
+ for (const ms of [2, 5, 10, 20, 40]) setTimeout(() => { if (!done) device.queue.submit([]); }, ms);
87
+ return promise;
88
+ }
17
89
  // What a scope may have queued before its next batch waits: 64 submissions, and
18
- // for a surface three frames (the submissions that present: a frame may submit
19
- // many times). Firefox settles completions on a 100 ms timer (bug 1870699), and
20
- // a model token takes about 25 submissions.
90
+ // for a surface three presented frames not yet retired (a frame may submit many
91
+ // times). Firefox settles completions on a 100 ms timer (bug 1870699), and a
92
+ // model token takes about 25 submissions.
21
93
  const queueFull = scope => scope.surface && scope.frames.length >= 3 ? scope.frames[0]
22
94
  : scope.submissions.length >= 64 ? scope.submissions[0] : null;
23
95
  const timestampQueries = A.DOLLY_GPU_MAX_COMMANDS * 2;
24
- // A batch record's byte length: exact for one of fixed layout, the least for
25
- // one with a variable tail, which records() checks.
26
- const fixedRecordBytes = new Map([[A.DOLLY_GPU_CREATE_BUFFER, 32], [A.DOLLY_GPU_RENDER, 64],
27
- [A.DOLLY_GPU_COMPUTE, 40], [A.DOLLY_GPU_COPY_BUFFER, 48], [A.DOLLY_GPU_MAP_READ, 32], [A.DOLLY_GPU_UNMAP, 16],
28
- [A.DOLLY_GPU_RELEASE, 16], [A.DOLLY_GPU_SUBMIT, 8], [A.DOLLY_GPU_RENDER_VERTEX, 88],
29
- [A.DOLLY_GPU_CAPTURE_FRAME, 32], [A.DOLLY_GPU_CREATE_TEXTURE, 48], [A.DOLLY_GPU_CREATE_SAMPLER, 48],
30
- [A.DOLLY_GPU_BEGIN_RENDER_PASS, 64], [A.DOLLY_GPU_END_RENDER_PASS, 8], [A.DOLLY_GPU_VIEWPORT, 48]]);
31
- const leastRecordBytes = new Map([[A.DOLLY_GPU_WRITE_BUFFER, 32], [A.DOLLY_GPU_CREATE_SHADER, 24],
32
- [A.DOLLY_GPU_RENDER_PIPELINE, 40], [A.DOLLY_GPU_COMPUTE_PIPELINE, 32], [A.DOLLY_GPU_BIND_GROUP, 32],
33
- [A.DOLLY_GPU_VERTEX_PIPELINE, 48], [A.DOLLY_GPU_COMPUTE_CONSTANTS, 32], [A.DOLLY_GPU_WRITE_TEXTURE, 48],
34
- [A.DOLLY_GPU_GRAPHICS_PIPELINE, 112], [A.DOLLY_GPU_RESOURCE_GROUP, 32], [A.DOLLY_GPU_DRAW_MESH, 80]]);
35
96
  // The records a render pass may hold, between BEGIN_RENDER_PASS and END_RENDER_PASS.
36
97
  const renderPassRecords = new Set([A.DOLLY_GPU_CREATE_BUFFER, A.DOLLY_GPU_WRITE_BUFFER, A.DOLLY_GPU_CREATE_SHADER,
37
98
  A.DOLLY_GPU_CREATE_TEXTURE, A.DOLLY_GPU_CREATE_SAMPLER, A.DOLLY_GPU_GRAPHICS_PIPELINE, A.DOLLY_GPU_RESOURCE_GROUP,
38
99
  A.DOLLY_GPU_END_RENDER_PASS, A.DOLLY_GPU_DRAW_MESH, A.DOLLY_GPU_VIEWPORT]);
39
- // The records that create an object, charged to the scope's object quota before they run.
40
- const creatingRecords = new Set([A.DOLLY_GPU_CREATE_BUFFER, A.DOLLY_GPU_CREATE_SHADER, A.DOLLY_GPU_RENDER_PIPELINE,
41
- A.DOLLY_GPU_COMPUTE_PIPELINE, A.DOLLY_GPU_BIND_GROUP, A.DOLLY_GPU_VERTEX_PIPELINE, A.DOLLY_GPU_COMPUTE_CONSTANTS,
42
- A.DOLLY_GPU_CREATE_TEXTURE, A.DOLLY_GPU_CREATE_SAMPLER, A.DOLLY_GPU_GRAPHICS_PIPELINE, A.DOLLY_GPU_RESOURCE_GROUP]);
100
+ // The records a pass begun by BEGIN_VIEW_PASS or BEGIN_COMPUTE_PASS may hold.
101
+ const viewPassRecords = new Set([A.DOLLY_GPU_SET_PIPELINE, A.DOLLY_GPU_SET_BIND_GROUP, A.DOLLY_GPU_SET_VERTEX_BUFFER,
102
+ A.DOLLY_GPU_SET_INDEX_BUFFER, A.DOLLY_GPU_DRAW, A.DOLLY_GPU_DRAW_INDEXED, A.DOLLY_GPU_DRAW_INDIRECT,
103
+ A.DOLLY_GPU_DRAW_INDEXED_INDIRECT, A.DOLLY_GPU_VIEWPORT, A.DOLLY_GPU_SET_BLEND_CONSTANT,
104
+ A.DOLLY_GPU_SET_STENCIL_REFERENCE, A.DOLLY_GPU_END_RENDER_PASS]);
105
+ const computePassRecords = new Set([A.DOLLY_GPU_SET_PIPELINE, A.DOLLY_GPU_SET_BIND_GROUP, A.DOLLY_GPU_DISPATCH,
106
+ A.DOLLY_GPU_DISPATCH_INDIRECT, A.DOLLY_GPU_END_COMPUTE_PASS]);
43
107
  // A request's byte length, header included; BATCH's is its records'.
44
108
  const requestBytes = new Map([[A.DOLLY_GPU_OPEN, 40], [A.DOLLY_GPU_WAIT, 32], [A.DOLLY_GPU_READ, 56],
45
- [A.DOLLY_GPU_CLOSE, 32], [A.DOLLY_GPU_INFO, 32], [A.DOLLY_GPU_CAPABILITIES, 32]]);
46
- let maxBuffer = bufferCeiling, capabilities;
109
+ [A.DOLLY_GPU_CLOSE, 32], [A.DOLLY_GPU_INFO, 32], [A.DOLLY_GPU_CAPABILITIES, 32], [A.DOLLY_GPU_LIMITS, 32]]);
110
+ // The device features and limits LIMITS reports, in its order; the GPU Worker asks the adapter
111
+ // for each limit, bounded by the scope quotas above where they are lower.
112
+ const reportedFeatures = ["depth-clip-control", "depth32float-stencil8", "texture-compression-bc", "timestamp-query",
113
+ "indirect-first-instance", "shader-f16", "rg11b10ufloat-renderable", "float32-filterable", "float32-blendable",
114
+ "subgroups", "texture-component-swizzle", "texture-formats-tier1", "texture-formats-tier2"];
115
+ const reportedLimits = ["maxTextureDimension1D", "maxTextureDimension2D", "maxTextureDimension3D", "maxTextureArrayLayers",
116
+ "maxBindGroups", "maxBindingsPerBindGroup", "maxDynamicUniformBuffersPerPipelineLayout",
117
+ "maxDynamicStorageBuffersPerPipelineLayout", "maxSampledTexturesPerShaderStage", "maxSamplersPerShaderStage",
118
+ "maxStorageBuffersPerShaderStage", "maxStorageTexturesPerShaderStage", "maxUniformBuffersPerShaderStage",
119
+ "maxUniformBufferBindingSize", "maxStorageBufferBindingSize", "minUniformBufferOffsetAlignment",
120
+ "minStorageBufferOffsetAlignment", "maxVertexBuffers", "maxBufferSize", "maxVertexAttributes",
121
+ "maxVertexBufferArrayStride", "maxInterStageShaderVariables", "maxColorAttachments", "maxColorAttachmentBytesPerSample",
122
+ "maxComputeWorkgroupStorageSize", "maxComputeInvocationsPerWorkgroup", "maxComputeWorkgroupSizeX",
123
+ "maxComputeWorkgroupSizeY", "maxComputeWorkgroupSizeZ", "maxComputeWorkgroupsPerDimension",
124
+ "maxStorageBuffersInVertexStage", "maxStorageBuffersInFragmentStage", "maxStorageTexturesInVertexStage",
125
+ "maxStorageTexturesInFragmentStage"];
126
+ const limitCeilings = { maxBufferSize: bufferCeiling, maxStorageBufferBindingSize: bufferCeiling,
127
+ maxComputeWorkgroupStorageSize: 65536 };
128
+ let maxBuffer = bufferCeiling, capabilities, limits;
47
129
  const slots = Array(A.DOLLY_GPU_SLOTS).fill(null), generations = slots.map(() => 0);
48
- let heap, memory, mailbox, control, canvas, context, device, format;
49
- let queueProgress = {queued: 0, completed: 0};
130
+ let heap, memory, mailbox, control, canvas, context, device, format, pageFrames;
131
+ let queueProgress = { queued: 0, completed: 0 };
50
132
  let usedBytes = 0, serial = Promise.resolve(), initializing;
133
+ // The scopes with a surface, in the order they opened it: the page shows the last one's, and the
134
+ // one below when it closes (the kernel admits a surface over another only for a descendant of its
135
+ // process, host/gpu/kernel.c).
136
+ const surfaces = [];
51
137
  const stats = { packets: 0, packetBytes: 0, frames: 0, dispatches: 0, readbackBytes: 0, batchWallMilliseconds: 0 };
52
138
 
139
+ // CAPABILITIES: feature bits, then the quotas and the device's limits, in dolly-gpu-0.wat's order.
140
+ function capabilityBytes(adapter, created) {
141
+ const bytes = new Uint8Array(128), v = new DataView(bytes.buffer), l = created.limits;
142
+ const has = name => created.features.has(name);
143
+ v.setUint32(0, (has("shader-f16") ? 1 : 0) | (has("subgroups") ? 2 : 0) |
144
+ (navigator.gpu.wgslLanguageFeatures?.has("packed_4x8_integer_dot_product") ? 4 : 0) | (has("timestamp-query") ? 8 : 0) |
145
+ A.DOLLY_GPU_FEATURE_CAPTURE_FRAME | A.DOLLY_GPU_FEATURE_TEXTURE_RENDER | A.DOLLY_GPU_FEATURE_LARGE_BATCH |
146
+ A.DOLLY_GPU_FEATURE_VERTEX_F16 | A.DOLLY_GPU_FEATURE_EXPLICIT |
147
+ (has("texture-compression-bc") ? A.DOLLY_GPU_FEATURE_TEXTURE_BC : 0) |
148
+ (format === "bgra8unorm" ? A.DOLLY_GPU_FEATURE_SURFACE_BGRA : 0), true);
149
+ v.setUint32(4, maxObjects, true);
150
+ [maxBuffer, maxBytes, l.maxStorageBufferBindingSize].forEach((n, i) => v.setBigUint64(8 + i * 8, BigInt(n), true));
151
+ [l.minUniformBufferOffsetAlignment, l.minStorageBufferOffsetAlignment, l.maxComputeWorkgroupStorageSize,
152
+ l.maxComputeInvocationsPerWorkgroup, l.maxComputeWorkgroupSizeX, l.maxComputeWorkgroupSizeY,
153
+ l.maxComputeWorkgroupSizeZ, l.maxComputeWorkgroupsPerDimension, A.DOLLY_GPU_MAX_BINDINGS,
154
+ l.maxStorageBuffersPerShaderStage, l.maxUniformBuffersPerShaderStage, l.maxBindGroups]
155
+ .forEach((n, i) => v.setUint32(32 + i * 4, n, true));
156
+ v.setBigUint64(80, BigInt(l.maxUniformBufferBindingSize), true);
157
+ v.setUint32(88, adapter.info?.subgroupMinSize ?? 4, true);
158
+ v.setUint32(92, adapter.info?.subgroupMaxSize ?? 128, true);
159
+ return bytes;
160
+ }
161
+ // LIMITS: a bit for each of reportedFeatures granted, then each of reportedLimits.
162
+ function limitBytes(created) {
163
+ const bytes = new Uint8Array(8 + reportedLimits.length * 8), v = new DataView(bytes.buffer);
164
+ const features = reportedFeatures.reduce((bits, name, i) => created.features.has(name) ? bits | (1 << i) : bits, 0);
165
+ v.setBigUint64(0, BigInt(features), true);
166
+ reportedLimits.forEach((name, i) => v.setBigUint64(8 + i * 8, BigInt(created.limits[name] ?? 0), true));
167
+ return bytes;
168
+ }
53
169
  async function getDevice() {
54
170
  if (device) return device;
55
171
  if (initializing) return initializing;
@@ -57,39 +173,19 @@ async function getDevice() {
57
173
  ensure(navigator.gpu, "This browser has no WebGPU provider", E.ENODEV);
58
174
  const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" });
59
175
  ensure(adapter, "This browser did not provide a GPU adapter", E.ENODEV);
60
- const requiredFeatures = ["timestamp-query", "shader-f16", "subgroups", "texture-compression-bc"].filter(name => adapter.features.has(name));
176
+ const requiredFeatures = reportedFeatures.filter(name => adapter.features.has(name));
61
177
  const requiredLimits = {};
62
- for (const [name, ceiling] of Object.entries({maxBufferSize: bufferCeiling,
63
- maxStorageBufferBindingSize: bufferCeiling, maxStorageBuffersPerShaderStage: A.DOLLY_GPU_MAX_BINDINGS,
64
- maxComputeWorkgroupStorageSize: 65536, maxComputeInvocationsPerWorkgroup: 1024,
65
- maxComputeWorkgroupSizeX: 1024, maxComputeWorkgroupSizeY: 1024})) {
66
- requiredLimits[name] = Math.min(adapter.limits[name], ceiling);
67
- }
68
- const created = await adapter.requestDevice({requiredFeatures, requiredLimits});
178
+ for (const name of reportedLimits) if (adapter.limits[name] !== undefined)
179
+ requiredLimits[name] = Math.min(adapter.limits[name], limitCeilings[name] ?? Infinity);
180
+ const created = await adapter.requestDevice({ requiredFeatures, requiredLimits });
69
181
  format = navigator.gpu.getPreferredCanvasFormat();
70
182
  maxBuffer = created.limits.maxBufferSize;
71
- const l = created.limits;
72
- capabilities = new Uint8Array(128);
73
- const v = new DataView(capabilities.buffer);
74
- v.setUint32(0, (created.features.has("shader-f16") ? 1 : 0) | (created.features.has("subgroups") ? 2 : 0) |
75
- (navigator.gpu.wgslLanguageFeatures?.has("packed_4x8_integer_dot_product") ? 4 : 0) |
76
- (created.features.has("timestamp-query") ? 8 : 0) | A.DOLLY_GPU_FEATURE_CAPTURE_FRAME | A.DOLLY_GPU_FEATURE_TEXTURE_RENDER | A.DOLLY_GPU_FEATURE_LARGE_BATCH | A.DOLLY_GPU_FEATURE_VERTEX_F16 |
77
- (created.features.has("texture-compression-bc") ? A.DOLLY_GPU_FEATURE_TEXTURE_BC : 0) |
78
- (format === "bgra8unorm" ? A.DOLLY_GPU_FEATURE_SURFACE_BGRA : 0), true);
79
- v.setUint32(4, maxObjects, true);
80
- [maxBuffer, maxBytes, l.maxStorageBufferBindingSize].forEach((n,i) => v.setBigUint64(8+i*8, BigInt(n), true));
81
- [l.minUniformBufferOffsetAlignment, l.minStorageBufferOffsetAlignment, l.maxComputeWorkgroupStorageSize,
82
- l.maxComputeInvocationsPerWorkgroup, l.maxComputeWorkgroupSizeX, l.maxComputeWorkgroupSizeY,
83
- l.maxComputeWorkgroupSizeZ, l.maxComputeWorkgroupsPerDimension, A.DOLLY_GPU_MAX_BINDINGS,
84
- l.maxStorageBuffersPerShaderStage, l.maxUniformBuffersPerShaderStage, l.maxBindGroups]
85
- .forEach((n,i) => v.setUint32(32+i*4,n,true));
86
- v.setBigUint64(80,BigInt(l.maxUniformBufferBindingSize),true);
87
- v.setUint32(88,adapter.info?.subgroupMinSize ?? 4,true);
88
- v.setUint32(92,adapter.info?.subgroupMaxSize ?? 128,true);
183
+ capabilities = capabilityBytes(adapter, created);
184
+ limits = limitBytes(created);
89
185
  device = created;
90
186
  const { vendor, architecture, description, isFallbackAdapter } = adapter.info;
91
187
  postMessage({ type: "status", info: { vendor, architecture, description, isFallbackAdapter, f16: created.features.has("shader-f16") } });
92
- queueProgress = {queued: 0, completed: 0};
188
+ queueProgress = { queued: 0, completed: 0 };
93
189
  if (canvas) {
94
190
  context = canvas.getContext("webgpu");
95
191
  ensure(context, "WebGPU canvas unavailable", E.ENOSYS);
@@ -108,129 +204,66 @@ async function getDevice() {
108
204
  finally { initializing = null; }
109
205
  }
110
206
 
111
- function integer(v, o) {
112
- const high = v.getUint32(o + 4, true);
207
+ const u32 = (v, at) => v.getUint32(at, true);
208
+ const f32 = (v, at) => v.getFloat32(at, true);
209
+ const zero64 = (v, at) => v.getBigUint64(at, true) === 0n;
210
+ // A 64-bit field: a handle, an offset or a size, at most 2^53 - 1.
211
+ function integer(v, at) {
212
+ const high = u32(v, at + 4);
113
213
  ensure(high <= 0x1fffff, "GPU integer exceeds host range");
114
- return high * 0x100000000 + v.getUint32(o, true);
214
+ return high * 0x100000000 + u32(v, at);
115
215
  }
116
216
  function header(bytes) {
117
217
  ensure(bytes.length >= 32, "Short GPU packet");
118
218
  const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
119
219
  const scope = integer(v, 8), sequence = integer(v, 16);
120
- ensure(v.getUint32(0, true) === A.DOLLY_GPU_VERSION && v.getUint32(28, true) === 0 &&
121
- v.getUint32(24, true) === bytes.length - 32 && scope > 0 && scope <= 0xffffffff &&
122
- sequence > 0 && sequence <= 0xffffffff, "Invalid GPU header");
123
- return { op: v.getUint32(4, true), scope, sequence, index: (scope - 1) % slots.length, bytes, v };
220
+ ensure(u32(v, 0) === A.DOLLY_GPU_VERSION && u32(v, 28) === 0 && u32(v, 24) === bytes.length - 32 &&
221
+ scope > 0 && scope <= 0xffffffff && sequence > 0 && sequence <= 0xffffffff, "Invalid GPU header");
222
+ return { op: u32(v, 4), scope, sequence, index: (scope - 1) % slots.length, bytes, v };
124
223
  }
125
224
  function text(bytes, start, count) {
126
- ensure(count > 0 && count <= 128 * 1024 && start <= bytes.length - count, "Invalid GPU text span");
127
- const value = decode.decode(bytes.subarray(start, start + count));
225
+ ensure(count > 0 && start <= bytes.length - count, "Invalid GPU text span");
226
+ const value = utf8.decode(bytes.subarray(start, start + count));
128
227
  ensure(!value.includes("\0"), "NUL in GPU text");
129
228
  return value;
130
229
  }
230
+
231
+ // A batch's records: framed, each decoded by its operation, before any runs.
131
232
  function records(request) {
132
233
  const { bytes, v } = request;
133
- ensure(bytes.length >= 40 && v.getUint32(36, true) === 0, "Invalid GPU batch");
134
- const count = v.getUint32(32, true), result = [];
234
+ ensure(bytes.length >= 40 && (u32(v, 36) & ~A.DOLLY_GPU_BATCH_CONTINUE) === 0, "Invalid GPU batch");
235
+ const count = u32(v, 32), result = [];
135
236
  ensure(count <= A.DOLLY_GPU_MAX_COMMANDS, "Too many GPU commands", E.E2BIG);
136
237
  let offset = 40;
137
- for (let i = 0; i < count; ++i) {
238
+ for (let position = 0; position < count; ++position) {
138
239
  ensure(offset <= bytes.length - 8, "Truncated GPU command");
139
- const opcode = v.getUint32(offset, true), size = v.getUint32(offset + 4, true);
240
+ const op = u32(v, offset), size = u32(v, offset + 4), definition = operations.get(op);
140
241
  ensure(size >= 8 && size % 8 === 0 && size <= bytes.length - offset, "Invalid GPU command length");
242
+ if (definition?.bytes) ensure(size === definition.bytes, "Wrong GPU command layout");
243
+ else ensure(definition && size >= definition.least, "Unsupported GPU command", E.ENOTSUP);
141
244
  const b = bytes.subarray(offset, offset + size), w = new DataView(b.buffer, b.byteOffset, b.byteLength);
142
- if (fixedRecordBytes.has(opcode)) ensure(size === fixedRecordBytes.get(opcode), "Wrong GPU command layout");
143
- else ensure(leastRecordBytes.has(opcode) && size >= leastRecordBytes.get(opcode), "Unsupported GPU command", E.ENOTSUP);
144
- // Validate every variable-length span before any command has side effects.
145
- if (opcode === A.DOLLY_GPU_WRITE_BUFFER) {
146
- const at = w.getUint32(24, true), n = w.getUint32(28, true);
147
- ensure(at >= 32 && at <= size - n && n % 4 === 0, "Invalid GPU upload span");
148
- } else if (opcode === A.DOLLY_GPU_CREATE_SHADER) {
149
- ensure(w.getUint32(20, true) === 0, "Shader reserved field");
150
- text(b, 24, w.getUint32(16, true));
151
- } else if (opcode === A.DOLLY_GPU_RENDER_PIPELINE || opcode === A.DOLLY_GPU_VERTEX_PIPELINE) {
152
- const vs = w.getUint32(32, true), fs = w.getUint32(36, true);
153
- ensure(vs <= 64 && fs <= 64, "GPU entry name too long");
154
- let at = 40;
155
- if (opcode === A.DOLLY_GPU_VERTEX_PIPELINE) {
156
- const count = w.getUint32(44, true), stride = w.getUint32(40, true);
157
- ensure(count > 0 && count <= 8 && stride > 0 && stride <= 2048 && stride % 4 === 0 && size >= 48 + count * 16, "Vertex layout");
158
- const locations = new Set();
159
- for (let i = 0; i < count; i++) {
160
- const o = 48 + i * 16, location = w.getUint32(o, true), components = w.getUint32(o + 4, true), offset = w.getUint32(o + 8, true);
161
- ensure(location < 16 && !locations.has(location) && components >= 2 && components <= 4 && offset % 4 === 0 && offset <= stride - components * 4 && w.getUint32(o + 12, true) === 0, "Vertex attribute");
162
- locations.add(location);
163
- }
164
- at = 48 + count * 16;
165
- }
166
- text(b, at, vs); text(b, at + vs, fs);
167
- } else if (opcode === A.DOLLY_GPU_COMPUTE_PIPELINE) {
168
- ensure(w.getUint32(28, true) === 0 && w.getUint32(24, true) <= 64, "Compute entry name");
169
- text(b, 32, w.getUint32(24, true));
170
- } else if (opcode === A.DOLLY_GPU_COMPUTE_CONSTANTS) {
171
- const entryBytes = w.getUint32(24,true), count = w.getUint32(28,true);
172
- ensure(entryBytes <= 64 && count <= 16, "Compute constants layout");
173
- text(b,32,entryBytes);
174
- let at = (32+entryBytes+7)&~7;
175
- const names = new Set();
176
- for (let i=0;i<count;i++) {
177
- ensure(at<=size-16,"Truncated compute constant");
178
- const n=w.getUint32(at,true);
179
- ensure(n<=64 && w.getUint32(at+4,true)===0 && Number.isFinite(w.getFloat64(at+8,true)),"Invalid compute constant");
180
- const name=text(b,at+16,n);
181
- ensure(!names.has(name),"Duplicate compute constant");names.add(name);
182
- at=(at+16+n+7)&~7;
183
- }
184
- ensure(at===size,"Trailing compute constant bytes");
185
- } else if (opcode === A.DOLLY_GPU_BIND_GROUP) {
186
- const n = w.getUint32(24, true);
187
- ensure(n <= A.DOLLY_GPU_MAX_BINDINGS && size === 32 + n * 24 && w.getUint32(28, true) === 0, "Bind group layout");
188
- }
189
- if (opcode === A.DOLLY_GPU_WRITE_TEXTURE) {
190
- ensure(w.getUint32(44,true) === 0 && w.getUint32(40,true) > 0 &&
191
- Math.ceil((48+w.getUint32(40,true))/8)*8 === size, "Texture upload span");
192
- } else if (opcode === A.DOLLY_GPU_GRAPHICS_PIPELINE) {
193
- const buffers=w.getUint32(104,true), attributes=w.getUint32(108,true);
194
- ensure(buffers<=8 && attributes<=16 && size===112+16*(buffers+attributes), "Graphics vertex layout");
195
- const locations=new Set();
196
- for(let i=0;i<buffers;i++) {
197
- const at=112+i*16, stride=w.getUint32(at,true);
198
- ensure(stride>0 && stride<=2048 && stride%4===0 && w.getUint32(at+4,true)<=1 &&
199
- w.getBigUint64(at+8,true)===0n, "Graphics vertex stride");
200
- }
201
- for(let i=0;i<attributes;i++) {
202
- const at=112+(buffers+i)*16, slot=w.getUint32(at,true), location=w.getUint32(at+4,true),
203
- type=w.getUint32(at+8,true), offset=w.getUint32(at+12,true);
204
- ensure(slot<buffers && location<16 && !locations.has(location) && type>0 && type<vertexFormats.length &&
205
- offset%Math.min(4,vertexBytes[type])===0 && offset<=w.getUint32(112+slot*16,true)-vertexBytes[type], "Graphics vertex attribute");
206
- locations.add(location);
207
- }
208
- } else if (opcode === A.DOLLY_GPU_RESOURCE_GROUP) {
209
- ensure(w.getUint32(24,true)<4 && w.getUint32(28,true)<=16 && size===32+32*w.getUint32(28,true), "Resource group layout");
210
- const bindings=new Set();
211
- for(let i=0;i<w.getUint32(28,true);i++) {
212
- const at=32+i*32, binding=w.getUint32(at,true), kind=w.getUint32(at+4,true);
213
- ensure(binding<32 && !bindings.has(binding) && kind<=3 && (kind===0 ||
214
- (w.getBigUint64(at+16,true)===0n && w.getBigUint64(at+24,true)===0n)), "Resource binding");
215
- bindings.add(binding);
216
- }
217
- } else if (opcode === A.DOLLY_GPU_DRAW_MESH) {
218
- const groups=w.getUint32(64,true), buffers=w.getUint32(68,true);
219
- ensure(groups<=4 && buffers<=8 && size===80+8*groups+24*buffers && w.getBigUint64(72,true)===0n, "Mesh layout");
245
+ try {
246
+ const record = definition.decode?.(w, b) ?? {};
247
+ record.op = op;
248
+ record.position = position;
249
+ result.push(record);
250
+ } catch (error) {
251
+ fail(error.errno ?? E.EINVAL, `record ${position} (opcode ${op}): ${error.message}`);
220
252
  }
221
- result.push({ opcode, b, w });
222
253
  offset += size;
223
254
  }
224
255
  ensure(offset === bytes.length, "Trailing GPU packet bytes");
225
256
  return result;
226
257
  }
258
+
227
259
  function object(scope, id, kind) {
228
260
  const result = scope.objects.get(id);
229
261
  ensure(result && (!kind || result.kind === kind), "Invalid GPU resource handle", E.EBADF);
230
262
  return result;
231
263
  }
232
264
  function insert(scope, id, kind, value, size = 0) {
233
- ensure(id > scope.high && id <= 0xffffffff && scope.objects.size + scope.retired.size < maxObjects, "Invalid or exhausted GPU object IDs", E.ENOSPC);
265
+ ensure(id > scope.high && id <= 0xffffffff && scope.objects.size + scope.retired.size < maxObjects,
266
+ "Invalid or exhausted GPU object IDs", E.ENOSPC);
234
267
  scope.high = id;
235
268
  scope.objects.set(id, { kind, value, size, mapped: null });
236
269
  }
@@ -268,13 +301,20 @@ function release(scope, id, progress) {
268
301
  function retire(scope) {
269
302
  if (scope.retiring) return scope.retiring;
270
303
  scope.lost = true;
271
- if (scope.surface) postMessage({ type: "status", active: false });
304
+ if (scope.surface) {
305
+ const shown = surfaces.at(-1) === scope;
306
+ surfaces.splice(surfaces.indexOf(scope), 1);
307
+ // The surface below is shown again from its next frame.
308
+ const below = surfaces.at(-1);
309
+ if (shown) postMessage(below ? { type: "status", active: true, ...below.size } : { type: "status", active: false });
310
+ }
272
311
  scope.retiring = (async () => {
273
312
  try { if (scope.device) await scope.device.queue.onSubmittedWorkDone(); } catch {}
274
313
  for (const r of scope.objects.values()) {
275
314
  r.value.destroy?.();
276
315
  usedBytes -= r.size;
277
316
  }
317
+ dropCovered(scope);
278
318
  scope.objects.clear();
279
319
  collectReleased(scope, Infinity);
280
320
  for (const t of scope.timers ?? []) { t.query.destroy(); t.resolve.destroy(); t.read.destroy(); }
@@ -284,439 +324,1286 @@ function retire(scope) {
284
324
  return scope.retiring;
285
325
  }
286
326
 
287
- async function batch(scope, commands) {
288
- const device = scope.device;
289
- const progress = queueProgress;
327
+ // The batch being run: its scope, device and queue progress, the pipelines it compiles, and its
328
+ // encoder, open pass and surface texture, which a batch sent with BATCH_CONTINUE leaves to the
329
+ // scope's next one.
330
+ const commandEncoder = batch => batch.encoder ??= batch.device.createCommandEncoder();
331
+ function endCompute(batch) {
332
+ batch.computePass?.end();
333
+ batch.computePass = null;
334
+ batch.explicitCompute = false;
335
+ }
336
+ function endRender(batch) {
337
+ batch.renderPass?.end();
338
+ batch.renderPass = null;
339
+ batch.viewPass = false;
340
+ }
341
+ // A pass's timestamp writes; an encoder's passes beyond the query set's pairs go untimed.
342
+ function timestamps(batch) {
343
+ batch.timer ??= batch.scope.timers?.find(t => !t.busy);
344
+ if (!batch.timer || batch.queries + 2 > timestampQueries) return {};
345
+ const beginningOfPassWriteIndex = batch.queries++, endOfPassWriteIndex = batch.queries++;
346
+ return { timestampWrites: { querySet: batch.timer.query, beginningOfPassWriteIndex, endOfPassWriteIndex } };
347
+ }
348
+ // The texture the batch draws the scope's frame into, one size for the whole batch.
349
+ function surfaceFrame(batch, width, height) {
350
+ if (!batch.texture) batch.texture = surfaceTexture(batch.scope, width, height);
351
+ else ensure(batch.texture.width === width && batch.texture.height === height, "Surface resized within batch");
352
+ return batch.texture;
353
+ }
354
+ // A batch's shaders and pipelines are created together and awaited before the batch first sets
355
+ // a pipeline, and at its end: Chrome compiles them in parallel so, one after another when each is
356
+ // awaited in turn.
357
+ function compile(batch, record, kind, creation, value = null) {
358
+ creation.catch(() => {});
359
+ insert(batch.scope, record.id, kind, value);
360
+ const entry = batch.scope.objects.get(record.id), promise = creation.then(created => { entry.value = created; });
361
+ promise.catch(() => {});
362
+ batch.compiling.push({ record, promise });
363
+ }
364
+ async function compiled(batch) {
365
+ const pending = batch.compiling.splice(0);
366
+ for (const [i, result] of (await Promise.allSettled(pending.map(c => c.promise))).entries()) {
367
+ if (result.status === "fulfilled") continue;
368
+ const { record } = pending[i], error = result.reason;
369
+ batch.scope.objects.delete(record.id);
370
+ throw Object.assign(new Error(`record ${record.position} (opcode ${record.op}): ${error.message}`),
371
+ { errno: error.errno ?? (error.reason === "validation" ? E.EINVAL : E.EIO), record: true });
372
+ }
373
+ }
374
+
375
+ // Each record's operation: its byte length (`bytes` exactly, or at least `least` for a variable
376
+ // tail), whether it creates an object (charged to the scope's object quota before it runs), its
377
+ // decode and its run.
378
+ const operations = new Map();
379
+ const operation = (op, layout) => operations.set(op, layout);
380
+ const bufferRange = (w, at) => ({ buffer: integer(w, at), offset: integer(w, at + 8), size: integer(w, at + 16) });
381
+
382
+ operation(A.DOLLY_GPU_CREATE_BUFFER, { bytes: 32, creates: true,
383
+ decode(w) {
384
+ const usage = u32(w, 24);
385
+ ensure(u32(w, 28) === 0 && usage > 0 && (usage & ~1023) === 0, "Buffer usage");
386
+ return { id: integer(w, 8), size: integer(w, 16), usage };
387
+ },
388
+ run({ scope, device }, { id, size, usage }) {
389
+ ensure(size > 0 && size <= maxBuffer && size <= maxBytes - usedBytes, "GPU allocation quota", E.ENOMEM);
390
+ insert(scope, id, "buffer", device.createBuffer({ size, usage }), size);
391
+ usedBytes += size;
392
+ } });
393
+
394
+ operation(A.DOLLY_GPU_WRITE_BUFFER, { least: 32,
395
+ decode(w, b) {
396
+ const offset = integer(w, 16), at = u32(w, 24), length = u32(w, 28);
397
+ ensure(at >= 32 && at <= b.length - length && length % 4 === 0, "Invalid GPU upload span");
398
+ ensure(offset % 4 === 0, "Unaligned GPU upload");
399
+ return { buffer: integer(w, 8), offset, data: b.subarray(at, at + length) };
400
+ },
401
+ run({ scope, device, progress }, { buffer, offset, data }) {
402
+ const r = object(scope, buffer, "buffer");
403
+ range(r, offset, data.length);
404
+ ensure(!r.mapped, "Upload into a mapped buffer");
405
+ device.queue.writeBuffer(r.value, offset, data);
406
+ progress.queued++;
407
+ } });
408
+
409
+ operation(A.DOLLY_GPU_CREATE_SHADER, { least: 24, creates: true,
410
+ decode(w, b) {
411
+ ensure(u32(w, 20) === 0, "Shader reserved field");
412
+ return { id: integer(w, 8), code: text(b, 24, u32(w, 16)) };
413
+ },
414
+ run(batch, record) {
415
+ const shader = batch.device.createShaderModule({ code: record.code });
416
+ compile(batch, record, "shader", shader.getCompilationInfo().then(info => {
417
+ const errors = info.messages.filter(m => m.type === "error");
418
+ ensure(!errors.length, errors.map(m => `${m.lineNum}:${m.linePos} ${m.message}`).join("\n").slice(0, 2048));
419
+ return shader;
420
+ }), shader);
421
+ } });
422
+
423
+ // The compact pipelines: one module's two entry points, drawn into the surface's format, and for
424
+ // VERTEX_PIPELINE one buffer of float vectors.
425
+ function decodeCompactPipeline(w, b, vertexLayout) {
426
+ const topology = u32(w, 24), blend = u32(w, 28), vs = u32(w, 32), fs = u32(w, 36);
427
+ ensure(vs <= 64 && fs <= 64, "GPU entry name too long");
428
+ ensure(topology <= 1 && blend <= 2, "Unsupported render state");
429
+ let at = 40, buffers = [];
430
+ if (vertexLayout) {
431
+ const stride = u32(w, 40), count = u32(w, 44), attributes = [], locations = new Set();
432
+ ensure(count > 0 && count <= 8 && stride > 0 && stride <= 2048 && stride % 4 === 0 && b.length >= 48 + count * 16, "Vertex layout");
433
+ for (let i = 0; i < count; i++) {
434
+ const o = 48 + i * 16, location = u32(w, o), components = u32(w, o + 4), offset = u32(w, o + 8);
435
+ ensure(location < 16 && !locations.has(location) && components >= 2 && components <= 4 && offset % 4 === 0 &&
436
+ offset <= stride - components * 4 && u32(w, o + 12) === 0, "Vertex attribute");
437
+ locations.add(location);
438
+ attributes.push({ shaderLocation: location, format: `float32x${components}`, offset });
439
+ }
440
+ buffers = [{ arrayStride: stride, stepMode: "vertex", attributes }];
441
+ at = 48 + count * 16;
442
+ }
443
+ return { id: integer(w, 8), shader: integer(w, 16), topology, blend, buffers,
444
+ vertexEntry: text(b, at, vs), fragmentEntry: text(b, at + vs, fs) };
445
+ }
446
+ async function runCompactPipeline(batch, record) {
447
+ await compiled(batch);
448
+ const module = object(batch.scope, record.shader, "shader").value;
449
+ const component = { srcFactor: "one", dstFactor: record.blend === 2 ? "one" : "one-minus-src-alpha", operation: "add" };
450
+ const target = { format, ...(record.blend ? { blend: { color: component, alpha: component } } : {}) };
451
+ const pipeline = await batch.device.createRenderPipelineAsync({ layout: "auto",
452
+ vertex: { module, entryPoint: record.vertexEntry, buffers: record.buffers },
453
+ fragment: { module, entryPoint: record.fragmentEntry, targets: [target] },
454
+ primitive: { topology: record.topology ? "triangle-strip" : "triangle-list" } });
455
+ insert(batch.scope, record.id, "render", pipeline);
456
+ }
457
+ operation(A.DOLLY_GPU_RENDER_PIPELINE, { least: 40, creates: true,
458
+ decode: (w, b) => decodeCompactPipeline(w, b, false), run: runCompactPipeline });
459
+ operation(A.DOLLY_GPU_VERTEX_PIPELINE, { least: 48, creates: true,
460
+ decode: (w, b) => decodeCompactPipeline(w, b, true), run: runCompactPipeline });
461
+
462
+ async function runCompactCompute(batch, record) {
463
+ await compiled(batch);
464
+ const module = object(batch.scope, record.shader, "shader").value;
465
+ const pipeline = await batch.device.createComputePipelineAsync({ layout: "auto",
466
+ compute: { module, entryPoint: record.entry, constants: record.constants } });
467
+ insert(batch.scope, record.id, "compute", pipeline);
468
+ }
469
+ operation(A.DOLLY_GPU_COMPUTE_PIPELINE, { least: 32, creates: true,
470
+ decode(w, b) {
471
+ ensure(u32(w, 28) === 0 && u32(w, 24) <= 64, "Compute entry name");
472
+ return { id: integer(w, 8), shader: integer(w, 16), entry: text(b, 32, u32(w, 24)), constants: Object.create(null) };
473
+ },
474
+ run: runCompactCompute });
475
+ operation(A.DOLLY_GPU_COMPUTE_CONSTANTS, { least: 32, creates: true,
476
+ decode(w, b) {
477
+ const entryBytes = u32(w, 24), count = u32(w, 28), constants = Object.create(null);
478
+ ensure(entryBytes <= 64 && count <= 16, "Compute constants layout");
479
+ const entry = text(b, 32, entryBytes);
480
+ let at = (32 + entryBytes + 7) & ~7;
481
+ for (let i = 0; i < count; i++) {
482
+ ensure(at <= b.length - 16, "Truncated compute constant");
483
+ const n = u32(w, at), value = w.getFloat64(at + 8, true);
484
+ ensure(n <= 64 && u32(w, at + 4) === 0 && Number.isFinite(value), "Invalid compute constant");
485
+ const name = text(b, at + 16, n);
486
+ ensure(!(name in constants), "Duplicate compute constant");
487
+ constants[name] = value;
488
+ at = (at + 16 + n + 7) & ~7;
489
+ }
490
+ ensure(at === b.length, "Trailing compute constant bytes");
491
+ return { id: integer(w, 8), shader: integer(w, 16), entry, constants };
492
+ },
493
+ run: runCompactCompute });
494
+
495
+ operation(A.DOLLY_GPU_BIND_GROUP, { least: 32, creates: true,
496
+ decode(w, b) {
497
+ const count = u32(w, 24), buffers = [];
498
+ ensure(count <= A.DOLLY_GPU_MAX_BINDINGS && b.length === 32 + count * 24 && u32(w, 28) === 0, "Bind group layout");
499
+ for (let i = 0; i < count; ++i) buffers.push(bufferRange(w, 32 + i * 24));
500
+ return { id: integer(w, 8), pipeline: integer(w, 16), buffers };
501
+ },
502
+ run({ scope, device }, record) {
503
+ const pipeline = object(scope, record.pipeline);
504
+ ensure(pipeline.kind === "render" || pipeline.kind === "compute", "Invalid binding pipeline");
505
+ const entries = record.buffers.map(({ buffer, offset, size }, binding) => {
506
+ const r = object(scope, buffer, "buffer");
507
+ range(r, offset, size);
508
+ return { binding, resource: { buffer: r.value, offset, size } };
509
+ });
510
+ insert(scope, record.id, "group", device.createBindGroup({ layout: pipeline.value.getBindGroupLayout(0), entries }));
511
+ } });
512
+
513
+ // A compact draw: a "render" pipeline into the scope's surface, in a render pass of its own.
514
+ function decodeSurfaceDraw(w, vertexBuffer) {
515
+ const vertices = u32(w, 24), instances = u32(w, 28), width = u32(w, 32), height = u32(w, 36), load = u32(w, 56);
516
+ const clearValue = [40, 44, 48, 52].map(at => f32(w, at));
517
+ ensure(width > 0 && height > 0 && width <= 4096 && height <= 2304, "Invalid GPU surface dimensions");
518
+ ensure(u32(w, 60) === 0 && load <= 1, "Render reserved field");
519
+ ensure(vertices > 0 && instances > 0 && vertices * instances <= 4 * 1024 * 1024, "Draw limit", E.E2BIG);
520
+ ensure(clearValue.every(Number.isFinite), "Invalid clear color");
521
+ return { pipeline: integer(w, 8), group: integer(w, 16), vertices, instances, width, height, load, clearValue,
522
+ vertexBuffer: vertexBuffer ? bufferRange(w, 64) : null };
523
+ }
524
+ function runSurfaceDraw(batch, record) {
525
+ const { scope } = batch;
526
+ ensure(scope.surface, "Invalid GPU surface dimensions");
527
+ const pipeline = object(scope, record.pipeline, "render").value;
528
+ const group = record.group ? object(scope, record.group, "group").value : null;
529
+ let vertices = null;
530
+ if (record.vertexBuffer) {
531
+ vertices = object(scope, record.vertexBuffer.buffer, "buffer");
532
+ range(vertices, record.vertexBuffer.offset, record.vertexBuffer.size);
533
+ }
534
+ const texture = surfaceFrame(batch, record.width, record.height);
535
+ const pass = commandEncoder(batch).beginRenderPass({ ...timestamps(batch), colorAttachments: [{ view: texture.createView(),
536
+ clearValue: record.clearValue, loadOp: record.load ? "clear" : "load", storeOp: "store" }] });
537
+ pass.setPipeline(pipeline);
538
+ if (group) pass.setBindGroup(0, group);
539
+ if (vertices) pass.setVertexBuffer(0, vertices.value, record.vertexBuffer.offset, record.vertexBuffer.size);
540
+ pass.draw(record.vertices, record.instances);
541
+ pass.end();
542
+ }
543
+ operation(A.DOLLY_GPU_RENDER, { bytes: 64, decode: w => decodeSurfaceDraw(w, false), run: runSurfaceDraw });
544
+ operation(A.DOLLY_GPU_RENDER_VERTEX, { bytes: 88, decode: w => decodeSurfaceDraw(w, true), run: runSurfaceDraw });
545
+
546
+ operation(A.DOLLY_GPU_COMPUTE, { bytes: 40,
547
+ decode(w) {
548
+ const workgroups = [24, 28, 32].map(at => u32(w, at));
549
+ ensure(u32(w, 36) === 0 && workgroups.every(n => n > 0 && n <= 65535) && workgroups.reduce((a, n) => a * n, 1) <= 1048576,
550
+ "Dispatch limit", E.E2BIG);
551
+ return { pipeline: integer(w, 8), group: integer(w, 16), workgroups };
552
+ },
553
+ run(batch, record) {
554
+ const pipeline = object(batch.scope, record.pipeline, "compute").value, group = object(batch.scope, record.group, "group").value;
555
+ // Consecutive COMPUTE records share one pass.
556
+ batch.computePass ??= commandEncoder(batch).beginComputePass(timestamps(batch));
557
+ batch.computePass.setPipeline(pipeline);
558
+ batch.computePass.setBindGroup(0, group);
559
+ batch.computePass.dispatchWorkgroups(...record.workgroups);
560
+ stats.dispatches++;
561
+ } });
562
+
563
+ operation(A.DOLLY_GPU_COPY_BUFFER, { bytes: 48,
564
+ decode: w => ({ source: integer(w, 8), destination: integer(w, 16), sourceOffset: integer(w, 24),
565
+ destinationOffset: integer(w, 32), size: integer(w, 40) }),
566
+ run(batch, { source, destination, sourceOffset, destinationOffset, size }) {
567
+ const from = object(batch.scope, source, "buffer"), to = object(batch.scope, destination, "buffer");
568
+ range(from, sourceOffset, size);
569
+ range(to, destinationOffset, size);
570
+ commandEncoder(batch).copyBufferToBuffer(from.value, sourceOffset, to.value, destinationOffset, size);
571
+ } });
572
+
573
+ operation(A.DOLLY_GPU_MAP_READ, { bytes: 32,
574
+ decode(w) {
575
+ const size = integer(w, 24);
576
+ ensure(size <= 16 * 1024 * 1024, "Mapping limit", E.E2BIG);
577
+ return { buffer: integer(w, 8), offset: integer(w, 16), size };
578
+ },
579
+ async run(batch, { buffer, offset, size }) {
580
+ ensure(!batch.encoder, "Submit before mapping");
581
+ const r = object(batch.scope, buffer, "buffer");
582
+ range(r, offset, size);
583
+ ensure(!r.mapped, "Mapping a mapped buffer");
584
+ await settled(batch.device, r.value.mapAsync(GPUMapMode.READ, offset, size));
585
+ r.mapped = r.value.getMappedRange(offset, size);
586
+ } });
587
+
588
+ operation(A.DOLLY_GPU_UNMAP, { bytes: 16, decode: w => ({ buffer: integer(w, 8) }),
589
+ run(batch, { buffer }) {
590
+ const r = object(batch.scope, buffer, "buffer");
591
+ r.value.unmap();
592
+ r.mapped = null;
593
+ } });
594
+
595
+ operation(A.DOLLY_GPU_RELEASE, { bytes: 16, decode: w => ({ handle: integer(w, 8) }),
596
+ run(batch, { handle }) {
597
+ ensure(!batch.encoder, "Submit before releasing GPU resources");
598
+ release(batch.scope, handle, batch.progress);
599
+ } });
600
+
601
+ // Submits the batch's encoder: its passes' timestamps resolve into the scope's timer, and a frame
602
+ // drawn into the surface is presented once the GPU has finished it.
603
+ operation(A.DOLLY_GPU_SUBMIT, { bytes: 8,
604
+ async run(batch) {
605
+ ensure(batch.encoder, "No GPU commands to submit");
606
+ const { scope, device, progress } = batch;
607
+ await queueFull(scope);
608
+ const measured = batch.queries ? batch.timer : null, count = batch.queries;
609
+ if (measured) {
610
+ batch.encoder.resolveQuerySet(measured.query, 0, count, measured.resolve, 0);
611
+ batch.encoder.copyBufferToBuffer(measured.resolve, 0, measured.read, 0, count * 8);
612
+ measured.busy = true;
613
+ }
614
+ device.queue.submit([batch.encoder.finish()]);
615
+ batch.encoder = null;
616
+ const submittedWork = ++progress.queued;
617
+ if (measured) measured.read.mapAsync(GPUMapMode.READ, 0, count * 8).then(() => {
618
+ const values = new BigUint64Array(measured.read.getMappedRange(0, count * 8));
619
+ let ns = 0n;
620
+ for (let i = 0; i < count; i += 2) if (values[i + 1] >= values[i]) ns += values[i + 1] - values[i];
621
+ if (!scope.lost) { scope.gpuMs = Number(ns) / 1e6; scope.gpuTotalMs += scope.gpuMs; scope.gpuSamples++; }
622
+ measured.read.unmap();
623
+ }).catch(() => {}).finally(() => { measured.busy = false; });
624
+ batch.timer = null;
625
+ batch.queries = 0;
626
+ const completed = settled(device, device.queue.onSubmittedWorkDone()).then(() => {
627
+ progress.completed = Math.max(progress.completed, submittedWork);
628
+ }).finally(() => scope.submissions.splice(scope.submissions.indexOf(completed), 1));
629
+ scope.submissions.push(completed);
630
+ completed.catch(() => {});
631
+ const { texture } = batch;
632
+ if (!texture) return;
633
+ present(scope, completed);
634
+ scope.size = { width: texture.width, height: texture.height };
635
+ if (surfaces.at(-1) === scope) {
636
+ stats.frames++;
637
+ postMessage({ type: "status", active: true, ...scope.size, stats: { ...stats, allocatedBytes: usedBytes } });
638
+ }
639
+ batch.texture = null;
640
+ } });
641
+
642
+ operation(A.DOLLY_GPU_CAPTURE_FRAME, { bytes: 32,
643
+ decode(w) {
644
+ const [x, y, width, height] = [16, 20, 24, 28].map(at => u32(w, at));
645
+ ensure(width > 0 && height > 0, "Capture rectangle outside surface");
646
+ return { buffer: integer(w, 8), x, y, width, height };
647
+ },
648
+ run(batch, { buffer, x, y, width, height }) {
649
+ const { scope, texture } = batch;
650
+ ensure(scope.surface && texture, "Capture requires a rendered surface in this batch");
651
+ ensure(x <= texture.width - width && y <= texture.height - height, "Capture rectangle outside surface");
652
+ const destination = object(scope, buffer, "buffer"), bytesPerRow = Math.ceil(width * 4 / 256) * 256;
653
+ range(destination, 0, bytesPerRow * (height - 1) + width * 4);
654
+ ensure(!destination.mapped && (destination.value.usage & GPUBufferUsage.COPY_DST), "Capture destination is mapped or not writable");
655
+ commandEncoder(batch).copyTextureToBuffer({ texture, origin: { x, y } }, { buffer: destination.value, bytesPerRow }, { width, height });
656
+ } });
657
+
658
+ operation(A.DOLLY_GPU_CREATE_TEXTURE, { bytes: 48, creates: true,
659
+ decode(w) {
660
+ const [width, height, layers, mips, type, usage, dimension] = [16, 20, 24, 28, 32, 36, 40].map(at => u32(w, at));
661
+ const samples = w.getUint16(44, true) || 1, viewFormat = w.getUint16(46, true);
662
+ ensure(width > 0 && height > 0 && layers > 0 && mips > 0 && type > 0 && type < textureFormats.length && usage > 0 &&
663
+ (usage & ~31) === 0 && dimension <= 3 && viewFormat < textureFormats.length && (samples === 1 || samples === 4), "Texture descriptor");
664
+ // Dimension 0, the mesh records' 2D texture: one layer, or a cube's six, and no storage usage.
665
+ if (dimension === 0) ensure((layers === 1 || (layers === 6 && width === height)) && !(usage & 8), "Texture descriptor");
666
+ ensure(mips <= 1 + Math.floor(Math.log2(Math.max(width, height, dimension === 3 ? layers : 1))) &&
667
+ (samples === 1 || (dimension !== 1 && dimension !== 3 && mips === 1 && layers === 1)), "Texture mips/samples");
668
+ const block = compressedFormat(type) ? 4 : 1;
669
+ if (block === 4) ensure(width % block === 0 && height % block === 0 && !(usage & 16), "BC texture descriptor");
670
+ let size = 0;
671
+ for (let i = 0; i < mips; i++) {
672
+ const levelLayers = dimension === 3 ? Math.max(1, layers >> i) : layers;
673
+ size += Math.ceil(Math.max(1, width >> i) / block) * Math.ceil(Math.max(1, height >> i) / block) * levelLayers *
674
+ blockBytes[type] * samples;
675
+ }
676
+ return { id: integer(w, 8), width, height, layers, mips, type, usage, dimension, samples, viewFormat, size };
677
+ },
678
+ run({ scope, device }, record) {
679
+ const { width, height, layers, mips, type, usage, dimension, samples, viewFormat, size } = record;
680
+ const l = device.limits, largest = [0, l.maxTextureDimension1D, l.maxTextureDimension2D, l.maxTextureDimension3D][dimension];
681
+ if (dimension === 0) {
682
+ ensure(width <= Math.min(8192, l.maxTextureDimension2D) && height <= Math.min(8192, l.maxTextureDimension2D), "Texture descriptor");
683
+ } else {
684
+ ensure(width <= largest && height <= (dimension === 1 ? 1 : largest) &&
685
+ layers <= (dimension === 3 ? largest : dimension === 1 ? 1 : l.maxTextureArrayLayers), "Texture dimensions");
686
+ }
687
+ if (compressedFormat(type)) ensure(device.features.has("texture-compression-bc"), "BC texture compression unavailable", E.ENOSYS);
688
+ if (type === 45) ensure(device.features.has("depth32float-stencil8"), "depth32float-stencil8 unavailable", E.ENOSYS);
689
+ ensure(size <= bufferCeiling && size <= maxBytes - usedBytes, "Texture allocation quota", E.ENOMEM);
690
+ const value = device.createTexture({ size: { width, height, depthOrArrayLayers: layers }, mipLevelCount: mips,
691
+ dimension: ["2d", "1d", "2d", "3d"][dimension], sampleCount: samples, format: textureFormats[type], usage,
692
+ viewFormats: viewFormat ? [textureFormats[viewFormat]] : [] });
693
+ insert(scope, record.id, "texture", value, size);
694
+ usedBytes += size;
695
+ Object.assign(object(scope, record.id), { width, height, layers, mips, type, dimension, samples });
696
+ } });
697
+
698
+ operation(A.DOLLY_GPU_WRITE_TEXTURE, { least: 48,
699
+ decode(w, b) {
700
+ const [mip, layer, x, y, width, height, length] = [16, 20, 24, 28, 32, 36, 40].map(at => u32(w, at));
701
+ ensure(u32(w, 44) === 0 && length > 0 && Math.ceil((48 + length) / 8) * 8 === b.length, "Texture upload span");
702
+ ensure(width > 0 && height > 0, "Texture upload range");
703
+ return { texture: integer(w, 8), mip, layer, x, y, width, height, data: b.subarray(48, 48 + length) };
704
+ },
705
+ run({ scope, device, progress }, { texture, mip, layer, x, y, width, height, data }) {
706
+ const r = object(scope, texture, "texture"), block = compressedFormat(r.type) ? 4 : 1;
707
+ ensure(!depthFormat(r.type) && !stencilFormat(r.type) && r.samples === 1 && mip < r.mips && layer < r.layers &&
708
+ x % block === 0 && y % block === 0 && width % block === 0 && height % block === 0 &&
709
+ x <= Math.ceil(Math.max(1, r.width >> mip) / block) * block - width &&
710
+ y <= Math.ceil(Math.max(1, r.height >> mip) / block) * block - height &&
711
+ data.length === (width / block) * (height / block) * blockBytes[r.type], "Texture upload range");
712
+ device.queue.writeTexture({ texture: r.value, mipLevel: mip, origin: { x, y, z: layer } }, data,
713
+ { bytesPerRow: width / block * blockBytes[r.type], rowsPerImage: height / block }, { width, height });
714
+ progress.queued++;
715
+ } });
716
+
717
+ operation(A.DOLLY_GPU_CREATE_SAMPLER, { least: 48, creates: true,
718
+ decode(w, b) {
719
+ ensure(b.length === 48 || b.length === 56, "Sampler layout");
720
+ const filters = [16, 20, 24].map(at => u32(w, at)), address = [28, 32, 36].map(at => u32(w, at));
721
+ const compare = u32(w, 40), maxAnisotropy = u32(w, 44);
722
+ const [lodMinClamp, lodMaxClamp] = b.length === 56 ? [f32(w, 48), f32(w, 52)] : [0, 32];
723
+ ensure(filters.every(n => n <= 1) && address.every(n => n <= 2) && compare <= 8 && maxAnisotropy >= 1 &&
724
+ maxAnisotropy <= 16 && (maxAnisotropy === 1 || filters.every(n => n === 1)) &&
725
+ Number.isFinite(lodMinClamp) && Number.isFinite(lodMaxClamp) && lodMinClamp >= 0 && lodMinClamp <= lodMaxClamp,
726
+ "Sampler descriptor");
727
+ const addresses = ["clamp-to-edge", "repeat", "mirror-repeat"], modes = ["nearest", "linear"];
728
+ return { id: integer(w, 8), descriptor: { minFilter: modes[filters[0]], magFilter: modes[filters[1]],
729
+ mipmapFilter: modes[filters[2]], addressModeU: addresses[address[0]], addressModeV: addresses[address[1]],
730
+ addressModeW: addresses[address[2]], compare: compareOps[compare], maxAnisotropy, lodMinClamp, lodMaxClamp } };
731
+ },
732
+ run({ scope, device }, { id, descriptor }) {
733
+ insert(scope, id, "sampler", device.createSampler(descriptor));
734
+ } });
735
+
736
+ // A blend component: operation, source and destination factor.
737
+ function blendComponent(w, at) {
738
+ const [blendOp, source, destination] = [0, 4, 8].map(o => u32(w, at + o));
739
+ ensure(blendOp < blendOps.length && source < blendFactors.length && destination < blendFactors.length, "Blend state");
740
+ return { operation: blendOps[blendOp], srcFactor: blendFactors[source], dstFactor: blendFactors[destination] };
741
+ }
742
+
743
+ // The mesh records, GRAPHICS_PIPELINE to DRAW_MESH: "main" entry points, a pipeline's own bind group
744
+ // layouts, and a DRAW_MESH that sets only what differs from the pass's last one.
745
+ operation(A.DOLLY_GPU_GRAPHICS_PIPELINE, { least: 112, creates: true,
746
+ decode(w, b) {
747
+ const [color, depth, topology, cull, front, compare, write] = [32, 36, 40, 44, 48, 52, 56].map(at => u32(w, at));
748
+ const slope = f32(w, 64), clamp = f32(w, 68), mask = u32(w, 72), blend = u32(w, 76);
749
+ ensure(color <= 4 && (depth === 0 || depth === 5 || depth === 6) && topology <= 3 && cull <= 2 && front <= 1 &&
750
+ compare >= 1 && compare <= 8 && write <= 1 && Number.isFinite(slope) && Number.isFinite(clamp) && mask <= 15 && blend <= 1,
751
+ "Graphics pipeline state");
752
+ const colorBlend = blendComponent(w, 80), alphaBlend = blendComponent(w, 92);
753
+ const bufferCount = u32(w, 104), attributeCount = u32(w, 108), buffers = [], locations = new Set();
754
+ ensure(bufferCount <= 8 && attributeCount <= 16 && b.length === 112 + 16 * (bufferCount + attributeCount), "Graphics vertex layout");
755
+ for (let i = 0; i < bufferCount; i++) {
756
+ const at = 112 + i * 16, stride = u32(w, at), instanced = u32(w, at + 4);
757
+ ensure(stride > 0 && stride <= 2048 && stride % 4 === 0 && instanced <= 1 && zero64(w, at + 8), "Graphics vertex stride");
758
+ buffers.push({ arrayStride: stride, stepMode: instanced ? "instance" : "vertex", attributes: [] });
759
+ }
760
+ for (let i = 0; i < attributeCount; i++) {
761
+ const at = 112 + (bufferCount + i) * 16, [slot, location, type, offset] = [0, 4, 8, 12].map(o => u32(w, at + o));
762
+ ensure(slot < bufferCount && location < 16 && !locations.has(location) && type > 0 && type < vertexFormats.length &&
763
+ offset % Math.min(4, vertexBytes[type]) === 0 && offset <= buffers[slot].arrayStride - vertexBytes[type],
764
+ "Graphics vertex attribute");
765
+ locations.add(location);
766
+ buffers[slot].attributes.push({ shaderLocation: location, format: vertexFormats[type], offset });
767
+ }
768
+ return { id: integer(w, 8), vertexShader: integer(w, 16), fragmentShader: integer(w, 24), color, mask, buffers,
769
+ blend: blend ? { color: colorBlend, alpha: alphaBlend } : null,
770
+ primitive: { topology: ["triangle-list", "triangle-strip", "line-list", "point-list"][topology],
771
+ ...(topology === 1 ? { stripIndexFormat: "uint16" } : {}), cullMode: ["none", "front", "back"][cull],
772
+ frontFace: front ? "cw" : "ccw" },
773
+ depthStencil: depth ? { format: textureFormats[depth], depthWriteEnabled: !!write, depthCompare: compareOps[compare],
774
+ depthBias: w.getInt32(60, true), depthBiasSlopeScale: slope, depthBiasClamp: clamp } : null };
775
+ },
776
+ async run({ scope, device }, record) {
777
+ const vertex = object(scope, record.vertexShader, "shader").value;
778
+ const fragment = object(scope, record.fragmentShader, "shader").value;
779
+ const target = { format: record.color ? textureFormats[record.color] : format, writeMask: record.mask,
780
+ ...(record.blend ? { blend: record.blend } : {}) };
781
+ const pipeline = await device.createRenderPipelineAsync({ layout: "auto",
782
+ vertex: { module: vertex, entryPoint: "main", buffers: record.buffers },
783
+ fragment: { module: fragment, entryPoint: "main", targets: [target] },
784
+ primitive: record.primitive, ...(record.depthStencil ? { depthStencil: record.depthStencil } : {}) });
785
+ insert(scope, record.id, "graphics", pipeline);
786
+ } });
787
+
788
+ operation(A.DOLLY_GPU_RESOURCE_GROUP, { least: 32, creates: true,
789
+ decode(w, b) {
790
+ const index = u32(w, 24), count = u32(w, 28), bindings = new Set(), entries = [];
791
+ ensure(index < 4 && count <= 16 && b.length === 32 + 32 * count, "Resource group layout");
792
+ for (let i = 0; i < count; i++) {
793
+ // Kind 0 is a buffer range, 1 a texture, 2 a cube texture, 3 a sampler.
794
+ const at = 32 + i * 32, binding = u32(w, at), kind = u32(w, at + 4);
795
+ ensure(binding < 32 && !bindings.has(binding) && kind <= 3 &&
796
+ (kind === 0 || (zero64(w, at + 16) && zero64(w, at + 24))), "Resource binding");
797
+ bindings.add(binding);
798
+ entries.push({ binding, kind, resource: integer(w, at + 8), offset: integer(w, at + 16), size: integer(w, at + 24) });
799
+ }
800
+ return { id: integer(w, 8), pipeline: integer(w, 16), index, entries };
801
+ },
802
+ run({ scope, device }, record) {
803
+ const pipeline = object(scope, record.pipeline, "graphics");
804
+ const entries = record.entries.map(({ binding, kind, resource, offset, size }) => {
805
+ const r = object(scope, resource, ["buffer", "texture", "texture", "sampler"][kind]);
806
+ if (kind === 0) {
807
+ range(r, offset, size);
808
+ ensure(size > 0 && !r.mapped, "Invalid bound buffer");
809
+ return { binding, resource: { buffer: r.value, offset, size } };
810
+ }
811
+ if (kind === 3) return { binding, resource: r.value };
812
+ ensure(kind !== 2 || r.layers === 6, "Cube binding requires six layers");
813
+ ensure(r.dimension === 0, "Resource groups bind two-dimensional textures");
814
+ return { binding, resource: r.value.createView({ dimension: kind === 2 ? "cube" : "2d", arrayLayerCount: kind === 2 ? 6 : 1,
815
+ aspect: r.type === 5 ? "depth-only" : "all" }) };
816
+ });
817
+ const layout = pipeline.value.getBindGroupLayout(record.index);
818
+ insert(scope, record.id, "resource-group", device.createBindGroup({ layout, entries }));
819
+ object(scope, record.id).index = record.index;
820
+ } });
821
+
822
+ operation(A.DOLLY_GPU_BEGIN_RENDER_PASS, { bytes: 64,
823
+ decode(w) {
824
+ const width = u32(w, 24), height = u32(w, 28), colorClear = u32(w, 32), depthClear = u32(w, 36);
825
+ const clearValue = [40, 44, 48, 52].map(at => f32(w, at)), depthClearValue = f32(w, 56), stencilClearValue = u32(w, 60);
826
+ ensure(width > 0 && height > 0 && colorClear <= 1 && depthClear <= 1 && clearValue.every(Number.isFinite) &&
827
+ depthClearValue >= 0 && depthClearValue <= 1 && stencilClearValue <= 255, "Render pass descriptor");
828
+ return { color: integer(w, 8), depth: integer(w, 16), width, height, colorClear, depthClear, clearValue,
829
+ depthClearValue, stencilClearValue };
830
+ },
831
+ run(batch, record) {
832
+ const { scope } = batch, { width, height } = record;
833
+ let colorTexture;
834
+ if (record.color) {
835
+ const r = object(scope, record.color, "texture");
836
+ ensure(r.width === width && r.height === height && r.type <= 4 && r.layers === 1 && r.dimension === 0 && r.samples === 1,
837
+ "Color attachment dimensions/format");
838
+ colorTexture = r.value;
839
+ } else {
840
+ ensure(scope.surface && width <= 4096 && height <= 2304, "Surface attachment dimensions/ownership");
841
+ colorTexture = surfaceFrame(batch, width, height);
842
+ }
843
+ let depthStencilAttachment;
844
+ if (record.depth) {
845
+ const r = object(scope, record.depth, "texture"), loadOp = record.depthClear ? "clear" : "load";
846
+ ensure(r.width === width && r.height === height && (r.type === 5 || r.type === 6) && r.layers === 1 && r.dimension === 0 &&
847
+ r.samples === 1, "Depth attachment dimensions/format");
848
+ depthStencilAttachment = { view: r.value.createView({ mipLevelCount: 1 }), depthLoadOp: loadOp, depthStoreOp: "store",
849
+ depthClearValue: record.depthClearValue,
850
+ ...(r.type === 5 ? { stencilLoadOp: loadOp, stencilStoreOp: "store", stencilClearValue: record.stencilClearValue } : {}) };
851
+ }
852
+ const colorAttachment = { view: colorTexture.createView({ mipLevelCount: 1 }), loadOp: record.colorClear ? "clear" : "load",
853
+ storeOp: "store", clearValue: record.clearValue };
854
+ batch.renderPass = commandEncoder(batch).beginRenderPass({ ...timestamps(batch), colorAttachments: [colorAttachment],
855
+ depthStencilAttachment });
856
+ Object.assign(batch, { passWidth: width, passHeight: height, renderPipeline: null, renderGroups: [], renderBuffers: [],
857
+ renderIndex: null });
858
+ } });
859
+
860
+ operation(A.DOLLY_GPU_END_RENDER_PASS, { bytes: 8,
861
+ run(batch) {
862
+ ensure(batch.renderPass, "No active render pass");
863
+ endRender(batch);
864
+ } });
865
+
866
+ operation(A.DOLLY_GPU_VIEWPORT, { bytes: 48,
867
+ decode(w) {
868
+ const viewport = [8, 12, 16, 20, 24, 28].map(at => f32(w, at)), [x, y, width, height, min, max] = viewport;
869
+ ensure(viewport.every(Number.isFinite) && x >= 0 && y >= 0 && width > 0 && height > 0 && min >= 0 && max <= 1 && min <= max,
870
+ "Viewport/scissor bounds");
871
+ return { viewport, scissor: [32, 36, 40, 44].map(at => u32(w, at)) };
872
+ },
873
+ run({ renderPass, passWidth, passHeight }, { viewport, scissor }) {
874
+ ensure(renderPass, "Viewport outside render pass");
875
+ const [x, y, width, height] = viewport;
876
+ ensure(x <= passWidth - width && y <= passHeight - height && scissor[0] <= passWidth - scissor[2] &&
877
+ scissor[1] <= passHeight - scissor[3], "Viewport/scissor bounds");
878
+ renderPass.setViewport(...viewport);
879
+ renderPass.setScissorRect(...scissor);
880
+ } });
881
+
882
+ operation(A.DOLLY_GPU_DRAW_MESH, { least: 80,
883
+ decode(w, b) {
884
+ const groupCount = u32(w, 64), bufferCount = u32(w, 68);
885
+ ensure(groupCount <= 4 && bufferCount <= 8 && b.length === 80 + 8 * groupCount + 24 * bufferCount && zero64(w, 72), "Mesh layout");
886
+ const [count, instances, first] = [40, 44, 48].map(at => u32(w, at)), base = w.getInt32(52, true);
887
+ const firstInstance = u32(w, 56), indexType = u32(w, 60);
888
+ ensure(count > 0 && instances > 0 && count * instances <= 4 * 1024 * 1024 && first + count <= 0xffffffff &&
889
+ firstInstance + instances <= 0xffffffff && indexType <= 2, "Mesh draw limit", E.E2BIG);
890
+ const groups = Array.from({ length: groupCount }, (_, i) => integer(w, 80 + i * 8));
891
+ const buffers = Array.from({ length: bufferCount }, (_, i) => bufferRange(w, 80 + 8 * groupCount + 24 * i));
892
+ let index = null;
893
+ if (indexType) {
894
+ index = { ...bufferRange(w, 16), format: indexType === 1 ? "uint16" : "uint32" };
895
+ ensure((first + count) * (indexType === 1 ? 2 : 4) <= index.size, "Index buffer range");
896
+ } else {
897
+ ensure(zero64(w, 16) && zero64(w, 24) && zero64(w, 32) && base === 0, "Unexpected nonindexed draw arguments");
898
+ }
899
+ return { pipeline: integer(w, 8), count, instances, first, base, firstInstance, groups, buffers, index };
900
+ },
901
+ run(batch, record) {
902
+ const { scope, renderPass } = batch;
903
+ ensure(renderPass, "Draw outside render pass");
904
+ const pipeline = object(scope, record.pipeline, "graphics").value;
905
+ if (batch.renderPipeline !== pipeline) {
906
+ renderPass.setPipeline(pipeline);
907
+ batch.renderPipeline = pipeline;
908
+ }
909
+ const groups = new Set();
910
+ for (const handle of record.groups) {
911
+ const r = object(scope, handle, "resource-group");
912
+ ensure(!groups.has(r.index), "Duplicate mesh group");
913
+ groups.add(r.index);
914
+ if (batch.renderGroups[r.index] !== r.value) {
915
+ renderPass.setBindGroup(r.index, r.value);
916
+ batch.renderGroups[r.index] = r.value;
917
+ }
918
+ }
919
+ record.buffers.forEach(({ buffer, offset, size }, slot) => {
920
+ const r = object(scope, buffer, "buffer"), bound = batch.renderBuffers[slot];
921
+ range(r, offset, size);
922
+ ensure(!r.mapped && size > 0, "Invalid vertex buffer");
923
+ if (bound?.[0] === r.value && bound[1] === offset && bound[2] === size) return;
924
+ renderPass.setVertexBuffer(slot, r.value, offset, size);
925
+ batch.renderBuffers[slot] = [r.value, offset, size];
926
+ });
927
+ if (!record.index) {
928
+ renderPass.draw(record.count, record.instances, record.first, record.firstInstance);
929
+ return;
930
+ }
931
+ const { buffer, offset, size, format: indexFormat } = record.index, r = object(scope, buffer, "buffer"), bound = batch.renderIndex;
932
+ range(r, offset, size);
933
+ ensure(!r.mapped, "Index buffer range");
934
+ if (bound?.[0] !== r.value || bound[1] !== indexFormat || bound[2] !== offset || bound[3] !== size) {
935
+ renderPass.setIndexBuffer(r.value, indexFormat, offset, size);
936
+ batch.renderIndex = [r.value, indexFormat, offset, size];
937
+ }
938
+ renderPass.drawIndexed(record.count, record.instances, record.first, record.base, record.firstInstance);
939
+ } });
940
+
941
+ // The explicit records (FEATURE_EXPLICIT) keep WebGPU's object model: views, layouts, whole
942
+ // pipeline descriptors, copies and pass state.
943
+ operation(A.DOLLY_GPU_CREATE_VIEW, { bytes: 56, creates: true,
944
+ decode(w) {
945
+ const [viewFormat, dimension, aspect, baseMip, mips, baseLayer, layers, swizzle] =
946
+ [24, 28, 32, 36, 40, 44, 48, 52].map(at => u32(w, at));
947
+ const channels = [0, 1, 2, 3].map(i => (swizzle >>> (8 * i)) & 255);
948
+ ensure(viewFormat < textureFormats.length && dimension <= 6 && aspect <= 2 && channels.every(n => n <= 5),
949
+ `View descriptor: format ${viewFormat}, dimension ${dimension}, aspect ${aspect}, swizzle ${swizzle.toString(16)}`);
950
+ const descriptor = { aspect: aspects[aspect], baseMipLevel: baseMip, baseArrayLayer: baseLayer,
951
+ ...(viewFormat ? { format: textureFormats[viewFormat] } : {}), ...(dimension ? { dimension: viewDimensions[dimension] } : {}),
952
+ ...(mips ? { mipLevelCount: mips } : {}), ...(layers ? { arrayLayerCount: layers } : {}) };
953
+ return { id: integer(w, 8), texture: integer(w, 16), viewFormat, baseMip, mips, descriptor,
954
+ swizzle: swizzle === 0x03020100 ? null : channels.map(n => "rgba01"[n]).join("") };
955
+ },
956
+ run({ scope, device }, record) {
957
+ const r = object(scope, record.texture, "texture"), { viewFormat, baseMip, mips, descriptor } = record;
958
+ ensure(baseMip < r.mips && mips <= r.mips - baseMip, `View descriptor: mips ${baseMip}+${mips} of ${r.mips}`);
959
+ if (srgbFormat(viewFormat) && r.value.usage & GPUTextureUsage.STORAGE_BINDING) {
960
+ descriptor.usage = r.value.usage & ~GPUTextureUsage.STORAGE_BINDING;
961
+ }
962
+ if (record.swizzle) {
963
+ ensure(device.features.has("texture-component-swizzle"), "Texture swizzles are unavailable", E.ENOTSUP);
964
+ descriptor.swizzle = record.swizzle;
965
+ }
966
+ insert(scope, record.id, "view", r.value.createView(descriptor));
967
+ Object.assign(object(scope, record.id), { texture: r, width: Math.max(1, r.width >> baseMip),
968
+ height: Math.max(1, r.height >> baseMip), type: viewFormat || r.type });
969
+ } });
970
+
971
+ // A bind group layout entry's type, 1 to 8, and its three fields (dolly-gpu-0.wat):
972
+ // a buffer (1-3) has a dynamic offset or not; a sampler (4-6) none; a texture (7) has its sample
973
+ // type, view dimension and multisampling; a storage texture (8) its access, view dimension and format.
974
+ function bindGroupLayoutEntry(w, at) {
975
+ const [binding, visibility, type, first, second, third] = [0, 4, 8, 12, 16, 20].map(o => u32(w, at + o));
976
+ const minBindingSize = integer(w, at + 24), entry = { binding, visibility };
977
+ ensure(visibility > 0 && visibility <= 7 && type >= 1 && type <= 8, "Bind group layout entry");
978
+ if (type <= 3) {
979
+ ensure(first <= 1 && second === 0 && third === 0, "Buffer layout entry");
980
+ entry.buffer = { type: ["uniform", "storage", "read-only-storage"][type - 1], hasDynamicOffset: first === 1, minBindingSize };
981
+ } else if (type <= 6) {
982
+ ensure(first === 0 && second === 0 && third === 0 && minBindingSize === 0, "Sampler layout entry");
983
+ entry.sampler = { type: ["filtering", "non-filtering", "comparison"][type - 4] };
984
+ } else if (type === 7) {
985
+ ensure(first >= 1 && first <= 5 && second >= 1 && second <= 6 && third <= 1 && minBindingSize === 0, "Texture layout entry");
986
+ entry.texture = { sampleType: ["float", "unfilterable-float", "depth", "sint", "uint"][first - 1],
987
+ viewDimension: viewDimensions[second], multisampled: third === 1 };
988
+ } else {
989
+ ensure(first >= 1 && first <= 3 && second >= 1 && second <= 6 && third > 0 && third < textureFormats.length &&
990
+ minBindingSize === 0, "Storage texture layout entry");
991
+ entry.storageTexture = { access: ["write-only", "read-only", "read-write"][first - 1], format: textureFormats[third],
992
+ viewDimension: viewDimensions[second] };
993
+ }
994
+ return entry;
995
+ }
996
+ operation(A.DOLLY_GPU_CREATE_BIND_GROUP_LAYOUT, { least: 24, creates: true,
997
+ decode(w, b) {
998
+ const count = u32(w, 16);
999
+ ensure(count <= A.DOLLY_GPU_MAX_LAYOUT_ENTRIES && b.length === 24 + 32 * count && u32(w, 20) === 0, "Bind group layout layout");
1000
+ return { id: integer(w, 8), entries: Array.from({ length: count }, (_, i) => bindGroupLayoutEntry(w, 24 + i * 32)) };
1001
+ },
1002
+ run({ scope, device }, { id, entries }) {
1003
+ insert(scope, id, "group-layout", device.createBindGroupLayout({ entries }));
1004
+ } });
1005
+
1006
+ operation(A.DOLLY_GPU_CREATE_PIPELINE_LAYOUT, { least: 24, creates: true,
1007
+ decode(w, b) {
1008
+ const count = u32(w, 16);
1009
+ ensure(count <= 8 && b.length === 24 + 8 * count && u32(w, 20) === 0, "Pipeline layout layout");
1010
+ return { id: integer(w, 8), layouts: Array.from({ length: count }, (_, i) => integer(w, 24 + i * 8)) };
1011
+ },
1012
+ run({ scope, device }, { id, layouts }) {
1013
+ const bindGroupLayouts = layouts.map(layout => object(scope, layout, "group-layout").value);
1014
+ insert(scope, id, "pipeline-layout", device.createPipelineLayout({ bindGroupLayouts }));
1015
+ } });
1016
+
1017
+ operation(A.DOLLY_GPU_CREATE_BIND_GROUP, { least: 32, creates: true,
1018
+ decode(w, b) {
1019
+ const count = u32(w, 24), entries = [];
1020
+ ensure(count <= A.DOLLY_GPU_MAX_LAYOUT_ENTRIES && b.length === 32 + 32 * count && u32(w, 28) === 0, "Bind group entries");
1021
+ for (let i = 0; i < count; i++) {
1022
+ // Kind 0 is a buffer range, 1 a sampler, 2 a texture view.
1023
+ const at = 32 + i * 32, binding = u32(w, at), kind = u32(w, at + 4);
1024
+ ensure(kind <= 2, "Bind group entry kind");
1025
+ if (kind !== 0) ensure(zero64(w, at + 16) && zero64(w, at + 24), "Bind group entry range");
1026
+ entries.push({ binding, kind, resource: integer(w, at + 8), offset: integer(w, at + 16), size: integer(w, at + 24) });
1027
+ }
1028
+ return { id: integer(w, 8), layout: integer(w, 16), entries };
1029
+ },
1030
+ run({ scope, device }, record) {
1031
+ const layout = object(scope, record.layout, "group-layout").value;
1032
+ const entries = record.entries.map(({ binding, kind, resource, offset, size }) => {
1033
+ const r = object(scope, resource, ["buffer", "sampler", "view"][kind]);
1034
+ if (kind !== 0) return { binding, resource: r.value };
1035
+ range(r, offset, size);
1036
+ ensure(size > 0 && !r.mapped, "Invalid bound buffer");
1037
+ return { binding, resource: { buffer: r.value, offset, size } };
1038
+ });
1039
+ insert(scope, record.id, "bind-group", device.createBindGroup({ layout, entries }));
1040
+ } });
1041
+
1042
+ // A stencil face: compare, then the operations on fail, depth fail and pass.
1043
+ function stencilFace(w, at) {
1044
+ const [compare, failOp, depthFailOp, passOp] = [0, 4, 8, 12].map(o => u32(w, at + o));
1045
+ ensure(compare >= 1 && compare <= 8 && [failOp, depthFailOp, passOp].every(n => n < stencilOps.length), "Stencil state");
1046
+ return { compare: compareOps[compare], failOp: stencilOps[failOp], depthFailOp: stencilOps[depthFailOp], passOp: stencilOps[passOp] };
1047
+ }
1048
+ // A render target; format 0 is the surface's, filled in by run.
1049
+ function renderTarget(w, at) {
1050
+ const [targetFormat, writeMask, blend, absent] = [0, 4, 8, 36].map(o => u32(w, at + o));
1051
+ ensure(targetFormat < textureFormats.length && writeMask <= 15 && blend <= 1 && absent <= 1, "Render target");
1052
+ if (absent) {
1053
+ ensure(!targetFormat && !writeMask && !blend, "Absent render target");
1054
+ return null;
1055
+ }
1056
+ return { format: textureFormats[targetFormat], writeMask,
1057
+ ...(blend ? { blend: { color: blendComponent(w, at + 12), alpha: blendComponent(w, at + 24) } } : {}) };
1058
+ }
1059
+ operation(A.DOLLY_GPU_CREATE_RENDER_PIPELINE, { least: 160, creates: true,
1060
+ decode(w, b) {
1061
+ const field = at => u32(w, at);
1062
+ const [targetCount, bufferCount, attributeCount, constantCount, vs, fs] = [136, 140, 144, 148, 152, 156].map(field);
1063
+ ensure(targetCount <= 8 && bufferCount <= 16 && attributeCount <= 32 && constantCount <= 64 && vs <= 64 && fs <= 64 &&
1064
+ b.length === Math.ceil((160 + 40 * targetCount + 16 * (bufferCount + attributeCount + constantCount) + vs + fs) / 8) * 8,
1065
+ "Render pipeline layout");
1066
+ const [topology, stripFormat, front, cull, unclipped, depth, depthWrite, depthCompare] = [40, 44, 48, 52, 56, 60, 64, 68].map(field);
1067
+ const [samples, mask, coverage] = [124, 128, 132].map(field), slope = f32(w, 116), clamp = f32(w, 120);
1068
+ ensure(topology <= 4 && stripFormat <= 2 && (stripFormat === 0 || topology === 1 || topology === 3) && front <= 1 && cull <= 2 &&
1069
+ unclipped <= 1 && [0, 5, 6, 43, 44, 45].includes(depth) && depthWrite <= 1 && depthCompare <= 8 &&
1070
+ (depth === 0) === (depthCompare === 0) && Number.isFinite(slope) && Number.isFinite(clamp) &&
1071
+ (samples === 1 || samples === 4) && coverage <= 1, "Render pipeline state");
1072
+ let at = 160;
1073
+ const targets = [], buffers = [], vertexConstants = {}, fragmentConstants = {};
1074
+ for (let i = 0; i < targetCount; i++, at += 40) targets.push(renderTarget(w, at));
1075
+ for (let i = 0; i < bufferCount; i++, at += 16) {
1076
+ ensure(field(at + 4) <= 1 && zero64(w, at + 8), "Vertex buffer layout");
1077
+ buffers.push({ arrayStride: field(at), stepMode: field(at + 4) ? "instance" : "vertex", attributes: [] });
1078
+ }
1079
+ for (let i = 0; i < attributeCount; i++, at += 16) {
1080
+ const [slot, shaderLocation, type, offset] = [0, 4, 8, 12].map(o => field(at + o));
1081
+ ensure(slot < bufferCount && type > 0 && type < vertexFormats.length, "Vertex attribute");
1082
+ buffers[slot].attributes.push({ shaderLocation, format: vertexFormats[type], offset });
1083
+ }
1084
+ for (let i = 0; i < constantCount; i++, at += 16) {
1085
+ const constantId = field(at), stages = field(at + 4), value = w.getFloat64(at + 8, true);
1086
+ ensure(stages > 0 && stages <= 3 && Number.isFinite(value), "Override constant");
1087
+ if (stages & 1) vertexConstants[constantId] = value;
1088
+ if (stages & 2) fragmentConstants[constantId] = value;
1089
+ }
1090
+ const fragmentShader = integer(w, 32);
1091
+ if (!fragmentShader) ensure(fs === 0 && targetCount === 0, "Render targets without a fragment stage");
1092
+ return { id: integer(w, 8), layout: integer(w, 16), vertexShader: integer(w, 24), fragmentShader,
1093
+ vertexEntry: text(b, at, vs), fragmentEntry: fragmentShader ? text(b, at + vs, fs) : null,
1094
+ buffers, vertexConstants, fragmentConstants, targets,
1095
+ primitive: { topology: ["triangle-list", "triangle-strip", "line-list", "line-strip", "point-list"][topology],
1096
+ ...(stripFormat ? { stripIndexFormat: ["", "uint16", "uint32"][stripFormat] } : {}), frontFace: front ? "cw" : "ccw",
1097
+ cullMode: ["none", "front", "back"][cull], ...(unclipped ? { unclippedDepth: true } : {}) },
1098
+ multisample: { count: samples, mask, alphaToCoverageEnabled: coverage === 1 },
1099
+ depthStencil: depth ? { format: textureFormats[depth], depthWriteEnabled: depthWrite === 1,
1100
+ depthCompare: compareOps[depthCompare], stencilFront: stencilFace(w, 72), stencilBack: stencilFace(w, 88),
1101
+ stencilReadMask: field(104), stencilWriteMask: field(108), depthBias: w.getInt32(112, true),
1102
+ depthBiasSlopeScale: slope, depthBiasClamp: clamp } : null };
1103
+ },
1104
+ run(batch, record) {
1105
+ const { scope } = batch;
1106
+ const descriptor = { layout: object(scope, record.layout, "pipeline-layout").value,
1107
+ vertex: { module: object(scope, record.vertexShader, "shader").value, entryPoint: record.vertexEntry,
1108
+ buffers: record.buffers, constants: record.vertexConstants },
1109
+ primitive: record.primitive, multisample: record.multisample };
1110
+ if (record.fragmentShader) {
1111
+ descriptor.fragment = { module: object(scope, record.fragmentShader, "shader").value, entryPoint: record.fragmentEntry,
1112
+ targets: record.targets.map(target => target && { ...target, format: target.format ?? format }),
1113
+ constants: record.fragmentConstants };
1114
+ }
1115
+ if (record.depthStencil) descriptor.depthStencil = record.depthStencil;
1116
+ compile(batch, record, "view-render", batch.device.createRenderPipelineAsync(descriptor));
1117
+ } });
1118
+
1119
+ operation(A.DOLLY_GPU_CREATE_COMPUTE_PIPELINE, { least: 40, creates: true,
1120
+ decode(w, b) {
1121
+ const constantCount = u32(w, 32), entryBytes = u32(w, 36), constants = {};
1122
+ ensure(constantCount <= 64 && entryBytes <= 64 && b.length === Math.ceil((40 + 16 * constantCount + entryBytes) / 8) * 8,
1123
+ "Compute pipeline layout");
1124
+ for (let i = 0; i < constantCount; i++) {
1125
+ const at = 40 + i * 16, value = w.getFloat64(at + 8, true);
1126
+ ensure(u32(w, at + 4) === 0 && Number.isFinite(value), "Override constant");
1127
+ constants[u32(w, at)] = value;
1128
+ }
1129
+ return { id: integer(w, 8), layout: integer(w, 16), shader: integer(w, 24),
1130
+ entry: text(b, 40 + 16 * constantCount, entryBytes), constants };
1131
+ },
1132
+ run(batch, record) {
1133
+ const { scope } = batch, layout = object(scope, record.layout, "pipeline-layout").value;
1134
+ const module = object(scope, record.shader, "shader").value;
1135
+ compile(batch, record, "view-compute", batch.device.createComputePipelineAsync({ layout,
1136
+ compute: { module, entryPoint: record.entry, constants: record.constants } }));
1137
+ } });
1138
+
1139
+ operation(A.DOLLY_GPU_CREATE_QUERY_SET, { bytes: 24, creates: true,
1140
+ decode(w) {
1141
+ const count = u32(w, 16);
1142
+ ensure(count > 0 && count <= 4096 && u32(w, 20) === 0, "Query set");
1143
+ return { id: integer(w, 8), count };
1144
+ },
1145
+ run({ scope, device }, { id, count }) {
1146
+ ensure(device.features.has("timestamp-query"), "Timestamp queries are unavailable", E.ENOTSUP);
1147
+ ensure(count * 8 <= maxBytes - usedBytes, "Query set allocation quota", E.ENOMEM);
1148
+ insert(scope, id, "query-set", device.createQuerySet({ type: "timestamp", count }), count * 8);
1149
+ usedBytes += count * 8;
1150
+ } });
1151
+
1152
+ function decodeBufferTextureCopy(w) {
1153
+ const [bytesPerRow, rowsPerImage] = [24, 28].map(at => u32(w, at));
1154
+ const [mipLevel, aspect, x, y, z, width, height, depthOrArrayLayers] = [40, 44, 48, 52, 56, 60, 64, 68].map(at => u32(w, at));
1155
+ ensure(aspect <= 2 && bytesPerRow % 256 === 0, "Buffer-texture copy");
1156
+ return { buffer: integer(w, 8), offset: integer(w, 16), bytesPerRow, rowsPerImage, texture: integer(w, 32),
1157
+ mipLevel, origin: { x, y, z }, aspect: aspects[aspect], extent: { width, height, depthOrArrayLayers } };
1158
+ }
1159
+ // The buffer and texture sides of a copy between them.
1160
+ function bufferTextureCopy({ scope }, record) {
1161
+ const r = object(scope, record.buffer, "buffer"), t = object(scope, record.texture, "texture");
1162
+ ensure(!r.mapped, "Buffer-texture copy");
1163
+ return [{ buffer: r.value, offset: record.offset, bytesPerRow: record.bytesPerRow, rowsPerImage: record.rowsPerImage },
1164
+ { texture: t.value, mipLevel: record.mipLevel, origin: record.origin, aspect: record.aspect }];
1165
+ }
1166
+ operation(A.DOLLY_GPU_COPY_BUFFER_TO_TEXTURE, { bytes: 72, decode: decodeBufferTextureCopy,
1167
+ run(batch, record) {
1168
+ const [buffer, image] = bufferTextureCopy(batch, record);
1169
+ commandEncoder(batch).copyBufferToTexture(buffer, image, record.extent);
1170
+ } });
1171
+ operation(A.DOLLY_GPU_COPY_TEXTURE_TO_BUFFER, { bytes: 72, decode: decodeBufferTextureCopy,
1172
+ run(batch, record) {
1173
+ const [buffer, image] = bufferTextureCopy(batch, record);
1174
+ commandEncoder(batch).copyTextureToBuffer(image, buffer, record.extent);
1175
+ } });
1176
+
1177
+ operation(A.DOLLY_GPU_COPY_TEXTURE_TO_TEXTURE, { bytes: 72,
1178
+ decode(w) {
1179
+ const field = at => u32(w, at);
1180
+ ensure(field(68) === 0, "Texture copy reserved field");
1181
+ return { source: integer(w, 8), sourceMip: field(24), sourceOrigin: { x: field(28), y: field(32), z: field(36) },
1182
+ destination: integer(w, 16), destinationMip: field(40), destinationOrigin: { x: field(44), y: field(48), z: field(52) },
1183
+ extent: { width: field(56), height: field(60), depthOrArrayLayers: field(64) } };
1184
+ },
1185
+ run(batch, record) {
1186
+ const source = object(batch.scope, record.source, "texture"), destination = object(batch.scope, record.destination, "texture");
1187
+ commandEncoder(batch).copyTextureToTexture({ texture: source.value, mipLevel: record.sourceMip, origin: record.sourceOrigin },
1188
+ { texture: destination.value, mipLevel: record.destinationMip, origin: record.destinationOrigin }, record.extent);
1189
+ } });
1190
+
1191
+ operation(A.DOLLY_GPU_CLEAR_BUFFER, { bytes: 32,
1192
+ decode: w => ({ buffer: integer(w, 8), offset: integer(w, 16), size: integer(w, 24) }),
1193
+ run(batch, { buffer, offset, size }) {
1194
+ const r = object(batch.scope, buffer, "buffer");
1195
+ range(r, offset, size);
1196
+ ensure(!r.mapped, "Clearing a mapped buffer");
1197
+ commandEncoder(batch).clearBuffer(r.value, offset, size);
1198
+ } });
1199
+
1200
+ operation(A.DOLLY_GPU_WRITE_TIMESTAMP, { bytes: 24,
1201
+ decode(w) {
1202
+ ensure(u32(w, 20) === 0, "Timestamp reserved field");
1203
+ return { querySet: integer(w, 8), query: u32(w, 16) };
1204
+ },
1205
+ run(batch, record) {
1206
+ const querySet = object(batch.scope, record.querySet, "query-set").value;
1207
+ commandEncoder(batch).beginComputePass({ timestampWrites: { querySet, beginningOfPassWriteIndex: record.query } }).end();
1208
+ } });
1209
+
1210
+ operation(A.DOLLY_GPU_RESOLVE_QUERY_SET, { bytes: 40,
1211
+ decode: w => ({ querySet: integer(w, 8), first: u32(w, 16), count: u32(w, 20), buffer: integer(w, 24), offset: integer(w, 32) }),
1212
+ run(batch, record) {
1213
+ const querySet = object(batch.scope, record.querySet, "query-set"), r = object(batch.scope, record.buffer, "buffer");
1214
+ ensure(!r.mapped, "Resolving into a mapped buffer");
1215
+ commandEncoder(batch).resolveQuerySet(querySet.value, record.first, record.count, r.value, record.offset);
1216
+ } });
1217
+
1218
+ // A colour attachment, or null for an absent one; view 0 is the scope's surface.
1219
+ function colourAttachment(w, at, absent) {
1220
+ const view = integer(w, at), resolve = integer(w, at + 8), load = u32(w, at + 16), store = u32(w, at + 20);
1221
+ const clearValue = [24, 28, 32, 36].map(o => f32(w, at + o));
1222
+ if (absent) {
1223
+ ensure(!view && !resolve && !load && !store && clearValue.every(v => v === 0), "Absent colour attachment");
1224
+ return null;
1225
+ }
1226
+ ensure(load <= 1 && store <= 1 && clearValue.every(Number.isFinite), "Colour attachment");
1227
+ return { view, resolve, loadOp: load ? "clear" : "load", storeOp: store ? "discard" : "store", clearValue };
1228
+ }
1229
+ // The depth and stencil aspects of a depth attachment, each read-only or loaded and stored.
1230
+ function depthAttachment(w, view) {
1231
+ const [depthLoad, depthStore, depthReadOnly] = [28, 32, 36].map(at => u32(w, at)), depthClearValue = f32(w, 40);
1232
+ const [stencilLoad, stencilStore, stencilReadOnly, stencilClearValue] = [44, 48, 52, 56].map(at => u32(w, at));
1233
+ ensure(depthLoad <= 1 && depthStore <= 1 && depthReadOnly <= 1 && depthClearValue >= 0 && depthClearValue <= 1 &&
1234
+ stencilLoad <= 1 && stencilStore <= 1 && stencilReadOnly <= 1, "Depth attachment");
1235
+ return { view,
1236
+ depth: depthReadOnly ? { depthReadOnly: true }
1237
+ : { depthLoadOp: depthLoad ? "clear" : "load", depthStoreOp: depthStore ? "discard" : "store", depthClearValue },
1238
+ stencil: stencilReadOnly ? { stencilReadOnly: true }
1239
+ : { stencilLoadOp: stencilLoad ? "clear" : "load", stencilStoreOp: stencilStore ? "discard" : "store", stencilClearValue } };
1240
+ }
1241
+ operation(A.DOLLY_GPU_BEGIN_VIEW_PASS, { least: 64,
1242
+ decode(w, b) {
1243
+ const count = u32(w, 16), absent = u32(w, 60), depthView = integer(w, 8);
1244
+ ensure(count <= 8 && b.length === 64 + 40 * count && absent >>> count === 0, "View pass layout");
1245
+ return { width: u32(w, 20), height: u32(w, 24),
1246
+ colours: Array.from({ length: count }, (_, i) => colourAttachment(w, 64 + i * 40, absent >>> i & 1)),
1247
+ depth: depthView ? depthAttachment(w, depthView) : null };
1248
+ },
1249
+ run(batch, record) {
1250
+ const { scope } = batch, { width, height } = record;
1251
+ // The pass's size: its first attachment's.
1252
+ let size;
1253
+ const colorAttachments = record.colours.map(colour => {
1254
+ if (!colour) return null;
1255
+ let view;
1256
+ if (colour.view) {
1257
+ const r = object(scope, colour.view, "view");
1258
+ view = r.value;
1259
+ size ??= [r.width, r.height];
1260
+ } else {
1261
+ ensure(scope.surface && width > 0 && height > 0 && width <= 4096 && height <= 2304, "Surface attachment dimensions/ownership");
1262
+ view = surfaceFrame(batch, width, height).createView();
1263
+ size ??= [width, height];
1264
+ }
1265
+ const resolveTarget = colour.resolve ? { resolveTarget: object(scope, colour.resolve, "view").value } : {};
1266
+ return { view, ...resolveTarget, loadOp: colour.loadOp, storeOp: colour.storeOp, clearValue: colour.clearValue };
1267
+ });
1268
+ let depthStencilAttachment;
1269
+ if (record.depth) {
1270
+ const r = object(scope, record.depth.view, "view");
1271
+ depthStencilAttachment = { view: r.value, ...(depthFormat(r.type) ? record.depth.depth : {}),
1272
+ ...(stencilFormat(r.type) ? record.depth.stencil : {}) };
1273
+ size ??= [r.width, r.height];
1274
+ }
1275
+ ensure(size, "A render pass needs an attachment");
1276
+ batch.renderPass = commandEncoder(batch).beginRenderPass({ ...timestamps(batch), colorAttachments, depthStencilAttachment });
1277
+ batch.viewPass = true;
1278
+ [batch.passWidth, batch.passHeight] = size;
1279
+ } });
1280
+
1281
+ operation(A.DOLLY_GPU_BEGIN_COMPUTE_PASS, { bytes: 8,
1282
+ run(batch) {
1283
+ batch.computePass = commandEncoder(batch).beginComputePass(timestamps(batch));
1284
+ batch.explicitCompute = true;
1285
+ } });
1286
+
1287
+ operation(A.DOLLY_GPU_END_COMPUTE_PASS, { bytes: 8,
1288
+ run(batch) {
1289
+ ensure(batch.explicitCompute, "No active compute pass");
1290
+ endCompute(batch);
1291
+ } });
1292
+
1293
+ operation(A.DOLLY_GPU_SET_PIPELINE, { bytes: 16, decode: w => ({ pipeline: integer(w, 8) }),
1294
+ async run(batch, { pipeline }) {
1295
+ await compiled(batch);
1296
+ if (batch.viewPass) {
1297
+ batch.renderPass.setPipeline(object(batch.scope, pipeline, "view-render").value);
1298
+ return;
1299
+ }
1300
+ ensure(batch.explicitCompute, "Pipeline outside a pass");
1301
+ batch.computePass.setPipeline(object(batch.scope, pipeline, "view-compute").value);
1302
+ } });
1303
+
1304
+ operation(A.DOLLY_GPU_SET_BIND_GROUP, { least: 24,
1305
+ decode(w, b) {
1306
+ const index = u32(w, 16), count = u32(w, 20);
1307
+ ensure(count <= 16 && b.length === Math.ceil((24 + 4 * count) / 8) * 8, "Bind group offsets");
1308
+ ensure(index < 8, "Bind group index");
1309
+ return { group: integer(w, 8), index, offsets: Array.from({ length: count }, (_, i) => u32(w, 24 + i * 4)) };
1310
+ },
1311
+ run(batch, { group, index, offsets }) {
1312
+ ensure(batch.viewPass || batch.explicitCompute, "Bind group outside a pass");
1313
+ const value = object(batch.scope, group, "bind-group").value;
1314
+ (batch.viewPass ? batch.renderPass : batch.computePass).setBindGroup(index, value, offsets);
1315
+ } });
1316
+
1317
+ // SET_VERTEX_BUFFER's and SET_INDEX_BUFFER's buffer, in a view pass.
1318
+ function boundBuffer(batch, record) {
1319
+ ensure(batch.viewPass, "Buffer binding outside a render pass");
1320
+ const r = object(batch.scope, record.buffer, "buffer");
1321
+ range(r, record.offset, record.size);
1322
+ ensure(!r.mapped, "Invalid vertex or index buffer");
1323
+ return r.value;
1324
+ }
1325
+ operation(A.DOLLY_GPU_SET_VERTEX_BUFFER, { bytes: 40,
1326
+ decode(w) {
1327
+ const slot = u32(w, 16);
1328
+ ensure(u32(w, 20) === 0, "Invalid vertex or index buffer");
1329
+ ensure(slot < 16, "Vertex buffer slot");
1330
+ return { slot, buffer: integer(w, 8), offset: integer(w, 24), size: integer(w, 32) };
1331
+ },
1332
+ run(batch, record) {
1333
+ const buffer = boundBuffer(batch, record);
1334
+ batch.renderPass.setVertexBuffer(record.slot, buffer, record.offset, record.size);
1335
+ } });
1336
+ operation(A.DOLLY_GPU_SET_INDEX_BUFFER, { bytes: 40,
1337
+ decode(w) {
1338
+ const indexType = u32(w, 16);
1339
+ ensure(u32(w, 20) === 0, "Invalid vertex or index buffer");
1340
+ ensure(indexType === 1 || indexType === 2, "Index format");
1341
+ return { indexFormat: indexType === 1 ? "uint16" : "uint32", buffer: integer(w, 8), offset: integer(w, 24), size: integer(w, 32) };
1342
+ },
1343
+ run(batch, record) {
1344
+ const buffer = boundBuffer(batch, record);
1345
+ batch.renderPass.setIndexBuffer(buffer, record.indexFormat, record.offset, record.size);
1346
+ } });
1347
+
1348
+ operation(A.DOLLY_GPU_DRAW, { bytes: 24, decode: w => ({ args: [8, 12, 16, 20].map(at => u32(w, at)) }),
1349
+ run(batch, { args }) {
1350
+ ensure(batch.viewPass, "Draw outside a render pass");
1351
+ batch.renderPass.draw(...args);
1352
+ } });
1353
+ operation(A.DOLLY_GPU_DRAW_INDEXED, { bytes: 32,
1354
+ decode(w) {
1355
+ ensure(u32(w, 28) === 0, "Draw reserved field");
1356
+ return { args: [u32(w, 8), u32(w, 12), u32(w, 16), w.getInt32(20, true), u32(w, 24)] };
1357
+ },
1358
+ run(batch, { args }) {
1359
+ ensure(batch.viewPass, "Draw outside a render pass");
1360
+ batch.renderPass.drawIndexed(...args);
1361
+ } });
1362
+
1363
+ // An indirect draw's or dispatch's arguments: a buffer and an offset in it.
1364
+ const decodeIndirect = w => ({ buffer: integer(w, 8), offset: integer(w, 16) });
1365
+ function indirectBuffer(batch, record) {
1366
+ const r = object(batch.scope, record.buffer, "buffer");
1367
+ ensure(!r.mapped, "Indirect arguments in a mapped buffer");
1368
+ return r.value;
1369
+ }
1370
+ operation(A.DOLLY_GPU_DRAW_INDIRECT, { bytes: 24, decode: decodeIndirect,
1371
+ run(batch, record) {
1372
+ const buffer = indirectBuffer(batch, record);
1373
+ ensure(batch.viewPass, "Draw outside a render pass");
1374
+ batch.renderPass.drawIndirect(buffer, record.offset);
1375
+ } });
1376
+ operation(A.DOLLY_GPU_DRAW_INDEXED_INDIRECT, { bytes: 24, decode: decodeIndirect,
1377
+ run(batch, record) {
1378
+ const buffer = indirectBuffer(batch, record);
1379
+ ensure(batch.viewPass, "Draw outside a render pass");
1380
+ batch.renderPass.drawIndexedIndirect(buffer, record.offset);
1381
+ } });
1382
+ operation(A.DOLLY_GPU_DISPATCH_INDIRECT, { bytes: 24, decode: decodeIndirect,
1383
+ run(batch, record) {
1384
+ const buffer = indirectBuffer(batch, record);
1385
+ ensure(batch.explicitCompute, "Dispatch outside a compute pass");
1386
+ batch.computePass.dispatchWorkgroupsIndirect(buffer, record.offset);
1387
+ stats.dispatches++;
1388
+ } });
1389
+
1390
+ operation(A.DOLLY_GPU_SET_BLEND_CONSTANT, { bytes: 24,
1391
+ decode(w) {
1392
+ const color = [8, 12, 16, 20].map(at => f32(w, at));
1393
+ ensure(color.every(Number.isFinite), "Blend constant");
1394
+ return { color };
1395
+ },
1396
+ run(batch, { color }) {
1397
+ ensure(batch.viewPass, "Blend constant");
1398
+ batch.renderPass.setBlendConstant(color);
1399
+ } });
1400
+
1401
+ operation(A.DOLLY_GPU_SET_STENCIL_REFERENCE, { bytes: 16,
1402
+ decode(w) {
1403
+ ensure(u32(w, 12) === 0, "Stencil reference");
1404
+ return { reference: u32(w, 8) };
1405
+ },
1406
+ run(batch, { reference }) {
1407
+ ensure(batch.viewPass, "Stencil reference");
1408
+ batch.renderPass.setStencilReference(reference);
1409
+ } });
1410
+
1411
+ operation(A.DOLLY_GPU_DISPATCH, { bytes: 24,
1412
+ decode(w) {
1413
+ ensure(u32(w, 20) === 0, "Dispatch reserved field");
1414
+ return { workgroups: [8, 12, 16].map(at => u32(w, at)) };
1415
+ },
1416
+ run(batch, { workgroups }) {
1417
+ ensure(batch.explicitCompute, "Dispatch outside a compute pass");
1418
+ batch.computePass.dispatchWorkgroups(...workgroups);
1419
+ stats.dispatches++;
1420
+ } });
1421
+
1422
+ async function runBatch(scope, records, flags) {
1423
+ const device = scope.device, progress = queueProgress;
290
1424
  await queueFull(scope);
291
- let encoder, texture, computePass, renderPass, passWidth, passHeight, timer, queries = 0;
292
- let renderPipeline, renderGroups, renderBuffers, renderIndex;
293
- const getEncoder = () => encoder ??= device.createCommandEncoder();
294
- const endCompute = () => { computePass?.end(); computePass = null; };
295
- const endRender = () => { renderPass?.end(); renderPass = null; };
296
- const timestamps = () => {
297
- timer ??= scope.timers?.find(t => !t.busy);
298
- if (!timer) return {};
299
- const beginningOfPassWriteIndex = queries++, endOfPassWriteIndex = queries++;
300
- return {timestampWrites: {querySet: timer.query, beginningOfPassWriteIndex, endOfPassWriteIndex}};
301
- };
1425
+ const batch = scope.open ?? { explicitCompute: false, viewPass: false, queries: 0 };
1426
+ scope.open = null;
1427
+ Object.assign(batch, { scope, device, progress, compiling: [] });
1428
+ let keepOpen = false;
302
1429
  const started = performance.now();
303
1430
  device.pushErrorScope("validation");
304
1431
  device.pushErrorScope("out-of-memory");
305
1432
  try {
306
- for (const { opcode: op, b, w } of commands) {
307
- const id = b.length >= 16 && op !== A.DOLLY_GPU_VIEWPORT ? integer(w, 8) : 0;
308
- if (op !== A.DOLLY_GPU_COMPUTE) endCompute();
309
- if (renderPass) ensure(renderPassRecords.has(op), "Command is invalid inside a render pass");
310
- if (creatingRecords.has(op)) ensure(id > scope.high && id <= 0xffffffff && scope.objects.size + scope.retired.size < maxObjects, "GPU object quota", E.ENOSPC);
311
- if (op === A.DOLLY_GPU_CREATE_BUFFER) {
312
- const size = integer(w, 16), usage = w.getUint32(24, true);
313
- ensure(size > 0 && size <= maxBuffer && size <= maxBytes - usedBytes, "GPU allocation quota", E.ENOMEM);
314
- ensure(w.getUint32(28, true) === 0 && usage > 0 && (usage & ~1023) === 0, "Buffer usage");
315
- const buffer = device.createBuffer({ size, usage });
316
- insert(scope, id, "buffer", buffer, size); usedBytes += size;
317
- } else if (op === A.DOLLY_GPU_WRITE_BUFFER) {
318
- const r = object(scope, id, "buffer"), offset = integer(w, 16), n = w.getUint32(28, true);
319
- range(r, offset, n); ensure(offset % 4 === 0 && !r.mapped, "Unaligned or mapped GPU upload");
320
- device.queue.writeBuffer(r.value, offset, b.subarray(w.getUint32(24, true), w.getUint32(24, true) + n));
321
- progress.queued++;
322
- } else if (op === A.DOLLY_GPU_CREATE_SHADER) {
323
- const shader = device.createShaderModule({ code: text(b, 24, w.getUint32(16, true)) });
324
- const info = await shader.getCompilationInfo();
325
- const errors = info.messages.filter(m => m.type === "error");
326
- ensure(!errors.length, errors.map(m => `${m.lineNum}:${m.linePos} ${m.message}`).join("\n").slice(0,2048));
327
- insert(scope, id, "shader", shader);
328
- } else if (op === A.DOLLY_GPU_RENDER_PIPELINE || op === A.DOLLY_GPU_VERTEX_PIPELINE) {
329
- const shader = object(scope, integer(w, 16), "shader").value;
330
- const topology = w.getUint32(24, true), blend = w.getUint32(28, true);
331
- ensure(topology <= 1 && blend <= 2, "Unsupported render state");
332
- const component = { srcFactor: "one", dstFactor: blend === 2 ? "one" : "one-minus-src-alpha", operation: "add" };
333
- const vs = w.getUint32(32, true), fs = w.getUint32(36, true);
334
- let at = 40, buffers = [];
335
- if (op === A.DOLLY_GPU_VERTEX_PIPELINE) {
336
- const count = w.getUint32(44, true), attributes = [];
337
- for (let i = 0; i < count; i++) {
338
- const o = 48 + i * 16;
339
- attributes.push({shaderLocation:w.getUint32(o,true),format:`float32x${w.getUint32(o+4,true)}`,offset:w.getUint32(o+8,true)});
340
- }
341
- buffers = [{arrayStride:w.getUint32(40,true),stepMode:"vertex",attributes}];
342
- at = 48 + count * 16;
343
- }
344
- const pipeline = await device.createRenderPipelineAsync({ layout: "auto",
345
- vertex: { module: shader, entryPoint: text(b,at,vs), buffers },
346
- fragment: { module: shader, entryPoint: text(b,at+vs,fs), targets: [{ format, ...(blend ? { blend: { color: component, alpha: component } } : {}) }] },
347
- primitive: { topology: topology ? "triangle-strip" : "triangle-list" } });
348
- insert(scope, id, "render", pipeline);
349
- } else if (op === A.DOLLY_GPU_COMPUTE_PIPELINE || op === A.DOLLY_GPU_COMPUTE_CONSTANTS) {
350
- const constants=Object.create(null);
351
- if(op===A.DOLLY_GPU_COMPUTE_CONSTANTS) {
352
- let at=(32+w.getUint32(24,true)+7)&~7;
353
- for(let i=0;i<w.getUint32(28,true);i++) {
354
- const n=w.getUint32(at,true);constants[text(b,at+16,n)]=w.getFloat64(at+8,true);at=(at+16+n+7)&~7;
355
- }
356
- }
357
- const pipeline = await device.createComputePipelineAsync({ layout: "auto", compute: {
358
- module: object(scope,integer(w,16),"shader").value, entryPoint: text(b,32,w.getUint32(24,true)), constants } });
359
- insert(scope,id,"compute",pipeline);
360
- } else if (op === A.DOLLY_GPU_BIND_GROUP) {
361
- const pipeline = object(scope,integer(w,16));
362
- ensure(["render","compute"].includes(pipeline.kind), "Invalid binding pipeline");
363
- const entries = [];
364
- for (let i=0; i<w.getUint32(24,true); ++i) {
365
- const at=32+i*24, r=object(scope,integer(w,at),"buffer"), offset=integer(w,at+8), size=integer(w,at+16);
366
- range(r,offset,size); entries.push({binding:i,resource:{buffer:r.value,offset,size}});
367
- }
368
- insert(scope,id,"group",device.createBindGroup({layout:pipeline.value.getBindGroupLayout(0),entries}));
369
- } else if (op === A.DOLLY_GPU_RENDER || op === A.DOLLY_GPU_RENDER_VERTEX) {
370
- const width=w.getUint32(32,true), height=w.getUint32(36,true);
371
- ensure(scope.surface && width>0 && height>0 && width<=4096 && height<=2304, "Invalid GPU surface dimensions");
372
- ensure(w.getUint32(60,true)===0 && w.getUint32(56,true)<=1, "Render reserved field");
373
- const vertices=w.getUint32(24,true), instances=w.getUint32(28,true);
374
- ensure(vertices>0 && instances>0 && vertices*instances<=4*1024*1024, "Draw limit",E.E2BIG);
375
- if (!texture) {
376
- if(canvas.width!==width || canvas.height!==height) { canvas.width=width; canvas.height=height; }
377
- texture=context.getCurrentTexture();
378
- } else ensure(canvas.width===width && canvas.height===height,"Surface resized within batch");
379
- const clearValue=[40,44,48,52].map(o=>w.getFloat32(o,true));
380
- ensure(clearValue.every(Number.isFinite), "Invalid clear color");
381
- const pass=getEncoder().beginRenderPass({...timestamps(),colorAttachments:[{view:texture.createView(),clearValue,
382
- loadOp:w.getUint32(56,true)?"clear":"load",storeOp:"store"}]});
383
- pass.setPipeline(object(scope,id,"render").value);
384
- const group=integer(w,16); if(group)pass.setBindGroup(0,object(scope,group,"group").value);
385
- if(op===A.DOLLY_GPU_RENDER_VERTEX) {
386
- const r=object(scope,integer(w,64),"buffer"), offset=integer(w,72), size=integer(w,80);
387
- range(r,offset,size);pass.setVertexBuffer(0,r.value,offset,size);
388
- }
389
- pass.draw(vertices,instances); pass.end();
390
- } else if (op === A.DOLLY_GPU_CREATE_TEXTURE) {
391
- const width=w.getUint32(16,true), height=w.getUint32(20,true), layers=w.getUint32(24,true),
392
- mips=w.getUint32(28,true), type=w.getUint32(32,true), usage=w.getUint32(36,true);
393
- ensure(width>0 && height>0 && width<=Math.min(8192,device.limits.maxTextureDimension2D) &&
394
- height<=Math.min(8192,device.limits.maxTextureDimension2D) && (layers===1 || (layers===6 && width===height)) &&
395
- mips>0 && mips<=1+Math.floor(Math.log2(Math.max(width,height))) &&
396
- type>0 && type<textureFormats.length && usage>0 && (usage&~23)===0 && w.getBigUint64(40,true)===0n, "Texture descriptor");
397
- let size=0;
398
- const block=type>=7?4:1;
399
- if(block===4) {
400
- ensure(device.features.has("texture-compression-bc"), "BC texture compression unavailable", E.ENOSYS);
401
- ensure(width%block===0 && height%block===0 && !(usage&16), "BC texture descriptor");
402
- }
403
- for(let i=0;i<mips;i++)size+=Math.ceil(Math.max(1,width>>i)/block)*Math.ceil(Math.max(1,height>>i)/block)*layers*blockBytes[type];
404
- ensure(size<=bufferCeiling && size<=maxBytes-usedBytes, "Texture allocation quota", E.ENOMEM);
405
- const value=device.createTexture({size:{width,height,depthOrArrayLayers:layers},mipLevelCount:mips,
406
- format:textureFormats[type],usage});
407
- insert(scope,id,"texture",value,size);usedBytes+=size;
408
- Object.assign(object(scope,id),{width,height,layers,mips,type});
409
- } else if (op === A.DOLLY_GPU_WRITE_TEXTURE) {
410
- const r=object(scope,id,"texture"), mip=w.getUint32(16,true), layer=w.getUint32(20,true),
411
- x=w.getUint32(24,true), y=w.getUint32(28,true), width=w.getUint32(32,true), height=w.getUint32(36,true), n=w.getUint32(40,true);
412
- const block=r.type>=7?4:1;
413
- ensure(r.type!==5 && r.type!==6 && mip<r.mips && layer<r.layers && width>0 && height>0 &&
414
- x%block===0 && y%block===0 && width%block===0 && height%block===0 &&
415
- x<=Math.ceil(Math.max(1,r.width>>mip)/block)*block-width &&
416
- y<=Math.ceil(Math.max(1,r.height>>mip)/block)*block-height &&
417
- n===(width/block)*(height/block)*blockBytes[r.type], "Texture upload range");
418
- device.queue.writeTexture({texture:r.value,mipLevel:mip,origin:{x,y,z:layer}},b.subarray(48,48+n),
419
- {bytesPerRow:width/block*blockBytes[r.type],rowsPerImage:height/block},{width,height});
420
- progress.queued++;
421
- } else if (op === A.DOLLY_GPU_CREATE_SAMPLER) {
422
- const filters=[16,20,24].map(at=>w.getUint32(at,true)), address=[28,32,36].map(at=>w.getUint32(at,true)),
423
- compare=w.getUint32(40,true), maxAnisotropy=w.getUint32(44,true);
424
- ensure(filters.every(n=>n<=1) && address.every(n=>n<=2) && compare<=8 && maxAnisotropy>=1 &&
425
- maxAnisotropy<=16 && (maxAnisotropy===1 || filters.every(n=>n===1)), "Sampler descriptor");
426
- const addresses=["clamp-to-edge","repeat","mirror-repeat"], modes=["nearest","linear"];
427
- insert(scope,id,"sampler",device.createSampler({minFilter:modes[filters[0]],magFilter:modes[filters[1]],
428
- mipmapFilter:modes[filters[2]],addressModeU:addresses[address[0]],addressModeV:addresses[address[1]],
429
- addressModeW:addresses[address[2]],compare:compareOps[compare],maxAnisotropy}));
430
- } else if (op === A.DOLLY_GPU_GRAPHICS_PIPELINE) {
431
- const color=w.getUint32(32,true), depth=w.getUint32(36,true), topology=w.getUint32(40,true),
432
- cull=w.getUint32(44,true), front=w.getUint32(48,true), compare=w.getUint32(52,true), write=w.getUint32(56,true),
433
- slope=w.getFloat32(64,true), clamp=w.getFloat32(68,true), mask=w.getUint32(72,true), blend=w.getUint32(76,true);
434
- ensure(color<=4 && (depth===0 || depth===5 || depth===6) && topology<=3 && cull<=2 && front<=1 &&
435
- compare>=1 && compare<=8 && write<=1 && Number.isFinite(slope) && Number.isFinite(clamp) && mask<=15 && blend<=1,
436
- "Graphics pipeline state");
437
- const component=at=>{
438
- const op=w.getUint32(at,true), src=w.getUint32(at+4,true), dst=w.getUint32(at+8,true);
439
- ensure(op<blendOps.length && src<blendFactors.length && dst<blendFactors.length,"Blend state");
440
- return {operation:blendOps[op],srcFactor:blendFactors[src],dstFactor:blendFactors[dst]};
441
- };
442
- const colorBlend=component(80), alphaBlend=component(92), count=w.getUint32(104,true), buffers=[];
443
- for(let i=0;i<count;i++)buffers.push({arrayStride:w.getUint32(112+i*16,true),
444
- stepMode:w.getUint32(116+i*16,true)?"instance":"vertex",attributes:[]});
445
- for(let i=0;i<w.getUint32(108,true);i++) {
446
- const at=112+(count+i)*16;
447
- buffers[w.getUint32(at,true)].attributes.push({shaderLocation:w.getUint32(at+4,true),
448
- format:vertexFormats[w.getUint32(at+8,true)],offset:w.getUint32(at+12,true)});
449
- }
450
- const value=await device.createRenderPipelineAsync({layout:"auto",
451
- vertex:{module:object(scope,integer(w,16),"shader").value,entryPoint:"main",buffers},
452
- fragment:{module:object(scope,integer(w,24),"shader").value,entryPoint:"main",
453
- targets:[{format:color?textureFormats[color]:format,writeMask:mask,...(blend?{blend:{color:colorBlend,alpha:alphaBlend}}:{})}]},
454
- primitive:{topology:["triangle-list","triangle-strip","line-list","point-list"][topology],
455
- ...(topology===1?{stripIndexFormat:"uint16"}:{}),cullMode:["none","front","back"][cull],frontFace:front?"cw":"ccw"},
456
- ...(depth?{depthStencil:{format:textureFormats[depth],depthWriteEnabled:!!write,depthCompare:compareOps[compare],
457
- depthBias:w.getInt32(60,true),depthBiasSlopeScale:slope,depthBiasClamp:clamp}}:{})});
458
- insert(scope,id,"graphics",value);
459
- } else if (op === A.DOLLY_GPU_RESOURCE_GROUP) {
460
- const pipeline=object(scope,integer(w,16),"graphics"), index=w.getUint32(24,true), entries=[];
461
- for(let i=0;i<w.getUint32(28,true);i++) {
462
- const at=32+i*32, binding=w.getUint32(at,true), kind=w.getUint32(at+4,true),
463
- r=object(scope,integer(w,at+8),["buffer","texture","texture","sampler"][kind]);
464
- let resource;
465
- if(kind===0) {
466
- const offset=integer(w,at+16), size=integer(w,at+24);range(r,offset,size);
467
- ensure(size>0 && !r.mapped,"Invalid bound buffer");resource={buffer:r.value,offset,size};
468
- } else if(kind===3)resource=r.value;
469
- else {
470
- ensure(kind!==2 || r.layers===6,"Cube binding requires six layers");
471
- resource=r.value.createView({dimension:kind===2?"cube":"2d",arrayLayerCount:kind===2?6:1,
472
- aspect:r.type===5?"depth-only":"all"});
473
- }
474
- entries.push({binding,resource});
475
- }
476
- insert(scope,id,"resource-group",device.createBindGroup({layout:pipeline.value.getBindGroupLayout(index),entries}));
477
- object(scope,id).index=index;
478
- } else if (op === A.DOLLY_GPU_BEGIN_RENDER_PASS) {
479
- const depth=integer(w,16), width=w.getUint32(24,true), height=w.getUint32(28,true),
480
- colorClear=w.getUint32(32,true), depthClear=w.getUint32(36,true), clearValue=[40,44,48,52].map(at=>w.getFloat32(at,true)),
481
- depthClearValue=w.getFloat32(56,true), stencilClearValue=w.getUint32(60,true);
482
- ensure(width>0 && height>0 && colorClear<=1 && depthClear<=1 && clearValue.every(Number.isFinite) &&
483
- depthClearValue>=0 && depthClearValue<=1 && stencilClearValue<=255,"Render pass descriptor");
484
- let colorTexture;
485
- if(id) {
486
- const r=object(scope,id,"texture");
487
- ensure(r.width===width && r.height===height && r.type<=4 && r.layers===1,"Color attachment dimensions/format");
488
- colorTexture=r.value;
489
- } else {
490
- ensure(scope.surface && width<=4096 && height<=2304,"Surface attachment dimensions/ownership");
491
- if(!texture) {
492
- if(canvas.width!==width || canvas.height!==height){canvas.width=width;canvas.height=height;}
493
- texture=context.getCurrentTexture();
494
- } else ensure(canvas.width===width && canvas.height===height,"Surface resized within batch");
495
- colorTexture=texture;
496
- }
497
- let depthStencilAttachment;
498
- if(depth) {
499
- const r=object(scope,depth,"texture");
500
- ensure(r.width===width && r.height===height && r.type>=5 && r.layers===1,"Depth attachment dimensions/format");
501
- depthStencilAttachment={view:r.value.createView({mipLevelCount:1}),depthLoadOp:depthClear?"clear":"load",depthStoreOp:"store",depthClearValue,
502
- ...(r.type===5?{stencilLoadOp:depthClear?"clear":"load",stencilStoreOp:"store",stencilClearValue}:{})};
503
- }
504
- renderPass=getEncoder().beginRenderPass({...timestamps(),colorAttachments:[{view:colorTexture.createView({mipLevelCount:1}),
505
- loadOp:colorClear?"clear":"load",storeOp:"store",clearValue}],depthStencilAttachment});
506
- passWidth=width;passHeight=height;
507
- renderPipeline=null;renderGroups=[];renderBuffers=[];renderIndex=null;
508
- } else if (op === A.DOLLY_GPU_END_RENDER_PASS) {
509
- ensure(renderPass,"No active render pass");endRender();
510
- } else if (op === A.DOLLY_GPU_VIEWPORT) {
511
- ensure(renderPass,"Viewport outside render pass");
512
- const [x,y,width,height,min,max]=[8,12,16,20,24,28].map(at=>w.getFloat32(at,true)),
513
- scissor=[32,36,40,44].map(at=>w.getUint32(at,true));
514
- ensure([x,y,width,height,min,max].every(Number.isFinite) && x>=0 && y>=0 && width>0 && height>0 &&
515
- x<=passWidth-width && y<=passHeight-height && min>=0 && max<=1 && min<=max &&
516
- scissor[0]<=passWidth-scissor[2] && scissor[1]<=passHeight-scissor[3],"Viewport/scissor bounds");
517
- renderPass.setViewport(x,y,width,height,min,max);renderPass.setScissorRect(...scissor);
518
- } else if (op === A.DOLLY_GPU_DRAW_MESH) {
519
- ensure(renderPass,"Draw outside render pass");
520
- const count=w.getUint32(40,true), instances=w.getUint32(44,true), first=w.getUint32(48,true),
521
- base=w.getInt32(52,true), firstInstance=w.getUint32(56,true), indexType=w.getUint32(60,true),
522
- groupCount=w.getUint32(64,true), bufferCount=w.getUint32(68,true);
523
- ensure(count>0 && instances>0 && count*instances<=4*1024*1024 && first+count<=0xffffffff &&
524
- firstInstance+instances<=0xffffffff && indexType<=2,"Mesh draw limit",E.E2BIG);
525
- const pipeline=object(scope,id,"graphics").value;
526
- if(renderPipeline!==pipeline){renderPass.setPipeline(pipeline);renderPipeline=pipeline;}
527
- const groups=new Set();
528
- for(let i=0;i<groupCount;i++) {
529
- const r=object(scope,integer(w,80+i*8),"resource-group");
530
- ensure(!groups.has(r.index),"Duplicate mesh group");groups.add(r.index);
531
- if(renderGroups[r.index]!==r.value){renderPass.setBindGroup(r.index,r.value);renderGroups[r.index]=r.value;}
532
- }
533
- for(let i=0;i<bufferCount;i++) {
534
- const at=80+8*groupCount+24*i, r=object(scope,integer(w,at),"buffer"), offset=integer(w,at+8), size=integer(w,at+16);
535
- range(r,offset,size);ensure(!r.mapped && size>0,"Invalid vertex buffer");
536
- const slot=i*3;
537
- if(renderBuffers[slot]!==r.value || renderBuffers[slot+1]!==offset || renderBuffers[slot+2]!==size){
538
- renderPass.setVertexBuffer(i,r.value,offset,size);
539
- renderBuffers[slot]=r.value;renderBuffers[slot+1]=offset;renderBuffers[slot+2]=size;
540
- }
541
- }
542
- if(indexType) {
543
- const r=object(scope,integer(w,16),"buffer"), offset=integer(w,24), size=integer(w,32), stride=indexType===1?2:4;
544
- range(r,offset,size);ensure(!r.mapped && (first+count)*stride<=size,"Index buffer range");
545
- if(!renderIndex || renderIndex[0]!==r.value || renderIndex[1]!==indexType || renderIndex[2]!==offset || renderIndex[3]!==size){
546
- renderPass.setIndexBuffer(r.value,indexType===1?"uint16":"uint32",offset,size);
547
- renderIndex=[r.value,indexType,offset,size];
548
- }
549
- renderPass.drawIndexed(count,instances,first,base,firstInstance);
550
- } else {
551
- ensure(w.getBigUint64(16,true)===0n && w.getBigUint64(24,true)===0n && w.getBigUint64(32,true)===0n && base===0,
552
- "Unexpected nonindexed draw arguments");
553
- renderPass.draw(count,instances,first,firstInstance);
554
- }
555
- } else if (op === A.DOLLY_GPU_COMPUTE) {
556
- const xyz=[24,28,32].map(o=>w.getUint32(o,true));
557
- ensure(w.getUint32(36,true)===0 && xyz.every(n=>n>0 && n<=65535) && xyz.reduce((a,n)=>a*n,1)<=1048576, "Dispatch limit",E.E2BIG);
558
- computePass ??= getEncoder().beginComputePass(timestamps());
559
- computePass.setPipeline(object(scope,id,"compute").value);
560
- computePass.setBindGroup(0,object(scope,integer(w,16),"group").value); computePass.dispatchWorkgroups(...xyz); stats.dispatches++;
561
- } else if (op === A.DOLLY_GPU_COPY_BUFFER) {
562
- const src=object(scope,id,"buffer"), dst=object(scope,integer(w,16),"buffer"), so=integer(w,24), to=integer(w,32), n=integer(w,40);
563
- range(src,so,n);range(dst,to,n);getEncoder().copyBufferToBuffer(src.value,so,dst.value,to,n);
564
- } else if (op === A.DOLLY_GPU_CAPTURE_FRAME) {
565
- ensure(scope.surface && texture, "Capture requires a rendered surface in this batch");
566
- const x=w.getUint32(16,true), y=w.getUint32(20,true), width=w.getUint32(24,true), height=w.getUint32(28,true);
567
- ensure(width>0 && height>0 && x<=texture.width-width && y<=texture.height-height, "Capture rectangle outside surface");
568
- const dst=object(scope,id,"buffer"), bytesPerRow=Math.ceil(width*4/256)*256;
569
- range(dst,0,bytesPerRow*(height-1)+width*4);
570
- ensure(!dst.mapped && (dst.value.usage & GPUBufferUsage.COPY_DST), "Capture destination is mapped or not writable");
571
- getEncoder().copyTextureToBuffer({texture,origin:{x,y}}, {buffer:dst.value,bytesPerRow}, {width,height});
572
- } else if (op === A.DOLLY_GPU_SUBMIT) {
573
- ensure(encoder,"No GPU commands to submit");
574
- await queueFull(scope);
575
- const measured = queries ? timer : null, count = queries;
576
- if (measured) {
577
- encoder.resolveQuerySet(measured.query,0,count,measured.resolve,0);
578
- encoder.copyBufferToBuffer(measured.resolve,0,measured.read,0,count*8);
579
- measured.busy = true;
580
- }
581
- device.queue.submit([encoder.finish()]);encoder=null;
582
- const submittedWork = ++progress.queued;
583
- if (measured) measured.read.mapAsync(GPUMapMode.READ,0,count*8).then(() => {
584
- const values=new BigUint64Array(measured.read.getMappedRange(0,count*8));
585
- let ns=0n;for(let i=0;i<count;i+=2)if(values[i+1]>=values[i])ns+=values[i+1]-values[i];
586
- if (!scope.lost) {scope.gpuMs=Number(ns)/1e6;scope.gpuTotalMs+=scope.gpuMs;scope.gpuSamples++;}
587
- measured.read.unmap();
588
- }).catch(()=>{}).finally(()=>{measured.busy=false;});
589
- timer=null;queries=0;
590
- const completed=device.queue.onSubmittedWorkDone().then(()=>{
591
- progress.completed=Math.max(progress.completed,submittedWork);
592
- }).finally(()=>{
593
- scope.submissions.splice(scope.submissions.indexOf(completed),1);
594
- if(scope.frames[0]===completed)scope.frames.shift();
595
- });
596
- scope.submissions.push(completed);
597
- if(texture)scope.frames.push(completed);
598
- completed.catch(()=>{});
599
- if(texture){stats.frames++;postMessage({type:"status",active:true,width:canvas.width,height:canvas.height,stats:{...stats,allocatedBytes:usedBytes}});texture=null;}
600
- } else if (op === A.DOLLY_GPU_MAP_READ) {
601
- ensure(!encoder,"Submit before mapping");
602
- const r=object(scope,id,"buffer"), offset=integer(w,16), size=integer(w,24);range(r,offset,size);
603
- ensure(!r.mapped && size<=16*1024*1024,"Mapping limit",E.E2BIG);
604
- await r.value.mapAsync(GPUMapMode.READ,offset,size);r.mapped=r.value.getMappedRange(offset,size);
605
- } else if (op === A.DOLLY_GPU_UNMAP) {
606
- const r=object(scope,id,"buffer");r.value.unmap();r.mapped=null;
607
- } else if (op === A.DOLLY_GPU_RELEASE) {
608
- ensure(!encoder,"Submit before releasing GPU resources");
609
- release(scope, id, progress);
1433
+ for (const record of records) try {
1434
+ const { op } = record, definition = operations.get(op);
1435
+ if (op !== A.DOLLY_GPU_COMPUTE && !batch.explicitCompute) endCompute(batch);
1436
+ if (batch.renderPass) ensure((batch.viewPass ? viewPassRecords : renderPassRecords).has(op), "Command is invalid inside a render pass");
1437
+ if (batch.explicitCompute) ensure(computePassRecords.has(op), "Command is invalid inside a compute pass");
1438
+ if (definition.creates) {
1439
+ ensure(record.id > scope.high && record.id <= 0xffffffff && scope.objects.size + scope.retired.size < maxObjects,
1440
+ "GPU object quota", E.ENOSPC);
610
1441
  }
1442
+ // Most runs are synchronous; awaiting only the others keeps a batch from yielding between them.
1443
+ const running = definition.run(batch, record);
1444
+ if (running) await running;
1445
+ } catch (error) {
1446
+ if (error.record) throw error;
1447
+ // WebGPU's own errors (a GPUPipelineError, a DOMException) keep their message read-only.
1448
+ fail(error.errno ?? (error.reason === "validation" ? E.EINVAL : E.EIO), `record ${record.position} (opcode ${record.op}): ${error.message}`);
611
1449
  }
612
- ensure(!encoder,"GPU batch has an unsubmitted encoder");
1450
+ await compiled(batch);
1451
+ if (flags & A.DOLLY_GPU_BATCH_CONTINUE) keepOpen = true;
1452
+ else ensure(!batch.encoder, "GPU batch has an unsubmitted encoder");
613
1453
  } finally {
614
- endCompute();endRender();
615
- const [oom, validation] = await Promise.all([device.popErrorScope(), device.popErrorScope()]);
616
- stats.batchWallMilliseconds += performance.now()-started;
617
- if(oom)fail(E.ENOMEM,oom.message);if(validation)fail(E.EINVAL,validation.message);
1454
+ if (keepOpen) scope.open = batch;
1455
+ else { endCompute(batch); endRender(batch); }
1456
+ validated(scope, Promise.all([device.popErrorScope(), device.popErrorScope()]));
1457
+ stats.batchWallMilliseconds += performance.now() - started;
618
1458
  }
619
1459
  }
1460
+ // WebGPU validates a batch on the GPU process's timeline, and Chrome answers an error scope only
1461
+ // after a round trip there (3.6 ms a batch, 2026-11-08), so a batch is answered without it: the
1462
+ // first error reaches the page's console when it is known and fails the scope's next WAIT.
1463
+ function validated(scope, scopes) {
1464
+ const settled = scopes.then(([oom, validation]) => {
1465
+ const error = oom ? [E.ENOMEM, oom.message] : validation ? [E.EINVAL, validation.message] : null;
1466
+ if (!error || scope.lost) return;
1467
+ postMessage({ type: "status", error: error[1].slice(0, 2048) });
1468
+ scope.invalid ??= error;
1469
+ }, () => {}).finally(() => scope.validations.splice(scope.validations.indexOf(settled), 1));
1470
+ scope.validations.push(settled);
1471
+ }
620
1472
 
621
1473
  function publish(request, error, data = new Uint8Array()) {
622
- const base=mailbox+request.index*(64+A.DOLLY_GPU_REPLY_BYTES), words=new Int32Array(memory,base,16);
623
- new Uint8Array(memory,base+64,data.length).set(data);
624
- Atomics.store(words,1,request.scope);Atomics.store(words,2,request.sequence);
625
- Atomics.store(words,3,error);Atomics.store(words,4,data.length);Atomics.store(words,0,1);
626
- postMessage({type:"complete"});
1474
+ const base = mailbox + request.index * (64 + A.DOLLY_GPU_REPLY_BYTES), words = new Int32Array(memory, base, 16);
1475
+ new Uint8Array(memory, base + 64, data.length).set(data);
1476
+ Atomics.store(words, 1, request.scope);
1477
+ Atomics.store(words, 2, request.sequence);
1478
+ Atomics.store(words, 3, error);
1479
+ Atomics.store(words, 4, data.length);
1480
+ Atomics.store(words, 0, 1);
1481
+ postMessage({ type: "complete" });
1482
+ }
1483
+ async function open(request, scope) {
1484
+ scope.device = await getDevice();
1485
+ ensure(!scope.lost, "GPU scope cancelled", E.ECANCELED);
1486
+ const width = u32(request.v, 32), height = u32(request.v, 36);
1487
+ ensure((width === 0 && height === 0) || (width > 0 && height > 0 && width <= 4096 && height <= 2304), "Invalid surface size");
1488
+ ensure(!width || context, "GPU presentation requires the display provider", E.ENOSYS);
1489
+ scope.surface = width > 0;
1490
+ if (scope.surface) { scope.size = { width, height }; surfaces.push(scope); }
1491
+ scope.gpuMs = 0; scope.gpuTotalMs = 0; scope.gpuSamples = 0;
1492
+ if (device.features.has("timestamp-query")) scope.timers = Array.from({ length: 3 }, () => ({
1493
+ query: device.createQuerySet({ type: "timestamp", count: timestampQueries }),
1494
+ resolve: device.createBuffer({ size: timestampQueries * 8, usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC }),
1495
+ read: device.createBuffer({ size: timestampQueries * 8, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST }), busy: false }));
1496
+ // A fixed name: the adapter's vendor/description would fingerprint the device.
1497
+ const name = encode.encode("WebGPU"), output = new Uint8Array(16 + name.length), v = new DataView(output.buffer);
1498
+ v.setBigUint64(0, BigInt(scope.id), true);
1499
+ v.setUint32(8, width, true);
1500
+ v.setUint32(12, height, true);
1501
+ output.set(name, 16);
1502
+ return output;
1503
+ }
1504
+ async function wait(scope) {
1505
+ const submitted = queueProgress.queued;
1506
+ await Promise.all([settled(device, device.queue.onSubmittedWorkDone()), ...scope.validations]);
1507
+ queueProgress.completed = Math.max(queueProgress.completed, submitted);
1508
+ collectReleased(scope, queueProgress.completed);
1509
+ const invalid = scope.invalid;
1510
+ scope.invalid = null;
1511
+ if (invalid) throw Object.assign(new Error(invalid[1]), { errno: invalid[0], reported: true });
1512
+ }
1513
+ function readback(request, scope) {
1514
+ const r = object(scope, integer(request.v, 32), "buffer"), offset = integer(request.v, 40), size = integer(request.v, 48);
1515
+ ensure(r.mapped && size <= A.DOLLY_GPU_REPLY_BYTES && offset <= r.mapped.byteLength - size, "Invalid readback");
1516
+ stats.readbackBytes += size;
1517
+ return new Uint8Array(r.mapped, offset, size).slice();
1518
+ }
1519
+ function info(scope) {
1520
+ const output = new Uint8Array(80), v = new DataView(output.buffer);
1521
+ v.setUint32(0, scope.timers ? 1 : 0, true);
1522
+ v.setUint32(4, A.DOLLY_GPU_MAX_BINDINGS, true);
1523
+ v.setBigUint64(8, BigInt(maxBuffer), true);
1524
+ v.setBigUint64(16, BigInt(maxBytes), true);
1525
+ v.setFloat64(24, scope.gpuMs, true);
1526
+ v.setFloat64(32, scope.gpuTotalMs, true);
1527
+ v.setBigUint64(40, BigInt(scope.gpuSamples), true);
1528
+ v.setFloat64(48, stats.batchWallMilliseconds, true);
1529
+ v.setBigUint64(56, BigInt(stats.frames), true);
1530
+ v.setBigUint64(64, BigInt(stats.dispatches), true);
1531
+ v.setBigUint64(72, BigInt(usedBytes), true);
1532
+ return output;
627
1533
  }
628
1534
  async function execute(request, scope, parsed) {
629
1535
  try {
630
- let output=new Uint8Array();
631
- if(request.op===A.DOLLY_GPU_OPEN) {
632
- scope.device=await getDevice();
633
- ensure(!scope.lost,"GPU scope cancelled",E.ECANCELED);
634
- const width=request.v.getUint32(32,true),height=request.v.getUint32(36,true);
635
- ensure((width===0&&height===0)||(width>0&&height>0&&width<=4096&&height<=2304),"Invalid surface size");
636
- ensure(!width || !slots.some(s=>s && s!==scope && s.surface),"GPU surface is busy",E.EBUSY);
637
- ensure(!width || context, "GPU presentation requires the display provider", E.ENOSYS);
638
- scope.surface=width>0;
639
- scope.gpuMs=0;scope.gpuTotalMs=0;scope.gpuSamples=0;
640
- if(device.features.has("timestamp-query")) scope.timers=Array.from({length:3},()=>({
641
- query:device.createQuerySet({type:"timestamp",count:timestampQueries}),
642
- resolve:device.createBuffer({size:timestampQueries*8,usage:GPUBufferUsage.QUERY_RESOLVE|GPUBufferUsage.COPY_SRC}),
643
- read:device.createBuffer({size:timestampQueries*8,usage:GPUBufferUsage.MAP_READ|GPUBufferUsage.COPY_DST}),busy:false}));
644
- // A fixed name: the adapter's vendor/description would fingerprint the device.
645
- const name=encode.encode("WebGPU");
646
- output=new Uint8Array(16+name.length);
647
- const v=new DataView(output.buffer);
648
- v.setBigUint64(0,BigInt(scope.id),true);v.setUint32(8,width,true);v.setUint32(12,height,true);
649
- output.set(name,16);
650
- } else {
651
- ensure(!scope.lost && device,"GPU scope lost",E.EIO);
652
- if(request.op===A.DOLLY_GPU_BATCH)await batch(scope,parsed);
653
- else if(request.op===A.DOLLY_GPU_WAIT) {
654
- const submitted=queueProgress.queued;
655
- await device.queue.onSubmittedWorkDone();
656
- queueProgress.completed=Math.max(queueProgress.completed,submitted);
657
- collectReleased(scope,queueProgress.completed);
658
- }
659
- else if(request.op===A.DOLLY_GPU_READ) {
660
- const r=object(scope,integer(request.v,32),"buffer"),offset=integer(request.v,40),size=integer(request.v,48);
661
- ensure(r.mapped && size<=A.DOLLY_GPU_REPLY_BYTES && offset<=r.mapped.byteLength-size,"Invalid readback");
662
- output=new Uint8Array(r.mapped,offset,size).slice();stats.readbackBytes+=size;
663
- } else if(request.op===A.DOLLY_GPU_CLOSE)await retire(scope);
664
- else if(request.op===A.DOLLY_GPU_CAPABILITIES)output=capabilities;
665
- else if(request.op===A.DOLLY_GPU_INFO) {
666
- output=new Uint8Array(80);const v=new DataView(output.buffer);
667
- v.setUint32(0,scope.timers?1:0,true);v.setUint32(4,A.DOLLY_GPU_MAX_BINDINGS,true);
668
- v.setBigUint64(8,BigInt(maxBuffer),true);v.setBigUint64(16,BigInt(maxBytes),true);
669
- v.setFloat64(24,scope.gpuMs,true);v.setFloat64(32,scope.gpuTotalMs,true);v.setBigUint64(40,BigInt(scope.gpuSamples),true);
670
- v.setFloat64(48,stats.batchWallMilliseconds,true);v.setBigUint64(56,BigInt(stats.frames),true);
671
- v.setBigUint64(64,BigInt(stats.dispatches),true);v.setBigUint64(72,BigInt(usedBytes),true);
672
- }
1536
+ let output = new Uint8Array();
1537
+ if (request.op === A.DOLLY_GPU_OPEN) output = await open(request, scope);
1538
+ else {
1539
+ ensure(!scope.lost && device, "GPU scope lost", E.EIO);
1540
+ if (request.op === A.DOLLY_GPU_BATCH) await runBatch(scope, parsed, u32(request.v, 36));
1541
+ else if (request.op === A.DOLLY_GPU_WAIT) await wait(scope);
1542
+ else if (request.op === A.DOLLY_GPU_READ) output = readback(request, scope);
1543
+ else if (request.op === A.DOLLY_GPU_CLOSE) await retire(scope);
1544
+ else if (request.op === A.DOLLY_GPU_CAPABILITIES) output = capabilities;
1545
+ else if (request.op === A.DOLLY_GPU_LIMITS) output = limits;
1546
+ else if (request.op === A.DOLLY_GPU_INFO) output = info(scope);
673
1547
  }
674
- publish(request,0,output);
675
- } catch(error) {
676
- postMessage({type:"status",error:String(error.message??error).slice(0,2048)});
677
- if(request.op===A.DOLLY_GPU_OPEN)await retire(scope);
678
- publish(request,error.errno??E.EIO);
679
- } finally {scope.busy=false;}
1548
+ publish(request, 0, output);
1549
+ } catch (error) {
1550
+ if (!error.reported) postMessage({ type: "status", error: String(error.message ?? error).slice(0, 2048) });
1551
+ if (request.op === A.DOLLY_GPU_OPEN) await retire(scope);
1552
+ publish(request, error.errno ?? E.EIO);
1553
+ } finally { scope.busy = false; }
680
1554
  }
681
1555
 
1556
+ // A request names its packet in shared memory, which is copied before it is checked; address 0
1557
+ // cancels the scope that `bytes` names.
682
1558
  self.onmessage = event => {
683
- const m=event.data;
684
- if(m.type==="configure") {
685
- heap=m.memory;memory=heap.buffer;mailbox=m.mailbox;canvas=m.canvas;control=new Int32Array(m.control);
686
- ensure(memory instanceof SharedArrayBuffer && Number.isSafeInteger(mailbox) && mailbox>0 && mailbox%64===0 &&
687
- mailbox<=memory.byteLength-slots.length*(64+A.DOLLY_GPU_REPLY_BYTES),"Invalid GPU mailbox");
688
- postMessage({type:"ready"});
1559
+ const m = event.data;
1560
+ if (m.type === "configure") {
1561
+ heap = m.memory; memory = heap.buffer; mailbox = m.mailbox; canvas = m.canvas; control = new Int32Array(m.control);
1562
+ pageFrames = m.shown ? new Int32Array(m.shown) : null;
1563
+ ensure(memory instanceof SharedArrayBuffer && Number.isSafeInteger(mailbox) && mailbox > 0 && mailbox % 64 === 0 &&
1564
+ mailbox <= memory.byteLength - slots.length * (64 + A.DOLLY_GPU_REPLY_BYTES), "Invalid GPU mailbox");
1565
+ postMessage({ type: "ready" });
689
1566
  return;
690
1567
  }
691
- if(m.type!=="request")return;
692
- memory=heap.buffer;
693
- let status=0;
1568
+ if (m.type !== "request") return;
1569
+ memory = heap.buffer;
1570
+ let status = 0;
694
1571
  try {
695
- const address=Number(m.address),size=Number(m.bytes);
696
- if(address===0) {
697
- ensure(Number.isSafeInteger(size)&&size>0&&size<=0xffffffff,"Invalid scope cancellation");
698
- const scope=slots[(size-1)%slots.length];
699
- if(scope?.id===size) {
700
- scope.lost=true;
701
- serial=serial.then(()=>retire(scope));
1572
+ const address = Number(m.address), size = Number(m.bytes);
1573
+ if (address === 0) {
1574
+ ensure(Number.isSafeInteger(size) && size > 0 && size <= 0xffffffff, "Invalid scope cancellation");
1575
+ const scope = slots[(size - 1) % slots.length];
1576
+ if (scope?.id === size) {
1577
+ scope.lost = true;
1578
+ serial = serial.then(() => retire(scope));
702
1579
  }
703
1580
  } else {
704
- ensure(Number.isSafeInteger(address)&&Number.isSafeInteger(size)&&address>0&&size>=32&&size<=A.DOLLY_GPU_PACKET_BYTES&&address<=memory.byteLength-size,"Invalid GPU request span",E.EFAULT);
705
- const request=header(new Uint8Array(memory,address,size).slice());
706
- ensure(request.op>=A.DOLLY_GPU_OPEN&&request.op<=A.DOLLY_GPU_CAPABILITIES,"Unknown GPU operation",E.ENOTSUP);
707
- ensure(!requestBytes.has(request.op)||requestBytes.get(request.op)===size,"GPU operation layout");
708
- const parsed=request.op===A.DOLLY_GPU_BATCH?records(request):null;
709
- let scope=slots[request.index];
710
- if(request.op===A.DOLLY_GPU_OPEN) {
711
- ensure(!scope&&request.scope>generations[request.index],"GPU scope slot is busy",E.EBUSY);
712
- scope={id:request.scope,index:request.index,sequence:0,objects:new Map(),retired:new Map(),high:0,submissions:[],frames:[],lost:false,surface:false};
713
- generations[request.index]=scope.id;slots[request.index]=scope;
1581
+ ensure(Number.isSafeInteger(address) && Number.isSafeInteger(size) && address > 0 && size >= 32 &&
1582
+ size <= A.DOLLY_GPU_PACKET_BYTES && address <= memory.byteLength - size, "Invalid GPU request span", E.EFAULT);
1583
+ const request = header(new Uint8Array(memory, address, size).slice());
1584
+ ensure(request.op >= A.DOLLY_GPU_OPEN && request.op <= A.DOLLY_GPU_LIMITS, "Unknown GPU operation", E.ENOTSUP);
1585
+ ensure(!requestBytes.has(request.op) || requestBytes.get(request.op) === size, "GPU operation layout");
1586
+ const parsed = request.op === A.DOLLY_GPU_BATCH ? records(request) : null;
1587
+ let scope = slots[request.index];
1588
+ if (request.op === A.DOLLY_GPU_OPEN) {
1589
+ ensure(!scope && request.scope > generations[request.index], "GPU scope slot is busy", E.EBUSY);
1590
+ scope = { id: request.scope, index: request.index, sequence: 0, objects: new Map(), retired: new Map(), high: 0,
1591
+ submissions: [], frames: [], shows: [], validations: [], invalid: null, lost: false, surface: false };
1592
+ generations[request.index] = scope.id;
1593
+ slots[request.index] = scope;
714
1594
  }
715
- ensure(scope?.id===request.scope,"Stale GPU scope",E.ESTALE);
716
- ensure(!scope.busy&&request.sequence>scope.sequence,"GPU request is busy or repeated",E.EBUSY);
717
- scope.sequence=request.sequence;scope.busy=true;stats.packets++;stats.packetBytes+=size;
718
- serial=serial.then(()=>execute(request,scope,parsed)).catch(error=>{publish(request,E.EIO);scope.busy=false;postMessage({type:"status",error:String(error).slice(0,2048)});});
1595
+ ensure(scope?.id === request.scope, "Stale GPU scope", E.ESTALE);
1596
+ ensure(!scope.busy && request.sequence > scope.sequence, "GPU request is busy or repeated", E.EBUSY);
1597
+ scope.sequence = request.sequence;
1598
+ scope.busy = true;
1599
+ stats.packets++;
1600
+ stats.packetBytes += size;
1601
+ serial = serial.then(() => execute(request, scope, parsed)).catch(error => {
1602
+ publish(request, E.EIO);
1603
+ scope.busy = false;
1604
+ postMessage({ type: "status", error: String(error).slice(0, 2048) });
1605
+ });
719
1606
  }
720
- } catch(error) {status=-(error.errno??E.EINVAL);}
721
- finally {Atomics.store(control,1,status);Atomics.store(control,0,0);Atomics.notify(control,0);}
1607
+ } catch (error) { status = -(error.errno ?? E.EINVAL); }
1608
+ finally { Atomics.store(control, 1, status); Atomics.store(control, 0, 0); Atomics.notify(control, 0); }
722
1609
  };