@daugasauron/dolly-host-gpu 0.0.0-stage → 0.1.4-rc.0
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/README.md +3 -2
- package/abi.mjs +50 -0
- package/bridge.mjs +45 -0
- package/gpu.mjs +114 -0
- package/module.json +15 -0
- package/package.json +12 -4
- package/worker.mjs +705 -0
package/README.md
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @daugasauron/dolly-host-gpu
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Dolly's `gpu@0` host module: WebGPU on the browser's adapter, over the display. Give the URL of its
|
|
4
|
+
`module.json` to `boot()`'s `modules`; @daugasauron/dolly's README says how.
|
package/abi.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Generated from host/gpu/dolly-gpu-0.wat, host/gpu/gpu.h, include/dolly/process.h.
|
|
2
|
+
export const DOLLY_GPU_VERSION = 0;
|
|
3
|
+
export const DOLLY_GPU_PROCESS_OP = 128;
|
|
4
|
+
export const DOLLY_GPU_SLOTS = 8;
|
|
5
|
+
export const DOLLY_GPU_REPLY_BYTES = 65536;
|
|
6
|
+
export const DOLLY_GPU_PACKET_BYTES = 1048576;
|
|
7
|
+
export const DOLLY_GPU_HEADER_BYTES = 32;
|
|
8
|
+
export const DOLLY_GPU_OPEN = 1;
|
|
9
|
+
export const DOLLY_GPU_BATCH = 2;
|
|
10
|
+
export const DOLLY_GPU_WAIT = 3;
|
|
11
|
+
export const DOLLY_GPU_READ = 4;
|
|
12
|
+
export const DOLLY_GPU_CLOSE = 5;
|
|
13
|
+
export const DOLLY_GPU_INFO = 6;
|
|
14
|
+
export const DOLLY_GPU_CAPABILITIES = 7;
|
|
15
|
+
export const DOLLY_GPU_MAX_BINDINGS = 16;
|
|
16
|
+
export const DOLLY_GPU_MAX_COMMANDS = 1024;
|
|
17
|
+
export const DOLLY_GPU_FEATURE_LARGE_BATCH = 128;
|
|
18
|
+
export const DOLLY_GPU_CREATE_BUFFER = 1;
|
|
19
|
+
export const DOLLY_GPU_WRITE_BUFFER = 2;
|
|
20
|
+
export const DOLLY_GPU_CREATE_SHADER = 3;
|
|
21
|
+
export const DOLLY_GPU_RENDER_PIPELINE = 4;
|
|
22
|
+
export const DOLLY_GPU_COMPUTE_PIPELINE = 5;
|
|
23
|
+
export const DOLLY_GPU_BIND_GROUP = 6;
|
|
24
|
+
export const DOLLY_GPU_RENDER = 7;
|
|
25
|
+
export const DOLLY_GPU_COMPUTE = 8;
|
|
26
|
+
export const DOLLY_GPU_COPY_BUFFER = 9;
|
|
27
|
+
export const DOLLY_GPU_MAP_READ = 10;
|
|
28
|
+
export const DOLLY_GPU_UNMAP = 11;
|
|
29
|
+
export const DOLLY_GPU_RELEASE = 12;
|
|
30
|
+
export const DOLLY_GPU_SUBMIT = 13;
|
|
31
|
+
export const DOLLY_GPU_VERTEX_PIPELINE = 14;
|
|
32
|
+
export const DOLLY_GPU_RENDER_VERTEX = 15;
|
|
33
|
+
export const DOLLY_GPU_COMPUTE_CONSTANTS = 16;
|
|
34
|
+
export const DOLLY_GPU_CAPTURE_FRAME = 17;
|
|
35
|
+
export const DOLLY_GPU_FEATURE_CAPTURE_FRAME = 16;
|
|
36
|
+
export const DOLLY_GPU_FEATURE_SURFACE_BGRA = 32;
|
|
37
|
+
export const DOLLY_GPU_FEATURE_TEXTURE_RENDER = 64;
|
|
38
|
+
export const DOLLY_GPU_FEATURE_TEXTURE_BC = 256;
|
|
39
|
+
export const DOLLY_GPU_FEATURE_VERTEX_F16 = 512;
|
|
40
|
+
export const DOLLY_GPU_CREATE_TEXTURE = 18;
|
|
41
|
+
export const DOLLY_GPU_WRITE_TEXTURE = 19;
|
|
42
|
+
export const DOLLY_GPU_CREATE_SAMPLER = 20;
|
|
43
|
+
export const DOLLY_GPU_GRAPHICS_PIPELINE = 21;
|
|
44
|
+
export const DOLLY_GPU_RESOURCE_GROUP = 22;
|
|
45
|
+
export const DOLLY_GPU_BEGIN_RENDER_PASS = 23;
|
|
46
|
+
export const DOLLY_GPU_END_RENDER_PASS = 24;
|
|
47
|
+
export const DOLLY_GPU_DRAW_MESH = 25;
|
|
48
|
+
export const DOLLY_GPU_VIEWPORT = 26;
|
|
49
|
+
export const DOLLY_ERRNO = Object.freeze({ E2BIG: 1, EACCES: 2, EADDRINUSE: 3, EADDRNOTAVAIL: 4, EAFNOSUPPORT: 5, EAGAIN: 6, EALREADY: 7, EBADF: 8, EBADMSG: 9, EBUSY: 10, ECANCELED: 11, ECHILD: 12, ECONNABORTED: 13, ECONNREFUSED: 14, ECONNRESET: 15, EDEADLK: 16, EDESTADDRREQ: 17, EDOM: 18, EDQUOT: 19, EEXIST: 20, EFAULT: 21, EFBIG: 22, EHOSTUNREACH: 23, EIDRM: 24, EILSEQ: 25, EINPROGRESS: 26, EINTR: 27, EINVAL: 28, EIO: 29, EISCONN: 30, EISDIR: 31, ELOOP: 32, EMFILE: 33, EMLINK: 34, EMSGSIZE: 35, EMULTIHOP: 36, ENAMETOOLONG: 37, ENETDOWN: 38, ENETRESET: 39, ENETUNREACH: 40, ENFILE: 41, ENOBUFS: 42, ENODEV: 43, ENOENT: 44, ENOEXEC: 45, ENOLCK: 46, ENOLINK: 47, ENOMEM: 48, ENOMSG: 49, ENOPROTOOPT: 50, ENOSPC: 51, ENOSYS: 52, ENOTCONN: 53, ENOTDIR: 54, ENOTEMPTY: 55, ENOTRECOVERABLE: 56, ENOTSOCK: 57, ENOTTY: 59, ENXIO: 60, EOVERFLOW: 61, EOWNERDEAD: 62, EPERM: 63, EPIPE: 64, EPROTO: 65, EPROTONOSUPPORT: 66, EPROTOTYPE: 67, ERANGE: 68, EROFS: 69, ESPIPE: 70, ESRCH: 71, ESTALE: 72, ETIMEDOUT: 73, ETXTBSY: 74, EXDEV: 75, ENODATA: 116, ENOTSUP: 138 });
|
|
50
|
+
export const DOLLY_GPU_ABI_DIGEST = "c947036666d5532b78179512c852d1fd58a143e067ca8c947055072ac7fc104a";
|
package/bridge.mjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { DOLLY_ERRNO as E } from "./abi.mjs";
|
|
2
|
+
import { DOLLY_GPU_SLOTS, DOLLY_GPU_REPLY_BYTES } from "./abi.mjs";
|
|
3
|
+
|
|
4
|
+
export async function createGpuBridge(memory, mailbox, canvas, complete, status) {
|
|
5
|
+
const worker = new Worker(new URL("./worker.mjs", import.meta.url), { type: "module", name: "dolly-gpu" });
|
|
6
|
+
const control = new Int32Array(new SharedArrayBuffer(8));
|
|
7
|
+
let failed = false;
|
|
8
|
+
let ready, rejectReady;
|
|
9
|
+
const initialized = new Promise((resolve, reject) => { ready = resolve; rejectReady = reject; });
|
|
10
|
+
const timer = setTimeout(() => stop("GPU provider did not initialize"), 10000);
|
|
11
|
+
function stop(message) {
|
|
12
|
+
if (failed) return;
|
|
13
|
+
failed = true;
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
rejectReady(new Error(message));
|
|
16
|
+
worker.terminate();
|
|
17
|
+
// A failed provider must wake every admitted process, including a wait.
|
|
18
|
+
for (let i=0; i<DOLLY_GPU_SLOTS; ++i) {
|
|
19
|
+
const words = new Int32Array(memory.buffer, mailbox+i*(64+DOLLY_GPU_REPLY_BYTES), 16);
|
|
20
|
+
Atomics.store(words,3,E.EIO); Atomics.store(words,4,0); Atomics.store(words,0,1);
|
|
21
|
+
}
|
|
22
|
+
status({active:false,error:message});
|
|
23
|
+
queueMicrotask(complete);
|
|
24
|
+
}
|
|
25
|
+
worker.onmessage = ({data}) => {
|
|
26
|
+
if (data.type === "ready") { clearTimeout(timer); ready(); }
|
|
27
|
+
else if (data.type === "complete") complete();
|
|
28
|
+
else if (data.type === "status") status(data);
|
|
29
|
+
};
|
|
30
|
+
worker.onerror = () => stop("GPU provider worker failed");
|
|
31
|
+
worker.postMessage({type:"configure", memory, mailbox, canvas, control:control.buffer}, canvas ? [canvas] : []);
|
|
32
|
+
await initialized;
|
|
33
|
+
function dispatch({address, bytes}) {
|
|
34
|
+
if (failed) return -E.EIO;
|
|
35
|
+
Atomics.store(control,0,1);
|
|
36
|
+
worker.postMessage({type:"request",address,bytes});
|
|
37
|
+
// Only wait for a bounded packet copy/admission, never for GPU execution.
|
|
38
|
+
if (Atomics.wait(control,0,1,5000) === "timed-out") {
|
|
39
|
+
stop("GPU provider stopped acknowledging requests");
|
|
40
|
+
return -E.EIO;
|
|
41
|
+
}
|
|
42
|
+
return Atomics.load(control,1);
|
|
43
|
+
}
|
|
44
|
+
return { dispatch, dispose: () => stop("GPU provider closed") };
|
|
45
|
+
}
|
package/gpu.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createGpuBridge } from "./bridge.mjs";
|
|
2
|
+
import { DOLLY_ERRNO as E } from "./abi.mjs";
|
|
3
|
+
export { DOLLY_GPU_ABI_DIGEST as digest } from "./abi.mjs";
|
|
4
|
+
|
|
5
|
+
// The page names the adapter gpu@0 programs get before any of them runs, or
|
|
6
|
+
// says that there is none. The module is admitted either way: without a
|
|
7
|
+
// device it grants nothing, and OPEN fails with ENODEV, so a program can fall
|
|
8
|
+
// back. A switch after boot passes `signal`, aborted once the registry stops
|
|
9
|
+
// waiting: late, it shows nothing, since nena says why.
|
|
10
|
+
export async function check({ signal: abandoned, ...page }) {
|
|
11
|
+
let adapter, reason = null;
|
|
12
|
+
if (!globalThis.navigator?.gpu) reason = "WebGPU is unavailable in this browser";
|
|
13
|
+
else try {
|
|
14
|
+
adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" });
|
|
15
|
+
if (!adapter) reason = "the browser did not provide a GPU adapter";
|
|
16
|
+
} catch (error) { reason = `GPU adapter initialization failed: ${error.message}`; }
|
|
17
|
+
const { vendor, architecture, description, isFallbackAdapter } = adapter?.info ?? {};
|
|
18
|
+
if (abandoned?.aborted) return null;
|
|
19
|
+
indicate(page, { vendor, architecture, description, isFallbackAdapter, f16: adapter?.features.has("shader-f16") }, reason);
|
|
20
|
+
if (reason) page.showStatus?.(`No WebGPU adapter (${reason}): programs that need the GPU cannot open it`);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// A new adapter state shows the indicators again; no adapter holds them shown.
|
|
25
|
+
function announce({ indicators }, element, state, borderColor) {
|
|
26
|
+
if (element.dataset.gpu !== state) {
|
|
27
|
+
indicators.hold("gpu", state === "unavailable");
|
|
28
|
+
indicators.show();
|
|
29
|
+
}
|
|
30
|
+
element.dataset.gpu = state;
|
|
31
|
+
element.style.borderColor = borderColor;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The page's indicator names the adapter, or says why there is none. Trusted
|
|
35
|
+
// page DOM over the display: guest frames cannot draw, cover or remove it, and
|
|
36
|
+
// only its link takes clicks.
|
|
37
|
+
function indicate(page, info, unavailable) {
|
|
38
|
+
let element = document.querySelector("#gpu-status");
|
|
39
|
+
if (!element) {
|
|
40
|
+
element = document.body.appendChild(document.createElement("div"));
|
|
41
|
+
element.id = "gpu-status";
|
|
42
|
+
element.className = "page-indicator";
|
|
43
|
+
element.setAttribute("role", "status");
|
|
44
|
+
element.style.cssText = "position:fixed;right:0.5rem;bottom:2rem;z-index:8;max-width:calc(100vw - 1rem);" +
|
|
45
|
+
"padding:0.15rem 0.45rem;background:#262626;border:1px solid;font-size:13px;pointer-events:none";
|
|
46
|
+
}
|
|
47
|
+
if (unavailable) {
|
|
48
|
+
element.replaceChildren(`No GPU: ${unavailable}`);
|
|
49
|
+
// The site's docs say how to enable WebGPU; another embedding has none.
|
|
50
|
+
if (page.site) {
|
|
51
|
+
const link = Object.assign(document.createElement("a"), { textContent: "How to enable WebGPU ↗",
|
|
52
|
+
href: new URL("docs/gpu.md#enabling-webgpu", page.site).href, target: "_blank", rel: "noopener" });
|
|
53
|
+
link.style.cssText = "color:#f2d45c;pointer-events:auto";
|
|
54
|
+
element.append(" · ", link);
|
|
55
|
+
}
|
|
56
|
+
announce(page, element, "unavailable", "#e98773");
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
const adapter = [info.vendor, info.architecture, info.description].filter(Boolean).join(" ") || "unnamed WebGPU adapter";
|
|
60
|
+
// SwiftShader and llvmpipe are software adapters even where a browser does not flag them.
|
|
61
|
+
const software = info.isFallbackAdapter || /swiftshader|llvmpipe/i.test(adapter);
|
|
62
|
+
element.textContent = `${software ? "Software adapter, not a GPU" : "GPU"}: ${adapter} · ${info.f16 ? "" : "no "}shader-f16`;
|
|
63
|
+
announce(page, element, software ? "software" : "hardware", software ? "#f2d45c" : "#77736c");
|
|
64
|
+
return { adapter, isFallbackAdapter: info.isFallbackAdapter };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function browser(page) {
|
|
68
|
+
const { mount } = page;
|
|
69
|
+
let canvas, surface;
|
|
70
|
+
if (mount) {
|
|
71
|
+
canvas = document.createElement("canvas");
|
|
72
|
+
canvas.id = "gpu-display";
|
|
73
|
+
canvas.hidden = true;
|
|
74
|
+
canvas.style.cssText = "position:absolute;inset:0;width:100%;height:100%;pointer-events:none";
|
|
75
|
+
mount.append(canvas);
|
|
76
|
+
surface = canvas.transferControlToOffscreen();
|
|
77
|
+
}
|
|
78
|
+
let status = {};
|
|
79
|
+
return {
|
|
80
|
+
configuration: { canvas: surface }, transfers: surface ? [surface] : [],
|
|
81
|
+
get status() { return status; },
|
|
82
|
+
page: { get gpu() { return status; } },
|
|
83
|
+
get surfaceSize() { return status.active ? { width: status.width, height: status.height } : undefined; },
|
|
84
|
+
// The provider reports the adapter of each device it creates, or why it has none.
|
|
85
|
+
messages: { "gpu-status"({ info, unavailable, ...message }) {
|
|
86
|
+
if (info || unavailable) Object.assign(message, indicate(page, info, unavailable));
|
|
87
|
+
status = { ...status, ...message };
|
|
88
|
+
if (message.active) delete status.error;
|
|
89
|
+
if (canvas && message.active !== undefined) canvas.hidden = !message.active;
|
|
90
|
+
if (message.error) console.warn("Dolly GPU:", message.error);
|
|
91
|
+
} },
|
|
92
|
+
dispose() { canvas?.remove(); },
|
|
93
|
+
// Disabled while the page goes on: a session without gpu@0 shows no adapter.
|
|
94
|
+
disabled() {
|
|
95
|
+
document.querySelector("#gpu-status")?.remove();
|
|
96
|
+
page.indicators.hold("gpu", false);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function worker({ send, configuration, get }) {
|
|
102
|
+
let bridge;
|
|
103
|
+
return {
|
|
104
|
+
bindings: { "env.dolly_gpu_dispatch": (address, bytes) => {
|
|
105
|
+
if (!bridge) return -E.ENOSYS;
|
|
106
|
+
return bridge.dispatch({ address, bytes });
|
|
107
|
+
} },
|
|
108
|
+
async start({ kernel, memory }) {
|
|
109
|
+
bridge = await createGpuBridge(memory, Number(kernel.dolly_gpu_mailbox_address()), configuration.canvas,
|
|
110
|
+
() => get("runtime").serviceDeferred(), status => send({ ...status, type: "gpu-status" }));
|
|
111
|
+
},
|
|
112
|
+
dispose() { bridge?.dispose(); bridge = null; },
|
|
113
|
+
};
|
|
114
|
+
}
|
package/module.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gpu",
|
|
3
|
+
"version": 0,
|
|
4
|
+
"gives": "WebGPU on the browser's adapter, over the display",
|
|
5
|
+
"dependencies": ["runtime@0"],
|
|
6
|
+
"phase": "kernel",
|
|
7
|
+
"imports": ["env.dolly_gpu_dispatch"],
|
|
8
|
+
"host": "gpu.mjs",
|
|
9
|
+
"contracts": ["dolly-gpu-0.wat"],
|
|
10
|
+
"process": [],
|
|
11
|
+
"headers": ["gpu.h", "gpu-abi.h"],
|
|
12
|
+
"kernel": ["kernel.c"],
|
|
13
|
+
"client": ["client.c"],
|
|
14
|
+
"package": "libdolly-gpu"
|
|
15
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
+
"version": "0.1.4-rc.0",
|
|
3
|
+
"type": "module",
|
|
2
4
|
"name": "@daugasauron/dolly-host-gpu",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
}
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@daugasauron/dolly": "0.1.4-rc.0"
|
|
8
|
+
},
|
|
9
|
+
"description": "Dolly's gpu@0 host module: WebGPU on the browser's adapter, over the display.",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/daugasauron/dolly.git"
|
|
13
|
+
}
|
|
14
|
+
}
|
package/worker.mjs
ADDED
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import * as A from "./abi.mjs";
|
|
2
|
+
import { DOLLY_ERRNO as E } from "./abi.mjs";
|
|
3
|
+
|
|
4
|
+
const decode = new TextDecoder("utf-8", { fatal: true });
|
|
5
|
+
const encode = new TextEncoder();
|
|
6
|
+
const fail = (code, message) => { throw Object.assign(new Error(message), { errno: code }); };
|
|
7
|
+
const ensure = (condition, message, code = E.EINVAL) => { if (!condition) fail(code, message); };
|
|
8
|
+
const textureFormats = [null, "rgba8unorm", "rgba8unorm-srgb", "r8unorm", "rg8unorm", "depth24plus-stencil8", "depth32float",
|
|
9
|
+
"bc1-rgba-unorm", "bc2-rgba-unorm", "bc3-rgba-unorm"];
|
|
10
|
+
const blockBytes = [0, 4, 4, 1, 2, 4, 4, 8, 16, 16];
|
|
11
|
+
const compareOps = [undefined, "never", "less", "equal", "less-equal", "greater", "not-equal", "greater-equal", "always"];
|
|
12
|
+
const blendOps = ["add", "subtract", "reverse-subtract", "min", "max"];
|
|
13
|
+
const blendFactors = ["zero", "one", "src", "one-minus-src", "dst", "one-minus-dst", "src-alpha", "one-minus-src-alpha", "dst-alpha", "one-minus-dst-alpha", "src-alpha-saturated"];
|
|
14
|
+
const vertexFormats = [null, "float32", "float32x2", "float32x3", "float32x4", "unorm8x4", "snorm8x4", "uint8x4", "sint8x4", "unorm16x2", "unorm16x4", "uint16x2", "uint16x4", "float16x2", "float16x4"];
|
|
15
|
+
const vertexBytes = [0, 4, 8, 12, 16, 4, 4, 4, 4, 4, 8, 4, 8, 4, 8];
|
|
16
|
+
const maxBytes = 4 * 1024 ** 3, bufferCeiling = 1024 ** 3, maxObjects = 4096;
|
|
17
|
+
// What a scope may have queued before its next batch waits: 64 submissions, and
|
|
18
|
+
// for a surface three frames (the submissions that present: a frame may submit
|
|
19
|
+
// many times). Firefox settles completions on a 100 ms timer (bug 1870699), and
|
|
20
|
+
// a model token takes about 25 submissions.
|
|
21
|
+
const queueFull = scope => scope.surface && scope.frames.length >= 3 ? scope.frames[0]
|
|
22
|
+
: scope.submissions.length >= 64 ? scope.submissions[0] : null;
|
|
23
|
+
const timestampQueries = A.DOLLY_GPU_MAX_COMMANDS * 2;
|
|
24
|
+
let maxBuffer = bufferCeiling, capabilities;
|
|
25
|
+
const slots = Array(A.DOLLY_GPU_SLOTS).fill(null), generations = slots.map(() => 0);
|
|
26
|
+
let heap, memory, mailbox, control, canvas, context, device, format;
|
|
27
|
+
let queueProgress = {queued: 0, completed: 0};
|
|
28
|
+
let usedBytes = 0, serial = Promise.resolve(), initializing;
|
|
29
|
+
const stats = { packets: 0, packetBytes: 0, frames: 0, dispatches: 0, readbackBytes: 0, batchWallMilliseconds: 0 };
|
|
30
|
+
|
|
31
|
+
async function getDevice() {
|
|
32
|
+
if (device) return device;
|
|
33
|
+
if (initializing) return initializing;
|
|
34
|
+
initializing = (async () => {
|
|
35
|
+
ensure(navigator.gpu, "This browser has no WebGPU provider", E.ENODEV);
|
|
36
|
+
const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" });
|
|
37
|
+
ensure(adapter, "This browser did not provide a GPU adapter", E.ENODEV);
|
|
38
|
+
const requiredFeatures = ["timestamp-query", "shader-f16", "subgroups", "texture-compression-bc"].filter(name => adapter.features.has(name));
|
|
39
|
+
const requiredLimits = {};
|
|
40
|
+
for (const [name, ceiling] of Object.entries({maxBufferSize: bufferCeiling,
|
|
41
|
+
maxStorageBufferBindingSize: bufferCeiling, maxStorageBuffersPerShaderStage: A.DOLLY_GPU_MAX_BINDINGS,
|
|
42
|
+
maxComputeWorkgroupStorageSize: 65536, maxComputeInvocationsPerWorkgroup: 1024,
|
|
43
|
+
maxComputeWorkgroupSizeX: 1024, maxComputeWorkgroupSizeY: 1024})) {
|
|
44
|
+
requiredLimits[name] = Math.min(adapter.limits[name], ceiling);
|
|
45
|
+
}
|
|
46
|
+
const created = await adapter.requestDevice({requiredFeatures, requiredLimits});
|
|
47
|
+
format = navigator.gpu.getPreferredCanvasFormat();
|
|
48
|
+
maxBuffer = created.limits.maxBufferSize;
|
|
49
|
+
const l = created.limits;
|
|
50
|
+
capabilities = new Uint8Array(128);
|
|
51
|
+
const v = new DataView(capabilities.buffer);
|
|
52
|
+
v.setUint32(0, (created.features.has("shader-f16") ? 1 : 0) | (created.features.has("subgroups") ? 2 : 0) |
|
|
53
|
+
(navigator.gpu.wgslLanguageFeatures?.has("packed_4x8_integer_dot_product") ? 4 : 0) |
|
|
54
|
+
(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 |
|
|
55
|
+
(created.features.has("texture-compression-bc") ? A.DOLLY_GPU_FEATURE_TEXTURE_BC : 0) |
|
|
56
|
+
(format === "bgra8unorm" ? A.DOLLY_GPU_FEATURE_SURFACE_BGRA : 0), true);
|
|
57
|
+
v.setUint32(4, maxObjects, true);
|
|
58
|
+
[maxBuffer, maxBytes, l.maxStorageBufferBindingSize].forEach((n,i) => v.setBigUint64(8+i*8, BigInt(n), true));
|
|
59
|
+
[l.minUniformBufferOffsetAlignment, l.minStorageBufferOffsetAlignment, l.maxComputeWorkgroupStorageSize,
|
|
60
|
+
l.maxComputeInvocationsPerWorkgroup, l.maxComputeWorkgroupSizeX, l.maxComputeWorkgroupSizeY,
|
|
61
|
+
l.maxComputeWorkgroupSizeZ, l.maxComputeWorkgroupsPerDimension, A.DOLLY_GPU_MAX_BINDINGS,
|
|
62
|
+
l.maxStorageBuffersPerShaderStage, l.maxUniformBuffersPerShaderStage, l.maxBindGroups]
|
|
63
|
+
.forEach((n,i) => v.setUint32(32+i*4,n,true));
|
|
64
|
+
v.setBigUint64(80,BigInt(l.maxUniformBufferBindingSize),true);
|
|
65
|
+
v.setUint32(88,adapter.info?.subgroupMinSize ?? 4,true);
|
|
66
|
+
v.setUint32(92,adapter.info?.subgroupMaxSize ?? 128,true);
|
|
67
|
+
device = created;
|
|
68
|
+
const { vendor, architecture, description, isFallbackAdapter } = adapter.info;
|
|
69
|
+
postMessage({ type: "status", info: { vendor, architecture, description, isFallbackAdapter, f16: created.features.has("shader-f16") } });
|
|
70
|
+
queueProgress = {queued: 0, completed: 0};
|
|
71
|
+
if (canvas) {
|
|
72
|
+
context = canvas.getContext("webgpu");
|
|
73
|
+
ensure(context, "WebGPU canvas unavailable", E.ENOSYS);
|
|
74
|
+
context.configure({ device, format, alphaMode: "opaque", usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC });
|
|
75
|
+
}
|
|
76
|
+
created.lost.then(info => {
|
|
77
|
+
if (device !== created) return;
|
|
78
|
+
device = null;
|
|
79
|
+
for (const scope of slots) if (scope) scope.lost = true;
|
|
80
|
+
postMessage({ type: "status", active: false, error: `GPU device lost: ${info.message}` });
|
|
81
|
+
});
|
|
82
|
+
return created;
|
|
83
|
+
})();
|
|
84
|
+
try { return await initializing; }
|
|
85
|
+
catch (error) { postMessage({ type: "status", unavailable: String(error.message ?? error).slice(0, 2048) }); throw error; }
|
|
86
|
+
finally { initializing = null; }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function integer(v, o) {
|
|
90
|
+
const high = v.getUint32(o + 4, true);
|
|
91
|
+
ensure(high <= 0x1fffff, "GPU integer exceeds host range");
|
|
92
|
+
return high * 0x100000000 + v.getUint32(o, true);
|
|
93
|
+
}
|
|
94
|
+
function header(bytes) {
|
|
95
|
+
ensure(bytes.length >= 32, "Short GPU packet");
|
|
96
|
+
const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
97
|
+
const scope = integer(v, 8), sequence = integer(v, 16);
|
|
98
|
+
ensure(v.getUint32(0, true) === 0 && v.getUint32(28, true) === 0 &&
|
|
99
|
+
v.getUint32(24, true) === bytes.length - 32 && scope > 0 && scope <= 0xffffffff &&
|
|
100
|
+
sequence > 0 && sequence <= 0xffffffff, "Invalid GPU header");
|
|
101
|
+
return { op: v.getUint32(4, true), scope, sequence, index: (scope - 1) % slots.length, bytes, v };
|
|
102
|
+
}
|
|
103
|
+
function text(bytes, start, count) {
|
|
104
|
+
ensure(count > 0 && count <= 128 * 1024 && start <= bytes.length - count, "Invalid GPU text span");
|
|
105
|
+
const value = decode.decode(bytes.subarray(start, start + count));
|
|
106
|
+
ensure(!value.includes("\0"), "NUL in GPU text");
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
function records(request) {
|
|
110
|
+
const { bytes, v } = request;
|
|
111
|
+
ensure(bytes.length >= 40 && v.getUint32(36, true) === 0, "Invalid GPU batch");
|
|
112
|
+
const count = v.getUint32(32, true), result = [];
|
|
113
|
+
ensure(count <= A.DOLLY_GPU_MAX_COMMANDS, "Too many GPU commands", E.E2BIG);
|
|
114
|
+
let offset = 40;
|
|
115
|
+
for (let i = 0; i < count; ++i) {
|
|
116
|
+
ensure(offset <= bytes.length - 8, "Truncated GPU command");
|
|
117
|
+
const opcode = v.getUint32(offset, true), size = v.getUint32(offset + 4, true);
|
|
118
|
+
ensure(size >= 8 && size % 8 === 0 && size <= bytes.length - offset, "Invalid GPU command length");
|
|
119
|
+
const b = bytes.subarray(offset, offset + size), w = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
120
|
+
const fixed = { 1: 32, 7: 64, 8: 40, 9: 48, 10: 32, 11: 16, 12: 16, 13: 8, 15: 88, 17: 32, 18: 48, 20: 48, 23: 64, 24: 8, 26: 48 }[opcode];
|
|
121
|
+
if (fixed) ensure(size === fixed, "Wrong GPU command layout");
|
|
122
|
+
else {
|
|
123
|
+
const minimum = { 2: 32, 3: 24, 4: 40, 5: 32, 6: 32, 14: 48, 16: 32, 19: 48, 21: 112, 22: 32, 25: 80 }[opcode];
|
|
124
|
+
ensure(minimum && size >= minimum, "Unsupported GPU command", E.ENOTSUP);
|
|
125
|
+
}
|
|
126
|
+
// Validate every variable-length span before any command has side effects.
|
|
127
|
+
if (opcode === 2) {
|
|
128
|
+
const at = w.getUint32(24, true), n = w.getUint32(28, true);
|
|
129
|
+
ensure(at >= 32 && at <= size - n && n % 4 === 0, "Invalid GPU upload span");
|
|
130
|
+
} else if (opcode === 3) {
|
|
131
|
+
ensure(w.getUint32(20, true) === 0, "Shader reserved field");
|
|
132
|
+
text(b, 24, w.getUint32(16, true));
|
|
133
|
+
} else if (opcode === 4 || opcode === 14) {
|
|
134
|
+
const vs = w.getUint32(32, true), fs = w.getUint32(36, true);
|
|
135
|
+
ensure(vs <= 64 && fs <= 64, "GPU entry name too long");
|
|
136
|
+
let at = 40;
|
|
137
|
+
if (opcode === 14) {
|
|
138
|
+
const count = w.getUint32(44, true), stride = w.getUint32(40, true);
|
|
139
|
+
ensure(count > 0 && count <= 8 && stride > 0 && stride <= 2048 && stride % 4 === 0 && size >= 48 + count * 16, "Vertex layout");
|
|
140
|
+
const locations = new Set();
|
|
141
|
+
for (let i = 0; i < count; i++) {
|
|
142
|
+
const o = 48 + i * 16, location = w.getUint32(o, true), components = w.getUint32(o + 4, true), offset = w.getUint32(o + 8, true);
|
|
143
|
+
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");
|
|
144
|
+
locations.add(location);
|
|
145
|
+
}
|
|
146
|
+
at = 48 + count * 16;
|
|
147
|
+
}
|
|
148
|
+
text(b, at, vs); text(b, at + vs, fs);
|
|
149
|
+
} else if (opcode === 5) {
|
|
150
|
+
ensure(w.getUint32(28, true) === 0 && w.getUint32(24, true) <= 64, "Compute entry name");
|
|
151
|
+
text(b, 32, w.getUint32(24, true));
|
|
152
|
+
} else if (opcode === 16) {
|
|
153
|
+
const entryBytes = w.getUint32(24,true), count = w.getUint32(28,true);
|
|
154
|
+
ensure(entryBytes <= 64 && count <= 16, "Compute constants layout");
|
|
155
|
+
text(b,32,entryBytes);
|
|
156
|
+
let at = (32+entryBytes+7)&~7;
|
|
157
|
+
const names = new Set();
|
|
158
|
+
for (let i=0;i<count;i++) {
|
|
159
|
+
ensure(at<=size-16,"Truncated compute constant");
|
|
160
|
+
const n=w.getUint32(at,true);
|
|
161
|
+
ensure(n<=64 && w.getUint32(at+4,true)===0 && Number.isFinite(w.getFloat64(at+8,true)),"Invalid compute constant");
|
|
162
|
+
const name=text(b,at+16,n);
|
|
163
|
+
ensure(!names.has(name),"Duplicate compute constant");names.add(name);
|
|
164
|
+
at=(at+16+n+7)&~7;
|
|
165
|
+
}
|
|
166
|
+
ensure(at===size,"Trailing compute constant bytes");
|
|
167
|
+
} else if (opcode === 6) {
|
|
168
|
+
const n = w.getUint32(24, true);
|
|
169
|
+
ensure(n <= A.DOLLY_GPU_MAX_BINDINGS && size === 32 + n * 24 && w.getUint32(28, true) === 0, "Bind group layout");
|
|
170
|
+
}
|
|
171
|
+
if (opcode === A.DOLLY_GPU_WRITE_TEXTURE) {
|
|
172
|
+
ensure(w.getUint32(44,true) === 0 && w.getUint32(40,true) > 0 &&
|
|
173
|
+
Math.ceil((48+w.getUint32(40,true))/8)*8 === size, "Texture upload span");
|
|
174
|
+
} else if (opcode === A.DOLLY_GPU_GRAPHICS_PIPELINE) {
|
|
175
|
+
const buffers=w.getUint32(104,true), attributes=w.getUint32(108,true);
|
|
176
|
+
ensure(buffers<=8 && attributes<=16 && size===112+16*(buffers+attributes), "Graphics vertex layout");
|
|
177
|
+
const locations=new Set();
|
|
178
|
+
for(let i=0;i<buffers;i++) {
|
|
179
|
+
const at=112+i*16, stride=w.getUint32(at,true);
|
|
180
|
+
ensure(stride>0 && stride<=2048 && stride%4===0 && w.getUint32(at+4,true)<=1 &&
|
|
181
|
+
w.getBigUint64(at+8,true)===0n, "Graphics vertex stride");
|
|
182
|
+
}
|
|
183
|
+
for(let i=0;i<attributes;i++) {
|
|
184
|
+
const at=112+(buffers+i)*16, slot=w.getUint32(at,true), location=w.getUint32(at+4,true),
|
|
185
|
+
type=w.getUint32(at+8,true), offset=w.getUint32(at+12,true);
|
|
186
|
+
ensure(slot<buffers && location<16 && !locations.has(location) && type>0 && type<vertexFormats.length &&
|
|
187
|
+
offset%Math.min(4,vertexBytes[type])===0 && offset<=w.getUint32(112+slot*16,true)-vertexBytes[type], "Graphics vertex attribute");
|
|
188
|
+
locations.add(location);
|
|
189
|
+
}
|
|
190
|
+
} else if (opcode === A.DOLLY_GPU_RESOURCE_GROUP) {
|
|
191
|
+
ensure(w.getUint32(24,true)<4 && w.getUint32(28,true)<=16 && size===32+32*w.getUint32(28,true), "Resource group layout");
|
|
192
|
+
const bindings=new Set();
|
|
193
|
+
for(let i=0;i<w.getUint32(28,true);i++) {
|
|
194
|
+
const at=32+i*32, binding=w.getUint32(at,true), kind=w.getUint32(at+4,true);
|
|
195
|
+
ensure(binding<32 && !bindings.has(binding) && kind<=3 && (kind===0 ||
|
|
196
|
+
(w.getBigUint64(at+16,true)===0n && w.getBigUint64(at+24,true)===0n)), "Resource binding");
|
|
197
|
+
bindings.add(binding);
|
|
198
|
+
}
|
|
199
|
+
} else if (opcode === A.DOLLY_GPU_DRAW_MESH) {
|
|
200
|
+
const groups=w.getUint32(64,true), buffers=w.getUint32(68,true);
|
|
201
|
+
ensure(groups<=4 && buffers<=8 && size===80+8*groups+24*buffers && w.getBigUint64(72,true)===0n, "Mesh layout");
|
|
202
|
+
}
|
|
203
|
+
result.push({ opcode, b, w });
|
|
204
|
+
offset += size;
|
|
205
|
+
}
|
|
206
|
+
ensure(offset === bytes.length, "Trailing GPU packet bytes");
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
function object(scope, id, kind) {
|
|
210
|
+
const result = scope.objects.get(id);
|
|
211
|
+
ensure(result && (!kind || result.kind === kind), "Invalid GPU resource handle", E.EBADF);
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
function insert(scope, id, kind, value, size = 0) {
|
|
215
|
+
ensure(id > scope.high && id <= 0xffffffff && scope.objects.size + scope.retired.size < maxObjects, "Invalid or exhausted GPU object IDs", E.ENOSPC);
|
|
216
|
+
scope.high = id;
|
|
217
|
+
scope.objects.set(id, { kind, value, size, mapped: null });
|
|
218
|
+
}
|
|
219
|
+
function range(resource, offset, length) {
|
|
220
|
+
ensure(offset >= 0 && length >= 0 && offset <= resource.size - length, "GPU buffer range");
|
|
221
|
+
}
|
|
222
|
+
function collectReleased(scope, completed) {
|
|
223
|
+
for (const [id, resource] of scope.retired) {
|
|
224
|
+
if (resource.submission > completed) continue;
|
|
225
|
+
resource.value.destroy?.();
|
|
226
|
+
usedBytes -= resource.size;
|
|
227
|
+
scope.retired.delete(id);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function release(scope, id, progress) {
|
|
231
|
+
const resource = object(scope, id);
|
|
232
|
+
scope.objects.delete(id);
|
|
233
|
+
if (!resource.size || progress.completed >= progress.queued) {
|
|
234
|
+
resource.value.destroy?.();
|
|
235
|
+
usedBytes -= resource.size;
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
resource.submission = progress.queued;
|
|
239
|
+
scope.retired.set(id, resource);
|
|
240
|
+
if (scope.releaseFence !== resource.submission) {
|
|
241
|
+
const submitted = scope.releaseFence = resource.submission;
|
|
242
|
+
// Invalid handles disappear immediately; allocations and object slots remain
|
|
243
|
+
// charged until the queue releases them. One fence covers a release cohort.
|
|
244
|
+
scope.device.queue.onSubmittedWorkDone().catch(() => {}).then(() => {
|
|
245
|
+
progress.completed = Math.max(progress.completed, submitted);
|
|
246
|
+
collectReleased(scope, progress.completed);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function retire(scope) {
|
|
251
|
+
if (scope.retiring) return scope.retiring;
|
|
252
|
+
scope.lost = true;
|
|
253
|
+
if (scope.surface) postMessage({ type: "status", active: false });
|
|
254
|
+
scope.retiring = (async () => {
|
|
255
|
+
try { if (scope.device) await scope.device.queue.onSubmittedWorkDone(); } catch {}
|
|
256
|
+
for (const r of scope.objects.values()) {
|
|
257
|
+
r.value.destroy?.();
|
|
258
|
+
usedBytes -= r.size;
|
|
259
|
+
}
|
|
260
|
+
scope.objects.clear();
|
|
261
|
+
collectReleased(scope, Infinity);
|
|
262
|
+
for (const t of scope.timers ?? []) { t.query.destroy(); t.resolve.destroy(); t.read.destroy(); }
|
|
263
|
+
if (slots[scope.index] === scope) slots[scope.index] = null;
|
|
264
|
+
postMessage({ type: "complete" });
|
|
265
|
+
})();
|
|
266
|
+
return scope.retiring;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function batch(scope, commands) {
|
|
270
|
+
const device = scope.device;
|
|
271
|
+
const progress = queueProgress;
|
|
272
|
+
await queueFull(scope);
|
|
273
|
+
let encoder, texture, computePass, renderPass, passWidth, passHeight, timer, queries = 0;
|
|
274
|
+
let renderPipeline, renderGroups, renderBuffers, renderIndex;
|
|
275
|
+
const getEncoder = () => encoder ??= device.createCommandEncoder();
|
|
276
|
+
const endCompute = () => { computePass?.end(); computePass = null; };
|
|
277
|
+
const endRender = () => { renderPass?.end(); renderPass = null; };
|
|
278
|
+
const timestamps = () => {
|
|
279
|
+
timer ??= scope.timers?.find(t => !t.busy);
|
|
280
|
+
if (!timer) return {};
|
|
281
|
+
const beginningOfPassWriteIndex = queries++, endOfPassWriteIndex = queries++;
|
|
282
|
+
return {timestampWrites: {querySet: timer.query, beginningOfPassWriteIndex, endOfPassWriteIndex}};
|
|
283
|
+
};
|
|
284
|
+
const started = performance.now();
|
|
285
|
+
device.pushErrorScope("validation");
|
|
286
|
+
device.pushErrorScope("out-of-memory");
|
|
287
|
+
try {
|
|
288
|
+
for (const { opcode: op, b, w } of commands) {
|
|
289
|
+
const id = b.length >= 16 && op !== A.DOLLY_GPU_VIEWPORT ? integer(w, 8) : 0;
|
|
290
|
+
if (op !== A.DOLLY_GPU_COMPUTE) endCompute();
|
|
291
|
+
if (renderPass) ensure([1,2,3,18,20,21,22,24,25,26].includes(op), "Command is invalid inside a render pass");
|
|
292
|
+
if ([1,3,4,5,6,14,16,18,20,21,22].includes(op)) ensure(id > scope.high && id <= 0xffffffff && scope.objects.size + scope.retired.size < maxObjects, "GPU object quota", E.ENOSPC);
|
|
293
|
+
if (op === A.DOLLY_GPU_CREATE_BUFFER) {
|
|
294
|
+
const size = integer(w, 16), usage = w.getUint32(24, true);
|
|
295
|
+
ensure(size > 0 && size <= maxBuffer && size <= maxBytes - usedBytes, "GPU allocation quota", E.ENOMEM);
|
|
296
|
+
ensure(w.getUint32(28, true) === 0 && usage > 0 && (usage & ~1023) === 0, "Buffer usage");
|
|
297
|
+
ensure(id > scope.high && scope.objects.size + scope.retired.size < maxObjects, "GPU resource quota", E.ENOSPC);
|
|
298
|
+
const buffer = device.createBuffer({ size, usage });
|
|
299
|
+
insert(scope, id, "buffer", buffer, size); usedBytes += size;
|
|
300
|
+
} else if (op === A.DOLLY_GPU_WRITE_BUFFER) {
|
|
301
|
+
const r = object(scope, id, "buffer"), offset = integer(w, 16), n = w.getUint32(28, true);
|
|
302
|
+
range(r, offset, n); ensure(offset % 4 === 0 && !r.mapped, "Unaligned or mapped GPU upload");
|
|
303
|
+
device.queue.writeBuffer(r.value, offset, b.subarray(w.getUint32(24, true), w.getUint32(24, true) + n));
|
|
304
|
+
progress.queued++;
|
|
305
|
+
} else if (op === A.DOLLY_GPU_CREATE_SHADER) {
|
|
306
|
+
const shader = device.createShaderModule({ code: text(b, 24, w.getUint32(16, true)) });
|
|
307
|
+
const info = await shader.getCompilationInfo();
|
|
308
|
+
const errors = info.messages.filter(m => m.type === "error");
|
|
309
|
+
ensure(!errors.length, errors.map(m => `${m.lineNum}:${m.linePos} ${m.message}`).join("\n").slice(0,2048));
|
|
310
|
+
insert(scope, id, "shader", shader);
|
|
311
|
+
} else if (op === A.DOLLY_GPU_RENDER_PIPELINE || op === A.DOLLY_GPU_VERTEX_PIPELINE) {
|
|
312
|
+
const shader = object(scope, integer(w, 16), "shader").value;
|
|
313
|
+
const topology = w.getUint32(24, true), blend = w.getUint32(28, true);
|
|
314
|
+
ensure(topology <= 1 && blend <= 2, "Unsupported render state");
|
|
315
|
+
const component = { srcFactor: "one", dstFactor: blend === 2 ? "one" : "one-minus-src-alpha", operation: "add" };
|
|
316
|
+
const vs = w.getUint32(32, true), fs = w.getUint32(36, true);
|
|
317
|
+
let at = 40, buffers = [];
|
|
318
|
+
if (op === A.DOLLY_GPU_VERTEX_PIPELINE) {
|
|
319
|
+
const count = w.getUint32(44, true), attributes = [];
|
|
320
|
+
for (let i = 0; i < count; i++) {
|
|
321
|
+
const o = 48 + i * 16;
|
|
322
|
+
attributes.push({shaderLocation:w.getUint32(o,true),format:`float32x${w.getUint32(o+4,true)}`,offset:w.getUint32(o+8,true)});
|
|
323
|
+
}
|
|
324
|
+
buffers = [{arrayStride:w.getUint32(40,true),stepMode:"vertex",attributes}];
|
|
325
|
+
at = 48 + count * 16;
|
|
326
|
+
}
|
|
327
|
+
const pipeline = await device.createRenderPipelineAsync({ layout: "auto",
|
|
328
|
+
vertex: { module: shader, entryPoint: text(b,at,vs), buffers },
|
|
329
|
+
fragment: { module: shader, entryPoint: text(b,at+vs,fs), targets: [{ format, ...(blend ? { blend: { color: component, alpha: component } } : {}) }] },
|
|
330
|
+
primitive: { topology: topology ? "triangle-strip" : "triangle-list" } });
|
|
331
|
+
insert(scope, id, "render", pipeline);
|
|
332
|
+
} else if (op === A.DOLLY_GPU_COMPUTE_PIPELINE || op === A.DOLLY_GPU_COMPUTE_CONSTANTS) {
|
|
333
|
+
const constants=Object.create(null);
|
|
334
|
+
if(op===A.DOLLY_GPU_COMPUTE_CONSTANTS) {
|
|
335
|
+
let at=(32+w.getUint32(24,true)+7)&~7;
|
|
336
|
+
for(let i=0;i<w.getUint32(28,true);i++) {
|
|
337
|
+
const n=w.getUint32(at,true);constants[text(b,at+16,n)]=w.getFloat64(at+8,true);at=(at+16+n+7)&~7;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const pipeline = await device.createComputePipelineAsync({ layout: "auto", compute: {
|
|
341
|
+
module: object(scope,integer(w,16),"shader").value, entryPoint: text(b,32,w.getUint32(24,true)), constants } });
|
|
342
|
+
insert(scope,id,"compute",pipeline);
|
|
343
|
+
} else if (op === A.DOLLY_GPU_BIND_GROUP) {
|
|
344
|
+
const pipeline = object(scope,integer(w,16));
|
|
345
|
+
ensure(["render","compute"].includes(pipeline.kind), "Invalid binding pipeline");
|
|
346
|
+
const entries = [];
|
|
347
|
+
for (let i=0; i<w.getUint32(24,true); ++i) {
|
|
348
|
+
const at=32+i*24, r=object(scope,integer(w,at),"buffer"), offset=integer(w,at+8), size=integer(w,at+16);
|
|
349
|
+
range(r,offset,size); entries.push({binding:i,resource:{buffer:r.value,offset,size}});
|
|
350
|
+
}
|
|
351
|
+
insert(scope,id,"group",device.createBindGroup({layout:pipeline.value.getBindGroupLayout(0),entries}));
|
|
352
|
+
} else if (op === A.DOLLY_GPU_RENDER || op === A.DOLLY_GPU_RENDER_VERTEX) {
|
|
353
|
+
const width=w.getUint32(32,true), height=w.getUint32(36,true);
|
|
354
|
+
ensure(scope.surface && width>0 && height>0 && width<=4096 && height<=2304, "Invalid GPU surface dimensions");
|
|
355
|
+
ensure(w.getUint32(60,true)===0 && w.getUint32(56,true)<=1, "Render reserved field");
|
|
356
|
+
const vertices=w.getUint32(24,true), instances=w.getUint32(28,true);
|
|
357
|
+
ensure(vertices>0 && instances>0 && vertices*instances<=4*1024*1024, "Draw limit",E.E2BIG);
|
|
358
|
+
if (!texture) {
|
|
359
|
+
if(canvas.width!==width || canvas.height!==height) { canvas.width=width; canvas.height=height; }
|
|
360
|
+
texture=context.getCurrentTexture();
|
|
361
|
+
} else ensure(canvas.width===width && canvas.height===height,"Surface resized within batch");
|
|
362
|
+
const clearValue=[40,44,48,52].map(o=>w.getFloat32(o,true));
|
|
363
|
+
ensure(clearValue.every(Number.isFinite), "Invalid clear color");
|
|
364
|
+
const pass=getEncoder().beginRenderPass({...timestamps(),colorAttachments:[{view:texture.createView(),clearValue,
|
|
365
|
+
loadOp:w.getUint32(56,true)?"clear":"load",storeOp:"store"}]});
|
|
366
|
+
pass.setPipeline(object(scope,id,"render").value);
|
|
367
|
+
const group=integer(w,16); if(group)pass.setBindGroup(0,object(scope,group,"group").value);
|
|
368
|
+
if(op===A.DOLLY_GPU_RENDER_VERTEX) {
|
|
369
|
+
const r=object(scope,integer(w,64),"buffer"), offset=integer(w,72), size=integer(w,80);
|
|
370
|
+
range(r,offset,size);pass.setVertexBuffer(0,r.value,offset,size);
|
|
371
|
+
}
|
|
372
|
+
pass.draw(vertices,instances); pass.end();
|
|
373
|
+
} else if (op === A.DOLLY_GPU_CREATE_TEXTURE) {
|
|
374
|
+
const width=w.getUint32(16,true), height=w.getUint32(20,true), layers=w.getUint32(24,true),
|
|
375
|
+
mips=w.getUint32(28,true), type=w.getUint32(32,true), usage=w.getUint32(36,true);
|
|
376
|
+
ensure(width>0 && height>0 && width<=Math.min(8192,device.limits.maxTextureDimension2D) &&
|
|
377
|
+
height<=Math.min(8192,device.limits.maxTextureDimension2D) && (layers===1 || (layers===6 && width===height)) &&
|
|
378
|
+
mips>0 && mips<=1+Math.floor(Math.log2(Math.max(width,height))) &&
|
|
379
|
+
type>0 && type<textureFormats.length && usage>0 && (usage&~23)===0 && w.getBigUint64(40,true)===0n, "Texture descriptor");
|
|
380
|
+
let size=0;
|
|
381
|
+
const block=type>=7?4:1;
|
|
382
|
+
if(block===4) {
|
|
383
|
+
ensure(device.features.has("texture-compression-bc"), "BC texture compression unavailable", E.ENOSYS);
|
|
384
|
+
ensure(width%block===0 && height%block===0 && !(usage&16), "BC texture descriptor");
|
|
385
|
+
}
|
|
386
|
+
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];
|
|
387
|
+
ensure(size<=bufferCeiling && size<=maxBytes-usedBytes, "Texture allocation quota", E.ENOMEM);
|
|
388
|
+
const value=device.createTexture({size:{width,height,depthOrArrayLayers:layers},mipLevelCount:mips,
|
|
389
|
+
format:textureFormats[type],usage});
|
|
390
|
+
insert(scope,id,"texture",value,size);usedBytes+=size;
|
|
391
|
+
Object.assign(object(scope,id),{width,height,layers,mips,type});
|
|
392
|
+
} else if (op === A.DOLLY_GPU_WRITE_TEXTURE) {
|
|
393
|
+
const r=object(scope,id,"texture"), mip=w.getUint32(16,true), layer=w.getUint32(20,true),
|
|
394
|
+
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);
|
|
395
|
+
const block=r.type>=7?4:1;
|
|
396
|
+
ensure(r.type!==5 && r.type!==6 && mip<r.mips && layer<r.layers && width>0 && height>0 &&
|
|
397
|
+
x%block===0 && y%block===0 && width%block===0 && height%block===0 &&
|
|
398
|
+
x<=Math.ceil(Math.max(1,r.width>>mip)/block)*block-width &&
|
|
399
|
+
y<=Math.ceil(Math.max(1,r.height>>mip)/block)*block-height &&
|
|
400
|
+
n===(width/block)*(height/block)*blockBytes[r.type], "Texture upload range");
|
|
401
|
+
device.queue.writeTexture({texture:r.value,mipLevel:mip,origin:{x,y,z:layer}},b.subarray(48,48+n),
|
|
402
|
+
{bytesPerRow:width/block*blockBytes[r.type],rowsPerImage:height/block},{width,height});
|
|
403
|
+
progress.queued++;
|
|
404
|
+
} else if (op === A.DOLLY_GPU_CREATE_SAMPLER) {
|
|
405
|
+
const filters=[16,20,24].map(at=>w.getUint32(at,true)), address=[28,32,36].map(at=>w.getUint32(at,true)),
|
|
406
|
+
compare=w.getUint32(40,true), maxAnisotropy=w.getUint32(44,true);
|
|
407
|
+
ensure(filters.every(n=>n<=1) && address.every(n=>n<=2) && compare<=8 && maxAnisotropy>=1 &&
|
|
408
|
+
maxAnisotropy<=16 && (maxAnisotropy===1 || filters.every(n=>n===1)), "Sampler descriptor");
|
|
409
|
+
const addresses=["clamp-to-edge","repeat","mirror-repeat"], modes=["nearest","linear"];
|
|
410
|
+
insert(scope,id,"sampler",device.createSampler({minFilter:modes[filters[0]],magFilter:modes[filters[1]],
|
|
411
|
+
mipmapFilter:modes[filters[2]],addressModeU:addresses[address[0]],addressModeV:addresses[address[1]],
|
|
412
|
+
addressModeW:addresses[address[2]],compare:compareOps[compare],maxAnisotropy}));
|
|
413
|
+
} else if (op === A.DOLLY_GPU_GRAPHICS_PIPELINE) {
|
|
414
|
+
const color=w.getUint32(32,true), depth=w.getUint32(36,true), topology=w.getUint32(40,true),
|
|
415
|
+
cull=w.getUint32(44,true), front=w.getUint32(48,true), compare=w.getUint32(52,true), write=w.getUint32(56,true),
|
|
416
|
+
slope=w.getFloat32(64,true), clamp=w.getFloat32(68,true), mask=w.getUint32(72,true), blend=w.getUint32(76,true);
|
|
417
|
+
ensure(color<=4 && (depth===0 || depth===5 || depth===6) && topology<=3 && cull<=2 && front<=1 &&
|
|
418
|
+
compare>=1 && compare<=8 && write<=1 && Number.isFinite(slope) && Number.isFinite(clamp) && mask<=15 && blend<=1,
|
|
419
|
+
"Graphics pipeline state");
|
|
420
|
+
const component=at=>{
|
|
421
|
+
const op=w.getUint32(at,true), src=w.getUint32(at+4,true), dst=w.getUint32(at+8,true);
|
|
422
|
+
ensure(op<blendOps.length && src<blendFactors.length && dst<blendFactors.length,"Blend state");
|
|
423
|
+
return {operation:blendOps[op],srcFactor:blendFactors[src],dstFactor:blendFactors[dst]};
|
|
424
|
+
};
|
|
425
|
+
const colorBlend=component(80), alphaBlend=component(92), count=w.getUint32(104,true), buffers=[];
|
|
426
|
+
for(let i=0;i<count;i++)buffers.push({arrayStride:w.getUint32(112+i*16,true),
|
|
427
|
+
stepMode:w.getUint32(116+i*16,true)?"instance":"vertex",attributes:[]});
|
|
428
|
+
for(let i=0;i<w.getUint32(108,true);i++) {
|
|
429
|
+
const at=112+(count+i)*16;
|
|
430
|
+
buffers[w.getUint32(at,true)].attributes.push({shaderLocation:w.getUint32(at+4,true),
|
|
431
|
+
format:vertexFormats[w.getUint32(at+8,true)],offset:w.getUint32(at+12,true)});
|
|
432
|
+
}
|
|
433
|
+
const value=await device.createRenderPipelineAsync({layout:"auto",
|
|
434
|
+
vertex:{module:object(scope,integer(w,16),"shader").value,entryPoint:"main",buffers},
|
|
435
|
+
fragment:{module:object(scope,integer(w,24),"shader").value,entryPoint:"main",
|
|
436
|
+
targets:[{format:color?textureFormats[color]:format,writeMask:mask,...(blend?{blend:{color:colorBlend,alpha:alphaBlend}}:{})}]},
|
|
437
|
+
primitive:{topology:["triangle-list","triangle-strip","line-list","point-list"][topology],
|
|
438
|
+
...(topology===1?{stripIndexFormat:"uint16"}:{}),cullMode:["none","front","back"][cull],frontFace:front?"cw":"ccw"},
|
|
439
|
+
...(depth?{depthStencil:{format:textureFormats[depth],depthWriteEnabled:!!write,depthCompare:compareOps[compare],
|
|
440
|
+
depthBias:w.getInt32(60,true),depthBiasSlopeScale:slope,depthBiasClamp:clamp}}:{})});
|
|
441
|
+
insert(scope,id,"graphics",value);
|
|
442
|
+
} else if (op === A.DOLLY_GPU_RESOURCE_GROUP) {
|
|
443
|
+
const pipeline=object(scope,integer(w,16),"graphics"), index=w.getUint32(24,true), entries=[];
|
|
444
|
+
for(let i=0;i<w.getUint32(28,true);i++) {
|
|
445
|
+
const at=32+i*32, binding=w.getUint32(at,true), kind=w.getUint32(at+4,true),
|
|
446
|
+
r=object(scope,integer(w,at+8),["buffer","texture","texture","sampler"][kind]);
|
|
447
|
+
let resource;
|
|
448
|
+
if(kind===0) {
|
|
449
|
+
const offset=integer(w,at+16), size=integer(w,at+24);range(r,offset,size);
|
|
450
|
+
ensure(size>0 && !r.mapped,"Invalid bound buffer");resource={buffer:r.value,offset,size};
|
|
451
|
+
} else if(kind===3)resource=r.value;
|
|
452
|
+
else {
|
|
453
|
+
ensure(kind!==2 || r.layers===6,"Cube binding requires six layers");
|
|
454
|
+
resource=r.value.createView({dimension:kind===2?"cube":"2d",arrayLayerCount:kind===2?6:1,
|
|
455
|
+
aspect:r.type===5?"depth-only":"all"});
|
|
456
|
+
}
|
|
457
|
+
entries.push({binding,resource});
|
|
458
|
+
}
|
|
459
|
+
insert(scope,id,"resource-group",device.createBindGroup({layout:pipeline.value.getBindGroupLayout(index),entries}));
|
|
460
|
+
object(scope,id).index=index;
|
|
461
|
+
} else if (op === A.DOLLY_GPU_BEGIN_RENDER_PASS) {
|
|
462
|
+
const depth=integer(w,16), width=w.getUint32(24,true), height=w.getUint32(28,true),
|
|
463
|
+
colorClear=w.getUint32(32,true), depthClear=w.getUint32(36,true), clearValue=[40,44,48,52].map(at=>w.getFloat32(at,true)),
|
|
464
|
+
depthClearValue=w.getFloat32(56,true), stencilClearValue=w.getUint32(60,true);
|
|
465
|
+
ensure(width>0 && height>0 && colorClear<=1 && depthClear<=1 && clearValue.every(Number.isFinite) &&
|
|
466
|
+
depthClearValue>=0 && depthClearValue<=1 && stencilClearValue<=255,"Render pass descriptor");
|
|
467
|
+
let colorTexture;
|
|
468
|
+
if(id) {
|
|
469
|
+
const r=object(scope,id,"texture");
|
|
470
|
+
ensure(r.width===width && r.height===height && r.type<=4 && r.layers===1,"Color attachment dimensions/format");
|
|
471
|
+
colorTexture=r.value;
|
|
472
|
+
} else {
|
|
473
|
+
ensure(scope.surface && width<=4096 && height<=2304,"Surface attachment dimensions/ownership");
|
|
474
|
+
if(!texture) {
|
|
475
|
+
if(canvas.width!==width || canvas.height!==height){canvas.width=width;canvas.height=height;}
|
|
476
|
+
texture=context.getCurrentTexture();
|
|
477
|
+
} else ensure(canvas.width===width && canvas.height===height,"Surface resized within batch");
|
|
478
|
+
colorTexture=texture;
|
|
479
|
+
}
|
|
480
|
+
let depthStencilAttachment;
|
|
481
|
+
if(depth) {
|
|
482
|
+
const r=object(scope,depth,"texture");
|
|
483
|
+
ensure(r.width===width && r.height===height && r.type>=5 && r.layers===1,"Depth attachment dimensions/format");
|
|
484
|
+
depthStencilAttachment={view:r.value.createView({mipLevelCount:1}),depthLoadOp:depthClear?"clear":"load",depthStoreOp:"store",depthClearValue,
|
|
485
|
+
...(r.type===5?{stencilLoadOp:depthClear?"clear":"load",stencilStoreOp:"store",stencilClearValue}:{})};
|
|
486
|
+
}
|
|
487
|
+
renderPass=getEncoder().beginRenderPass({...timestamps(),colorAttachments:[{view:colorTexture.createView({mipLevelCount:1}),
|
|
488
|
+
loadOp:colorClear?"clear":"load",storeOp:"store",clearValue}],depthStencilAttachment});
|
|
489
|
+
passWidth=width;passHeight=height;
|
|
490
|
+
renderPipeline=null;renderGroups=[];renderBuffers=[];renderIndex=null;
|
|
491
|
+
} else if (op === A.DOLLY_GPU_END_RENDER_PASS) {
|
|
492
|
+
ensure(renderPass,"No active render pass");endRender();
|
|
493
|
+
} else if (op === A.DOLLY_GPU_VIEWPORT) {
|
|
494
|
+
ensure(renderPass,"Viewport outside render pass");
|
|
495
|
+
const [x,y,width,height,min,max]=[8,12,16,20,24,28].map(at=>w.getFloat32(at,true)),
|
|
496
|
+
scissor=[32,36,40,44].map(at=>w.getUint32(at,true));
|
|
497
|
+
ensure([x,y,width,height,min,max].every(Number.isFinite) && x>=0 && y>=0 && width>0 && height>0 &&
|
|
498
|
+
x<=passWidth-width && y<=passHeight-height && min>=0 && max<=1 && min<=max &&
|
|
499
|
+
scissor[0]<=passWidth-scissor[2] && scissor[1]<=passHeight-scissor[3],"Viewport/scissor bounds");
|
|
500
|
+
renderPass.setViewport(x,y,width,height,min,max);renderPass.setScissorRect(...scissor);
|
|
501
|
+
} else if (op === A.DOLLY_GPU_DRAW_MESH) {
|
|
502
|
+
ensure(renderPass,"Draw outside render pass");
|
|
503
|
+
const count=w.getUint32(40,true), instances=w.getUint32(44,true), first=w.getUint32(48,true),
|
|
504
|
+
base=w.getInt32(52,true), firstInstance=w.getUint32(56,true), indexType=w.getUint32(60,true),
|
|
505
|
+
groupCount=w.getUint32(64,true), bufferCount=w.getUint32(68,true);
|
|
506
|
+
ensure(count>0 && instances>0 && count*instances<=4*1024*1024 && first+count<=0xffffffff &&
|
|
507
|
+
firstInstance+instances<=0xffffffff && indexType<=2,"Mesh draw limit",E.E2BIG);
|
|
508
|
+
const pipeline=object(scope,id,"graphics").value;
|
|
509
|
+
if(renderPipeline!==pipeline){renderPass.setPipeline(pipeline);renderPipeline=pipeline;}
|
|
510
|
+
const groups=new Set();
|
|
511
|
+
for(let i=0;i<groupCount;i++) {
|
|
512
|
+
const r=object(scope,integer(w,80+i*8),"resource-group");
|
|
513
|
+
ensure(!groups.has(r.index),"Duplicate mesh group");groups.add(r.index);
|
|
514
|
+
if(renderGroups[r.index]!==r.value){renderPass.setBindGroup(r.index,r.value);renderGroups[r.index]=r.value;}
|
|
515
|
+
}
|
|
516
|
+
for(let i=0;i<bufferCount;i++) {
|
|
517
|
+
const at=80+8*groupCount+24*i, r=object(scope,integer(w,at),"buffer"), offset=integer(w,at+8), size=integer(w,at+16);
|
|
518
|
+
range(r,offset,size);ensure(!r.mapped && size>0,"Invalid vertex buffer");
|
|
519
|
+
const slot=i*3;
|
|
520
|
+
if(renderBuffers[slot]!==r.value || renderBuffers[slot+1]!==offset || renderBuffers[slot+2]!==size){
|
|
521
|
+
renderPass.setVertexBuffer(i,r.value,offset,size);
|
|
522
|
+
renderBuffers[slot]=r.value;renderBuffers[slot+1]=offset;renderBuffers[slot+2]=size;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if(indexType) {
|
|
526
|
+
const r=object(scope,integer(w,16),"buffer"), offset=integer(w,24), size=integer(w,32), stride=indexType===1?2:4;
|
|
527
|
+
range(r,offset,size);ensure(!r.mapped && (first+count)*stride<=size,"Index buffer range");
|
|
528
|
+
if(!renderIndex || renderIndex[0]!==r.value || renderIndex[1]!==indexType || renderIndex[2]!==offset || renderIndex[3]!==size){
|
|
529
|
+
renderPass.setIndexBuffer(r.value,indexType===1?"uint16":"uint32",offset,size);
|
|
530
|
+
renderIndex=[r.value,indexType,offset,size];
|
|
531
|
+
}
|
|
532
|
+
renderPass.drawIndexed(count,instances,first,base,firstInstance);
|
|
533
|
+
} else {
|
|
534
|
+
ensure(w.getBigUint64(16,true)===0n && w.getBigUint64(24,true)===0n && w.getBigUint64(32,true)===0n && base===0,
|
|
535
|
+
"Unexpected nonindexed draw arguments");
|
|
536
|
+
renderPass.draw(count,instances,first,firstInstance);
|
|
537
|
+
}
|
|
538
|
+
} else if (op === A.DOLLY_GPU_COMPUTE) {
|
|
539
|
+
const xyz=[24,28,32].map(o=>w.getUint32(o,true));
|
|
540
|
+
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);
|
|
541
|
+
computePass ??= getEncoder().beginComputePass(timestamps());
|
|
542
|
+
computePass.setPipeline(object(scope,id,"compute").value);
|
|
543
|
+
computePass.setBindGroup(0,object(scope,integer(w,16),"group").value); computePass.dispatchWorkgroups(...xyz); stats.dispatches++;
|
|
544
|
+
} else if (op === A.DOLLY_GPU_COPY_BUFFER) {
|
|
545
|
+
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);
|
|
546
|
+
range(src,so,n);range(dst,to,n);getEncoder().copyBufferToBuffer(src.value,so,dst.value,to,n);
|
|
547
|
+
} else if (op === A.DOLLY_GPU_CAPTURE_FRAME) {
|
|
548
|
+
ensure(scope.surface && texture, "Capture requires a rendered surface in this batch");
|
|
549
|
+
const x=w.getUint32(16,true), y=w.getUint32(20,true), width=w.getUint32(24,true), height=w.getUint32(28,true);
|
|
550
|
+
ensure(width>0 && height>0 && x<=texture.width-width && y<=texture.height-height, "Capture rectangle outside surface");
|
|
551
|
+
const dst=object(scope,id,"buffer"), bytesPerRow=Math.ceil(width*4/256)*256;
|
|
552
|
+
range(dst,0,bytesPerRow*(height-1)+width*4);
|
|
553
|
+
ensure(!dst.mapped && (dst.value.usage & GPUBufferUsage.COPY_DST), "Capture destination is mapped or not writable");
|
|
554
|
+
getEncoder().copyTextureToBuffer({texture,origin:{x,y}}, {buffer:dst.value,bytesPerRow}, {width,height});
|
|
555
|
+
} else if (op === A.DOLLY_GPU_SUBMIT) {
|
|
556
|
+
ensure(encoder,"No GPU commands to submit");
|
|
557
|
+
await queueFull(scope);
|
|
558
|
+
const measured = queries ? timer : null, count = queries;
|
|
559
|
+
if (measured) {
|
|
560
|
+
encoder.resolveQuerySet(measured.query,0,count,measured.resolve,0);
|
|
561
|
+
encoder.copyBufferToBuffer(measured.resolve,0,measured.read,0,count*8);
|
|
562
|
+
measured.busy = true;
|
|
563
|
+
}
|
|
564
|
+
device.queue.submit([encoder.finish()]);encoder=null;
|
|
565
|
+
const submittedWork = ++progress.queued;
|
|
566
|
+
if (measured) measured.read.mapAsync(GPUMapMode.READ,0,count*8).then(() => {
|
|
567
|
+
const values=new BigUint64Array(measured.read.getMappedRange(0,count*8));
|
|
568
|
+
let ns=0n;for(let i=0;i<count;i+=2)if(values[i+1]>=values[i])ns+=values[i+1]-values[i];
|
|
569
|
+
if (!scope.lost) {scope.gpuMs=Number(ns)/1e6;scope.gpuTotalMs+=scope.gpuMs;scope.gpuSamples++;}
|
|
570
|
+
measured.read.unmap();
|
|
571
|
+
}).catch(()=>{}).finally(()=>{measured.busy=false;});
|
|
572
|
+
timer=null;queries=0;
|
|
573
|
+
const completed=device.queue.onSubmittedWorkDone().then(()=>{
|
|
574
|
+
progress.completed=Math.max(progress.completed,submittedWork);
|
|
575
|
+
}).finally(()=>{
|
|
576
|
+
scope.submissions.splice(scope.submissions.indexOf(completed),1);
|
|
577
|
+
if(scope.frames[0]===completed)scope.frames.shift();
|
|
578
|
+
});
|
|
579
|
+
scope.submissions.push(completed);
|
|
580
|
+
if(texture)scope.frames.push(completed);
|
|
581
|
+
completed.catch(()=>{});
|
|
582
|
+
if(texture){stats.frames++;postMessage({type:"status",active:true,width:canvas.width,height:canvas.height,stats:{...stats,allocatedBytes:usedBytes}});texture=null;}
|
|
583
|
+
} else if (op === A.DOLLY_GPU_MAP_READ) {
|
|
584
|
+
ensure(!encoder,"Submit before mapping");
|
|
585
|
+
const r=object(scope,id,"buffer"), offset=integer(w,16), size=integer(w,24);range(r,offset,size);
|
|
586
|
+
ensure(!r.mapped && size<=16*1024*1024,"Mapping limit",E.E2BIG);
|
|
587
|
+
await r.value.mapAsync(GPUMapMode.READ,offset,size);r.mapped=r.value.getMappedRange(offset,size);
|
|
588
|
+
} else if (op === A.DOLLY_GPU_UNMAP) {
|
|
589
|
+
const r=object(scope,id,"buffer");r.value.unmap();r.mapped=null;
|
|
590
|
+
} else if (op === A.DOLLY_GPU_RELEASE) {
|
|
591
|
+
ensure(!encoder,"Submit before releasing GPU resources");
|
|
592
|
+
release(scope, id, progress);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
ensure(!encoder,"GPU batch has an unsubmitted encoder");
|
|
596
|
+
} finally {
|
|
597
|
+
endCompute();endRender();
|
|
598
|
+
const [oom, validation] = await Promise.all([device.popErrorScope(), device.popErrorScope()]);
|
|
599
|
+
stats.batchWallMilliseconds += performance.now()-started;
|
|
600
|
+
if(oom)fail(E.ENOMEM,oom.message);if(validation)fail(E.EINVAL,validation.message);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function publish(request, error, data = new Uint8Array()) {
|
|
605
|
+
const base=mailbox+request.index*(64+A.DOLLY_GPU_REPLY_BYTES), words=new Int32Array(memory,base,16);
|
|
606
|
+
new Uint8Array(memory,base+64,data.length).set(data);
|
|
607
|
+
Atomics.store(words,1,request.scope);Atomics.store(words,2,request.sequence);
|
|
608
|
+
Atomics.store(words,3,error);Atomics.store(words,4,data.length);Atomics.store(words,0,1);
|
|
609
|
+
postMessage({type:"complete"});
|
|
610
|
+
}
|
|
611
|
+
async function execute(request, scope, parsed) {
|
|
612
|
+
try {
|
|
613
|
+
let output=new Uint8Array();
|
|
614
|
+
if(request.op===A.DOLLY_GPU_OPEN) {
|
|
615
|
+
scope.device=await getDevice();
|
|
616
|
+
ensure(!scope.lost,"GPU scope cancelled",E.ECANCELED);
|
|
617
|
+
const width=request.v.getUint32(32,true),height=request.v.getUint32(36,true);
|
|
618
|
+
ensure((width===0&&height===0)||(width>0&&height>0&&width<=4096&&height<=2304),"Invalid surface size");
|
|
619
|
+
ensure(!width || !slots.some(s=>s && s!==scope && s.surface),"GPU surface is busy",E.EBUSY);
|
|
620
|
+
ensure(!width || context, "GPU presentation requires the display provider", E.ENOSYS);
|
|
621
|
+
scope.surface=width>0;
|
|
622
|
+
scope.gpuMs=0;scope.gpuTotalMs=0;scope.gpuSamples=0;
|
|
623
|
+
if(device.features.has("timestamp-query")) scope.timers=Array.from({length:3},()=>({
|
|
624
|
+
query:device.createQuerySet({type:"timestamp",count:timestampQueries}),
|
|
625
|
+
resolve:device.createBuffer({size:timestampQueries*8,usage:GPUBufferUsage.QUERY_RESOLVE|GPUBufferUsage.COPY_SRC}),
|
|
626
|
+
read:device.createBuffer({size:timestampQueries*8,usage:GPUBufferUsage.MAP_READ|GPUBufferUsage.COPY_DST}),busy:false}));
|
|
627
|
+
// A fixed name: the adapter's vendor/description would fingerprint the device.
|
|
628
|
+
const name=encode.encode("WebGPU");
|
|
629
|
+
output=new Uint8Array(16+name.length);
|
|
630
|
+
const v=new DataView(output.buffer);
|
|
631
|
+
v.setBigUint64(0,BigInt(scope.id),true);v.setUint32(8,width,true);v.setUint32(12,height,true);
|
|
632
|
+
output.set(name,16);
|
|
633
|
+
} else {
|
|
634
|
+
ensure(!scope.lost && device,"GPU scope lost",E.EIO);
|
|
635
|
+
if(request.op===A.DOLLY_GPU_BATCH)await batch(scope,parsed);
|
|
636
|
+
else if(request.op===A.DOLLY_GPU_WAIT) {
|
|
637
|
+
const submitted=queueProgress.queued;
|
|
638
|
+
await device.queue.onSubmittedWorkDone();
|
|
639
|
+
queueProgress.completed=Math.max(queueProgress.completed,submitted);
|
|
640
|
+
collectReleased(scope,queueProgress.completed);
|
|
641
|
+
}
|
|
642
|
+
else if(request.op===A.DOLLY_GPU_READ) {
|
|
643
|
+
const r=object(scope,integer(request.v,32),"buffer"),offset=integer(request.v,40),size=integer(request.v,48);
|
|
644
|
+
ensure(r.mapped && size<=A.DOLLY_GPU_REPLY_BYTES && offset<=r.mapped.byteLength-size,"Invalid readback");
|
|
645
|
+
output=new Uint8Array(r.mapped,offset,size).slice();stats.readbackBytes+=size;
|
|
646
|
+
} else if(request.op===A.DOLLY_GPU_CLOSE)await retire(scope);
|
|
647
|
+
else if(request.op===A.DOLLY_GPU_CAPABILITIES)output=capabilities;
|
|
648
|
+
else if(request.op===A.DOLLY_GPU_INFO) {
|
|
649
|
+
output=new Uint8Array(80);const v=new DataView(output.buffer);
|
|
650
|
+
v.setUint32(0,scope.timers?1:0,true);v.setUint32(4,A.DOLLY_GPU_MAX_BINDINGS,true);
|
|
651
|
+
v.setBigUint64(8,BigInt(maxBuffer),true);v.setBigUint64(16,BigInt(maxBytes),true);
|
|
652
|
+
v.setFloat64(24,scope.gpuMs,true);v.setFloat64(32,scope.gpuTotalMs,true);v.setBigUint64(40,BigInt(scope.gpuSamples),true);
|
|
653
|
+
v.setFloat64(48,stats.batchWallMilliseconds,true);v.setBigUint64(56,BigInt(stats.frames),true);
|
|
654
|
+
v.setBigUint64(64,BigInt(stats.dispatches),true);v.setBigUint64(72,BigInt(usedBytes),true);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
publish(request,0,output);
|
|
658
|
+
} catch(error) {
|
|
659
|
+
postMessage({type:"status",error:String(error.message??error).slice(0,2048)});
|
|
660
|
+
if(request.op===A.DOLLY_GPU_OPEN)await retire(scope);
|
|
661
|
+
publish(request,error.errno??E.EIO);
|
|
662
|
+
} finally {scope.busy=false;}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
self.onmessage = event => {
|
|
666
|
+
const m=event.data;
|
|
667
|
+
if(m.type==="configure") {
|
|
668
|
+
heap=m.memory;memory=heap.buffer;mailbox=m.mailbox;canvas=m.canvas;control=new Int32Array(m.control);
|
|
669
|
+
ensure(memory instanceof SharedArrayBuffer && Number.isSafeInteger(mailbox) && mailbox>0 && mailbox%64===0 &&
|
|
670
|
+
mailbox<=memory.byteLength-slots.length*(64+A.DOLLY_GPU_REPLY_BYTES),"Invalid GPU mailbox");
|
|
671
|
+
postMessage({type:"ready"});
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if(m.type!=="request")return;
|
|
675
|
+
memory=heap.buffer;
|
|
676
|
+
let status=0;
|
|
677
|
+
try {
|
|
678
|
+
const address=Number(m.address),size=Number(m.bytes);
|
|
679
|
+
if(address===0) {
|
|
680
|
+
ensure(Number.isSafeInteger(size)&&size>0&&size<=0xffffffff,"Invalid scope cancellation");
|
|
681
|
+
const scope=slots[(size-1)%slots.length];
|
|
682
|
+
if(scope?.id===size) {
|
|
683
|
+
scope.lost=true;
|
|
684
|
+
serial=serial.then(()=>retire(scope));
|
|
685
|
+
}
|
|
686
|
+
} else {
|
|
687
|
+
ensure(Number.isSafeInteger(address)&&Number.isSafeInteger(size)&&address>0&&size>=32&&size<=A.DOLLY_GPU_PACKET_BYTES&&address<=memory.byteLength-size,"Invalid GPU request span",E.EFAULT);
|
|
688
|
+
const request=header(new Uint8Array(memory,address,size).slice());
|
|
689
|
+
ensure(request.op>=1&&request.op<=7,"Unknown GPU operation",E.ENOTSUP);
|
|
690
|
+
ensure((request.op!==1||size===40)&&(![3,5,6,7].includes(request.op)||size===32)&&(request.op!==4||size===56),"GPU operation layout");
|
|
691
|
+
const parsed=request.op===2?records(request):null;
|
|
692
|
+
let scope=slots[request.index];
|
|
693
|
+
if(request.op===1) {
|
|
694
|
+
ensure(!scope&&request.scope>generations[request.index],"GPU scope slot is busy",E.EBUSY);
|
|
695
|
+
scope={id:request.scope,index:request.index,sequence:0,objects:new Map(),retired:new Map(),high:0,submissions:[],frames:[],lost:false,surface:false};
|
|
696
|
+
generations[request.index]=scope.id;slots[request.index]=scope;
|
|
697
|
+
}
|
|
698
|
+
ensure(scope?.id===request.scope,"Stale GPU scope",E.ESTALE);
|
|
699
|
+
ensure(!scope.busy&&request.sequence>scope.sequence,"GPU request is busy or repeated",E.EBUSY);
|
|
700
|
+
scope.sequence=request.sequence;scope.busy=true;stats.packets++;stats.packetBytes+=size;
|
|
701
|
+
serial=serial.then(()=>execute(request,scope,parsed)).catch(error=>{publish(request,E.EIO);scope.busy=false;postMessage({type:"status",error:String(error).slice(0,2048)});});
|
|
702
|
+
}
|
|
703
|
+
} catch(error) {status=-(error.errno??E.EINVAL);}
|
|
704
|
+
finally {Atomics.store(control,1,status);Atomics.store(control,0,0);Atomics.notify(control,0);}
|
|
705
|
+
};
|