@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.
- package/abi.mjs +33 -1
- package/bridge.mjs +2 -2
- package/gpu.mjs +22 -9
- package/package.json +2 -2
- 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
|
|
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
|
-
|
|
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",
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
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
|
|
19
|
-
//
|
|
20
|
-
//
|
|
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
|
|
40
|
-
const
|
|
41
|
-
A.
|
|
42
|
-
A.
|
|
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
|
-
|
|
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 =
|
|
176
|
+
const requiredFeatures = reportedFeatures.filter(name => adapter.features.has(name));
|
|
61
177
|
const requiredLimits = {};
|
|
62
|
-
for (const
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
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 &&
|
|
127
|
-
const value =
|
|
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
|
|
134
|
-
const count = v
|
|
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
|
|
238
|
+
for (let position = 0; position < count; ++position) {
|
|
138
239
|
ensure(offset <= bytes.length - 8, "Truncated GPU command");
|
|
139
|
-
const
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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,
|
|
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)
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
307
|
-
const
|
|
308
|
-
if (op !== A.DOLLY_GPU_COMPUTE) endCompute();
|
|
309
|
-
if (renderPass) ensure(renderPassRecords.has(op), "Command is invalid inside a render pass");
|
|
310
|
-
if (
|
|
311
|
-
if (
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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);
|
|
625
|
-
Atomics.store(words,
|
|
626
|
-
|
|
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
|
-
|
|
633
|
-
ensure(!scope.lost,"GPU scope
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
if(
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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&&
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
ensure(
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
scope
|
|
713
|
-
|
|
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;
|
|
718
|
-
|
|
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
|
|
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
|
};
|