@simulatte/webgpu 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -12
- package/LICENSE +191 -0
- package/README.md +55 -41
- package/api-contract.md +67 -49
- package/architecture.md +317 -0
- package/assets/package-layers.svg +3 -3
- package/docs/doe-api-reference.html +1842 -0
- package/doe-api-design.md +237 -0
- package/examples/doe-api/README.md +19 -0
- package/examples/doe-api/buffers-readback.js +3 -2
- package/examples/{doe-routines/compute-once-like-input.js → doe-api/compute-one-shot-like-input.js} +1 -1
- package/examples/{doe-routines/compute-once-matmul.js → doe-api/compute-one-shot-matmul.js} +2 -2
- package/examples/{doe-routines/compute-once-multiple-inputs.js → doe-api/compute-one-shot-multiple-inputs.js} +1 -1
- package/examples/{doe-routines/compute-once.js → doe-api/compute-one-shot.js} +1 -1
- package/examples/doe-api/{compile-and-dispatch.js → kernel-create-and-dispatch.js} +4 -6
- package/examples/doe-api/{compute-dispatch.js → kernel-run.js} +4 -6
- package/headless-webgpu-comparison.md +3 -3
- package/jsdoc-style-guide.md +435 -0
- package/native/doe_napi.c +1481 -84
- package/package.json +18 -6
- package/prebuilds/darwin-arm64/doe_napi.node +0 -0
- package/prebuilds/darwin-arm64/libwebgpu_doe.dylib +0 -0
- package/prebuilds/darwin-arm64/metadata.json +5 -5
- package/prebuilds/linux-x64/metadata.json +1 -1
- package/scripts/generate-doe-api-docs.js +1607 -0
- package/scripts/generate-readme-assets.js +3 -3
- package/src/build_metadata.js +7 -4
- package/src/bun-ffi.js +1229 -474
- package/src/bun.js +5 -1
- package/src/compute.d.ts +16 -7
- package/src/compute.js +84 -53
- package/src/full.d.ts +16 -7
- package/src/full.js +12 -10
- package/src/index.js +679 -1324
- package/src/runtime_cli.js +17 -17
- package/src/shared/capabilities.js +144 -0
- package/src/shared/compiler-errors.js +78 -0
- package/src/shared/encoder-surface.js +295 -0
- package/src/shared/full-surface.js +514 -0
- package/src/shared/public-surface.js +82 -0
- package/src/shared/resource-lifecycle.js +120 -0
- package/src/shared/validation.js +495 -0
- package/src/webgpu_constants.js +30 -0
- package/support-contracts.md +2 -2
- package/compat-scope.md +0 -46
- package/layering-plan.md +0 -259
- package/src/auto_bind_group_layout.js +0 -32
- package/src/doe.d.ts +0 -184
- package/src/doe.js +0 -641
- package/zig-source-inventory.md +0 -468
|
@@ -0,0 +1,1607 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const PACKAGE_ROOT = resolve(__dirname, "..");
|
|
9
|
+
const README_PATH = resolve(PACKAGE_ROOT, "README.md");
|
|
10
|
+
const DOE_PACKAGE_ROOT = existsSync(resolve(PACKAGE_ROOT, "..", "webgpu-doe", "package.json"))
|
|
11
|
+
? resolve(PACKAGE_ROOT, "..", "webgpu-doe")
|
|
12
|
+
: resolve(PACKAGE_ROOT, "node_modules", "@simulatte", "webgpu-doe");
|
|
13
|
+
const DOE_JS_PATH = resolve(DOE_PACKAGE_ROOT, "src", "index.js");
|
|
14
|
+
const DOE_DTS_PATH = resolve(DOE_PACKAGE_ROOT, "src", "index.d.ts");
|
|
15
|
+
const EXAMPLES_DIR = resolve(PACKAGE_ROOT, "examples", "doe-api");
|
|
16
|
+
const OUTPUT_DIR = resolve(PACKAGE_ROOT, "docs");
|
|
17
|
+
const OUTPUT_PATH = resolve(OUTPUT_DIR, "doe-api-reference.html");
|
|
18
|
+
const DOE_PACKAGE_SOURCE_PATH = "@simulatte/webgpu-doe/src/index.js";
|
|
19
|
+
const DOE_PACKAGE_TYPE_PATH = "@simulatte/webgpu-doe/src/index.d.ts";
|
|
20
|
+
const DOE_GITHUB_PREFIX = "https://github.com/clocksmith/fawn/tree/main/nursery/webgpu-doe/";
|
|
21
|
+
|
|
22
|
+
const API_ENTRY_SPECS = [
|
|
23
|
+
{
|
|
24
|
+
id: "doe.requestDevice",
|
|
25
|
+
title: "doe.requestDevice",
|
|
26
|
+
signature: "doe.requestDevice(options?) -> Promise<gpu>",
|
|
27
|
+
marker: "Request a device and return the bound Doe API in one step.",
|
|
28
|
+
sourcePath: DOE_PACKAGE_SOURCE_PATH,
|
|
29
|
+
typePath: DOE_PACKAGE_TYPE_PATH,
|
|
30
|
+
sourceLabel: "Source",
|
|
31
|
+
typeLabel: "Types",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "doe.bind",
|
|
35
|
+
title: "doe.bind",
|
|
36
|
+
signature: "doe.bind(device) -> gpu",
|
|
37
|
+
marker: "Wrap an existing device in the bound Doe API.",
|
|
38
|
+
sourcePath: DOE_PACKAGE_SOURCE_PATH,
|
|
39
|
+
typePath: DOE_PACKAGE_TYPE_PATH,
|
|
40
|
+
sourceLabel: "Source",
|
|
41
|
+
typeLabel: "Types",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "gpu.buffer.create",
|
|
45
|
+
title: "gpu.buffer.create",
|
|
46
|
+
signature: "gpu.buffer.create(options) -> GPUBuffer",
|
|
47
|
+
marker: "Create a buffer with explicit size and Doe usage tokens.",
|
|
48
|
+
sourcePath: DOE_PACKAGE_SOURCE_PATH,
|
|
49
|
+
typePath: DOE_PACKAGE_TYPE_PATH,
|
|
50
|
+
sourceLabel: "Source",
|
|
51
|
+
typeLabel: "Types",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "gpu.buffer.read",
|
|
55
|
+
title: "gpu.buffer.read",
|
|
56
|
+
signature: "gpu.buffer.read(options) -> Promise<TypedArray>",
|
|
57
|
+
marker: "Read a buffer back into a typed array.",
|
|
58
|
+
sourcePath: DOE_PACKAGE_SOURCE_PATH,
|
|
59
|
+
typePath: DOE_PACKAGE_TYPE_PATH,
|
|
60
|
+
sourceLabel: "Source",
|
|
61
|
+
typeLabel: "Types",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "gpu.kernel.run",
|
|
65
|
+
title: "gpu.kernel.run",
|
|
66
|
+
signature: "gpu.kernel.run(options) -> Promise<void>",
|
|
67
|
+
marker: "Compile and dispatch a one-off compute job.",
|
|
68
|
+
sourcePath: DOE_PACKAGE_SOURCE_PATH,
|
|
69
|
+
typePath: DOE_PACKAGE_TYPE_PATH,
|
|
70
|
+
sourceLabel: "Source",
|
|
71
|
+
typeLabel: "Types",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "gpu.kernel.create",
|
|
75
|
+
title: "gpu.kernel.create",
|
|
76
|
+
signature: "gpu.kernel.create(options) -> DoeKernel",
|
|
77
|
+
marker: "Compile a reusable compute kernel.",
|
|
78
|
+
sourcePath: DOE_PACKAGE_SOURCE_PATH,
|
|
79
|
+
typePath: DOE_PACKAGE_TYPE_PATH,
|
|
80
|
+
sourceLabel: "Source",
|
|
81
|
+
typeLabel: "Types",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "DoeKernel",
|
|
85
|
+
title: "DoeKernel",
|
|
86
|
+
signature: "class DoeKernel",
|
|
87
|
+
marker: "Reusable compute kernel compiled by `gpu.kernel.create(...)`.",
|
|
88
|
+
sourcePath: DOE_PACKAGE_SOURCE_PATH,
|
|
89
|
+
typePath: DOE_PACKAGE_TYPE_PATH,
|
|
90
|
+
sourceLabel: "Source",
|
|
91
|
+
typeLabel: "Types",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "DoeKernel.dispatch",
|
|
95
|
+
title: "kernel.dispatch",
|
|
96
|
+
signature: "kernel.dispatch(options) -> Promise<void>",
|
|
97
|
+
marker: "Dispatch this compiled kernel once.",
|
|
98
|
+
sourcePath: DOE_PACKAGE_SOURCE_PATH,
|
|
99
|
+
typePath: DOE_PACKAGE_TYPE_PATH,
|
|
100
|
+
sourceLabel: "Source",
|
|
101
|
+
typeLabel: "Types",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "gpu.compute",
|
|
105
|
+
title: "gpu.compute",
|
|
106
|
+
signature: "gpu.compute(options) -> Promise<TypedArray>",
|
|
107
|
+
marker: "Run a one-shot typed-array compute workflow.",
|
|
108
|
+
sourcePath: DOE_PACKAGE_SOURCE_PATH,
|
|
109
|
+
typePath: DOE_PACKAGE_TYPE_PATH,
|
|
110
|
+
sourceLabel: "Source",
|
|
111
|
+
typeLabel: "Types",
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const EXAMPLE_ORDER = [
|
|
116
|
+
{
|
|
117
|
+
filename: "buffers-readback.js",
|
|
118
|
+
title: "Buffer create + readback",
|
|
119
|
+
summary: "Create a Doe-managed buffer from host data, then read it back through gpu.buffer.read(...).",
|
|
120
|
+
apiIds: ["gpu.buffer.create", "gpu.buffer.read"],
|
|
121
|
+
accent: "buffer",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
filename: "kernel-run.js",
|
|
125
|
+
title: "One-off kernel run",
|
|
126
|
+
summary: "Use gpu.kernel.run(...) when you want explicit buffers but do not need to keep compiled kernel state.",
|
|
127
|
+
apiIds: ["gpu.kernel.run", "gpu.buffer.create", "gpu.buffer.read"],
|
|
128
|
+
accent: "kernel",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
filename: "kernel-create-and-dispatch.js",
|
|
132
|
+
title: "Reusable kernel dispatch",
|
|
133
|
+
summary: "Compile a DoeKernel once with gpu.kernel.create(...), then dispatch it explicitly.",
|
|
134
|
+
apiIds: ["gpu.kernel.create", "DoeKernel", "DoeKernel.dispatch"],
|
|
135
|
+
accent: "kernel",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
filename: "compute-one-shot.js",
|
|
139
|
+
title: "One-shot compute",
|
|
140
|
+
summary: "Run the opinionated gpu.compute(...) helper with one typed-array input and inferred output sizing.",
|
|
141
|
+
apiIds: ["gpu.compute"],
|
|
142
|
+
accent: "compute",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
filename: "compute-one-shot-like-input.js",
|
|
146
|
+
title: "One-shot compute with likeInput",
|
|
147
|
+
summary: "Use gpu.compute(...) with a uniform input and likeInput sizing to keep output shape explicit.",
|
|
148
|
+
apiIds: ["gpu.compute"],
|
|
149
|
+
accent: "compute",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
filename: "compute-one-shot-multiple-inputs.js",
|
|
153
|
+
title: "One-shot compute with multiple inputs",
|
|
154
|
+
summary: "Feed multiple typed-array inputs through gpu.compute(...) while keeping the shader and result explicit.",
|
|
155
|
+
apiIds: ["gpu.compute"],
|
|
156
|
+
accent: "compute",
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
filename: "compute-one-shot-matmul.js",
|
|
160
|
+
title: "One-shot compute matmul",
|
|
161
|
+
summary: "Run a larger matrix multiply through gpu.compute(...) with explicit tensor dimensions and output size.",
|
|
162
|
+
apiIds: ["gpu.compute"],
|
|
163
|
+
accent: "compute",
|
|
164
|
+
},
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
function readUtf8(path) {
|
|
168
|
+
return readFileSync(path, "utf8");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function resolveDocLink(path) {
|
|
172
|
+
if (path.startsWith("@simulatte/webgpu-doe/")) {
|
|
173
|
+
return `${DOE_GITHUB_PREFIX}${path.slice("@simulatte/webgpu-doe/".length)}`;
|
|
174
|
+
}
|
|
175
|
+
return `../${path}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function escapeHtml(value) {
|
|
179
|
+
return String(value)
|
|
180
|
+
.replaceAll("&", "&")
|
|
181
|
+
.replaceAll("<", "<")
|
|
182
|
+
.replaceAll(">", ">")
|
|
183
|
+
.replaceAll('"', """);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function escapeAttribute(value) {
|
|
187
|
+
return escapeHtml(value).replaceAll("'", "'");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function stripMarkdownInline(value) {
|
|
191
|
+
return value
|
|
192
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
193
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function extractIntro(readme) {
|
|
197
|
+
const lines = readme.split("\n");
|
|
198
|
+
const intro = [];
|
|
199
|
+
for (const line of lines) {
|
|
200
|
+
if (line.startsWith("## Start here")) {
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
if (line.startsWith("# ")) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (line.includes("<") && line.includes(">")) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (line.trim() === "") {
|
|
210
|
+
if (intro.length > 0 && intro.at(-1) !== "") {
|
|
211
|
+
intro.push("");
|
|
212
|
+
}
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
intro.push(line);
|
|
216
|
+
}
|
|
217
|
+
return intro.join("\n").trim();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function extractDocBlock(source, marker) {
|
|
221
|
+
const markerIndex = source.indexOf(marker);
|
|
222
|
+
if (markerIndex === -1) {
|
|
223
|
+
throw new Error(`Could not find marker in doe.js: ${marker}`);
|
|
224
|
+
}
|
|
225
|
+
const start = source.lastIndexOf("/**", markerIndex);
|
|
226
|
+
const end = source.indexOf("*/", start);
|
|
227
|
+
if (start === -1 || end === -1) {
|
|
228
|
+
throw new Error(`Could not find JSDoc block for marker: ${marker}`);
|
|
229
|
+
}
|
|
230
|
+
return source
|
|
231
|
+
.slice(start + 3, end)
|
|
232
|
+
.split("\n")
|
|
233
|
+
.map((line) => line.replace(/^\s*\*\s?/, ""))
|
|
234
|
+
.join("\n")
|
|
235
|
+
.trim();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function parseDocBlock(block) {
|
|
239
|
+
const lines = block.split("\n");
|
|
240
|
+
const summary = stripMarkdownInline(lines[0]?.trim() ?? "");
|
|
241
|
+
const sections = {
|
|
242
|
+
summary,
|
|
243
|
+
surface: "",
|
|
244
|
+
input: "",
|
|
245
|
+
returns: "",
|
|
246
|
+
details: [],
|
|
247
|
+
notes: [],
|
|
248
|
+
example: "",
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
let inExample = false;
|
|
252
|
+
let seenExample = false;
|
|
253
|
+
const exampleLines = [];
|
|
254
|
+
|
|
255
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
256
|
+
const raw = lines[index];
|
|
257
|
+
const line = raw.trim();
|
|
258
|
+
if (line === "```js") {
|
|
259
|
+
inExample = true;
|
|
260
|
+
seenExample = true;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (line === "```") {
|
|
264
|
+
inExample = false;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (inExample) {
|
|
268
|
+
exampleLines.push(raw);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (line.startsWith("Surface:")) {
|
|
272
|
+
sections.surface = stripMarkdownInline(line.slice("Surface:".length).trim());
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (line.startsWith("Input:")) {
|
|
276
|
+
sections.input = stripMarkdownInline(line.slice("Input:".length).trim());
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (line.startsWith("Returns:")) {
|
|
280
|
+
sections.returns = stripMarkdownInline(line.slice("Returns:".length).trim());
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (line.startsWith("- ")) {
|
|
284
|
+
sections.notes.push(stripMarkdownInline(line.slice(2).trim()));
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (line !== "" && !seenExample && !line.startsWith("This example")) {
|
|
288
|
+
sections.details.push(stripMarkdownInline(line));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
sections.details = sections.details.filter((line) => line !== "");
|
|
293
|
+
sections.example = exampleLines.join("\n").trim();
|
|
294
|
+
return sections;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function extractSignatureSnippet(dts, signature) {
|
|
298
|
+
const escaped = signature.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
299
|
+
const regex = new RegExp(escaped, "m");
|
|
300
|
+
return regex.test(dts);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function readExamples() {
|
|
304
|
+
return EXAMPLE_ORDER.map((spec) => {
|
|
305
|
+
const source = readUtf8(resolve(EXAMPLES_DIR, spec.filename)).trim();
|
|
306
|
+
return {
|
|
307
|
+
...spec,
|
|
308
|
+
source,
|
|
309
|
+
runnableSource: source.replace(/^import\s+\{\s*doe\s*\}\s+from\s+"@simulatte\/webgpu\/compute";\n\n?/, ""),
|
|
310
|
+
tokens: `${spec.title} ${spec.summary} ${spec.filename} ${spec.apiIds.join(" ")}`.toLowerCase(),
|
|
311
|
+
};
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function buildApiEntries(doeSource, dtsSource) {
|
|
316
|
+
return API_ENTRY_SPECS.map((spec) => {
|
|
317
|
+
const doc = parseDocBlock(extractDocBlock(doeSource, spec.marker));
|
|
318
|
+
return {
|
|
319
|
+
id: spec.id,
|
|
320
|
+
title: spec.title,
|
|
321
|
+
signature: spec.signature,
|
|
322
|
+
doc,
|
|
323
|
+
sourcePath: spec.sourcePath,
|
|
324
|
+
typePath: spec.typePath,
|
|
325
|
+
sourceLabel: spec.sourceLabel,
|
|
326
|
+
typeLabel: spec.typeLabel,
|
|
327
|
+
hasTypeHint: extractSignatureSnippet(dtsSource, "compute<T") || spec.id !== "gpu.compute",
|
|
328
|
+
tokens: `${spec.title} ${spec.signature} ${doc.summary} ${doc.surface} ${doc.input} ${doc.returns} ${doc.details.join(" ")} ${doc.notes.join(" ")}`.toLowerCase(),
|
|
329
|
+
};
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function renderApiEntry(entry) {
|
|
334
|
+
const notes = entry.doc.notes
|
|
335
|
+
.map((note) => `<li>${escapeHtml(note)}</li>`)
|
|
336
|
+
.join("");
|
|
337
|
+
const details = entry.doc.details
|
|
338
|
+
.map((line) => `<p>${escapeHtml(line)}</p>`)
|
|
339
|
+
.join("");
|
|
340
|
+
const exampleBlock = entry.doc.example
|
|
341
|
+
? `<details class="apiExample"><summary>JSDoc example</summary><pre><code>${escapeHtml(entry.doc.example)}</code></pre></details>`
|
|
342
|
+
: "";
|
|
343
|
+
|
|
344
|
+
return `
|
|
345
|
+
<article class="apiCard searchTarget" data-search="${escapeAttribute(entry.tokens)}" id="${escapeAttribute(entry.id)}">
|
|
346
|
+
<div class="apiHeader">
|
|
347
|
+
<div>
|
|
348
|
+
<div class="eyebrow">${escapeHtml(entry.doc.surface)}</div>
|
|
349
|
+
<h3>${escapeHtml(entry.title)}</h3>
|
|
350
|
+
</div>
|
|
351
|
+
<code class="signature">${escapeHtml(entry.signature)}</code>
|
|
352
|
+
</div>
|
|
353
|
+
<p class="summary">${escapeHtml(entry.doc.summary)}</p>
|
|
354
|
+
<dl class="contractGrid">
|
|
355
|
+
<div><dt>Input</dt><dd>${escapeHtml(entry.doc.input)}</dd></div>
|
|
356
|
+
<div><dt>Returns</dt><dd>${escapeHtml(entry.doc.returns)}</dd></div>
|
|
357
|
+
</dl>
|
|
358
|
+
<div class="detailsBody">${details}</div>
|
|
359
|
+
${notes ? `<ul class="notes">${notes}</ul>` : ""}
|
|
360
|
+
${exampleBlock}
|
|
361
|
+
<div class="linkRow">
|
|
362
|
+
<a href="${escapeAttribute(resolveDocLink(entry.sourcePath))}">${escapeHtml(entry.sourceLabel)}</a>
|
|
363
|
+
<a href="${escapeAttribute(resolveDocLink(entry.typePath))}">${escapeHtml(entry.typeLabel)}</a>
|
|
364
|
+
</div>
|
|
365
|
+
</article>`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function renderExampleCard(example) {
|
|
369
|
+
return `
|
|
370
|
+
<article class="exampleCard searchTarget" data-search="${escapeAttribute(example.tokens)}" data-accent="${escapeAttribute(example.accent)}" id="example-${escapeAttribute(example.filename)}">
|
|
371
|
+
<div class="exampleTop">
|
|
372
|
+
<div>
|
|
373
|
+
<div class="eyebrow">Example</div>
|
|
374
|
+
<h3>${escapeHtml(example.title)}</h3>
|
|
375
|
+
</div>
|
|
376
|
+
<code class="filename">${escapeHtml(example.filename)}</code>
|
|
377
|
+
</div>
|
|
378
|
+
<p class="summary">${escapeHtml(example.summary)}</p>
|
|
379
|
+
<div class="exampleLinks">
|
|
380
|
+
${example.apiIds.map((apiId) => `<a href="#${escapeAttribute(apiId)}">${escapeHtml(apiId)}</a>`).join("")}
|
|
381
|
+
</div>
|
|
382
|
+
<div class="buttonRow">
|
|
383
|
+
<button type="button" class="runButton" data-run-example="${escapeAttribute(example.filename)}">Run example</button>
|
|
384
|
+
<button type="button" class="ghostButton" data-reset-example="${escapeAttribute(example.filename)}">Reset</button>
|
|
385
|
+
<button type="button" class="ghostButton" data-copy-example="${escapeAttribute(example.filename)}">Copy</button>
|
|
386
|
+
<a class="ghostLink" href="../examples/doe-api/${escapeAttribute(example.filename)}">Open source</a>
|
|
387
|
+
</div>
|
|
388
|
+
<div class="outputShell">
|
|
389
|
+
<div class="outputMeta" data-output-meta="${escapeAttribute(example.filename)}">Ready to run in a browser with WebGPU.</div>
|
|
390
|
+
<div class="metricChips" data-output-stats="${escapeAttribute(example.filename)}"></div>
|
|
391
|
+
<canvas class="chartCanvas" data-output-chart="${escapeAttribute(example.filename)}" width="960" height="220"></canvas>
|
|
392
|
+
<div class="valueGrid" data-output-values="${escapeAttribute(example.filename)}"></div>
|
|
393
|
+
<pre class="outputBlock" data-output-text="${escapeAttribute(example.filename)}"></pre>
|
|
394
|
+
</div>
|
|
395
|
+
<details class="editorDetails">
|
|
396
|
+
<summary>View or edit source</summary>
|
|
397
|
+
<div class="editorShell">
|
|
398
|
+
<textarea class="editor" data-example-editor="${escapeAttribute(example.filename)}" spellcheck="false">${escapeHtml(example.source)}</textarea>
|
|
399
|
+
</div>
|
|
400
|
+
</details>
|
|
401
|
+
</article>`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function buildPage(data) {
|
|
405
|
+
const apiCards = data.apiEntries.map(renderApiEntry).join("\n");
|
|
406
|
+
const exampleCards = data.examples.map(renderExampleCard).join("\n");
|
|
407
|
+
const dataJson = JSON.stringify(data);
|
|
408
|
+
|
|
409
|
+
return `<!doctype html>
|
|
410
|
+
<html lang="en">
|
|
411
|
+
<head>
|
|
412
|
+
<meta charset="utf-8" />
|
|
413
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
414
|
+
<title>Doe API reference</title>
|
|
415
|
+
<style>
|
|
416
|
+
:root {
|
|
417
|
+
--bg: #06111a;
|
|
418
|
+
--bg-2: #0d1b2a;
|
|
419
|
+
--panel: rgba(8, 15, 26, 0.76);
|
|
420
|
+
--panel-2: rgba(12, 24, 38, 0.92);
|
|
421
|
+
--line: rgba(160, 196, 255, 0.16);
|
|
422
|
+
--text: #eef6ff;
|
|
423
|
+
--muted: #93a8bf;
|
|
424
|
+
--hot: #fb7185;
|
|
425
|
+
--cold: #22d3ee;
|
|
426
|
+
--gold: #facc15;
|
|
427
|
+
--success: #34d399;
|
|
428
|
+
--paper: rgba(255, 255, 255, 0.03);
|
|
429
|
+
--shadow: 0 30px 80px rgba(0, 0, 0, 0.35);
|
|
430
|
+
--radius: 28px;
|
|
431
|
+
--radius-sm: 18px;
|
|
432
|
+
--mono: "SFMono-Regular", Menlo, Consolas, monospace;
|
|
433
|
+
--sans: "Instrument Sans", "Inter", "Segoe UI", system-ui, sans-serif;
|
|
434
|
+
}
|
|
435
|
+
* { box-sizing: border-box; }
|
|
436
|
+
html { scroll-behavior: smooth; }
|
|
437
|
+
body {
|
|
438
|
+
margin: 0;
|
|
439
|
+
font-family: var(--sans);
|
|
440
|
+
color: var(--text);
|
|
441
|
+
background:
|
|
442
|
+
radial-gradient(circle at top left, rgba(34, 211, 238, 0.22), transparent 32%),
|
|
443
|
+
radial-gradient(circle at top right, rgba(251, 113, 133, 0.18), transparent 28%),
|
|
444
|
+
linear-gradient(160deg, var(--bg) 0%, var(--bg-2) 100%);
|
|
445
|
+
min-height: 100vh;
|
|
446
|
+
}
|
|
447
|
+
a { color: inherit; }
|
|
448
|
+
code, pre, textarea { font-family: var(--mono); }
|
|
449
|
+
.shell {
|
|
450
|
+
display: grid;
|
|
451
|
+
grid-template-columns: 248px minmax(0, 1fr);
|
|
452
|
+
gap: 20px;
|
|
453
|
+
width: min(1380px, calc(100vw - 40px));
|
|
454
|
+
margin: 20px auto 56px;
|
|
455
|
+
}
|
|
456
|
+
.rail {
|
|
457
|
+
position: sticky;
|
|
458
|
+
top: 24px;
|
|
459
|
+
height: calc(100vh - 48px);
|
|
460
|
+
border: 1px solid var(--line);
|
|
461
|
+
border-radius: var(--radius);
|
|
462
|
+
background: linear-gradient(180deg, rgba(7, 13, 23, 0.92), rgba(10, 19, 32, 0.84));
|
|
463
|
+
box-shadow: var(--shadow);
|
|
464
|
+
padding: 20px;
|
|
465
|
+
display: flex;
|
|
466
|
+
flex-direction: column;
|
|
467
|
+
gap: 20px;
|
|
468
|
+
backdrop-filter: blur(20px);
|
|
469
|
+
}
|
|
470
|
+
.brand {
|
|
471
|
+
display: grid;
|
|
472
|
+
gap: 8px;
|
|
473
|
+
}
|
|
474
|
+
.brand h1 {
|
|
475
|
+
margin: 0;
|
|
476
|
+
font-size: 1.4rem;
|
|
477
|
+
letter-spacing: -0.03em;
|
|
478
|
+
}
|
|
479
|
+
.brand p {
|
|
480
|
+
margin: 0;
|
|
481
|
+
color: var(--muted);
|
|
482
|
+
line-height: 1.5;
|
|
483
|
+
font-size: 0.95rem;
|
|
484
|
+
}
|
|
485
|
+
.search {
|
|
486
|
+
width: 100%;
|
|
487
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
488
|
+
border-radius: 14px;
|
|
489
|
+
background: rgba(255, 255, 255, 0.05);
|
|
490
|
+
color: var(--text);
|
|
491
|
+
padding: 12px 14px;
|
|
492
|
+
font: inherit;
|
|
493
|
+
}
|
|
494
|
+
.navGroup {
|
|
495
|
+
display: grid;
|
|
496
|
+
gap: 8px;
|
|
497
|
+
}
|
|
498
|
+
.navGroup h2 {
|
|
499
|
+
margin: 0 0 4px;
|
|
500
|
+
color: var(--muted);
|
|
501
|
+
font-size: 0.75rem;
|
|
502
|
+
text-transform: uppercase;
|
|
503
|
+
letter-spacing: 0.14em;
|
|
504
|
+
}
|
|
505
|
+
.navGroup a {
|
|
506
|
+
text-decoration: none;
|
|
507
|
+
color: var(--text);
|
|
508
|
+
padding: 10px 12px;
|
|
509
|
+
border-radius: 12px;
|
|
510
|
+
background: rgba(255, 255, 255, 0.03);
|
|
511
|
+
border: 1px solid transparent;
|
|
512
|
+
transition: 160ms ease;
|
|
513
|
+
}
|
|
514
|
+
.navGroup a:hover {
|
|
515
|
+
border-color: rgba(255, 255, 255, 0.12);
|
|
516
|
+
transform: translateX(3px);
|
|
517
|
+
}
|
|
518
|
+
.navMeta {
|
|
519
|
+
margin-top: auto;
|
|
520
|
+
color: var(--muted);
|
|
521
|
+
font-size: 0.85rem;
|
|
522
|
+
line-height: 1.6;
|
|
523
|
+
}
|
|
524
|
+
main {
|
|
525
|
+
display: grid;
|
|
526
|
+
gap: 24px;
|
|
527
|
+
}
|
|
528
|
+
.hero,
|
|
529
|
+
.panel,
|
|
530
|
+
.supportPanel {
|
|
531
|
+
border: 1px solid var(--line);
|
|
532
|
+
border-radius: var(--radius);
|
|
533
|
+
background: linear-gradient(180deg, rgba(10, 18, 30, 0.86), rgba(13, 23, 37, 0.78));
|
|
534
|
+
box-shadow: var(--shadow);
|
|
535
|
+
backdrop-filter: blur(18px);
|
|
536
|
+
}
|
|
537
|
+
.hero {
|
|
538
|
+
padding: 36px;
|
|
539
|
+
overflow: hidden;
|
|
540
|
+
position: relative;
|
|
541
|
+
}
|
|
542
|
+
.hero::after {
|
|
543
|
+
content: "";
|
|
544
|
+
position: absolute;
|
|
545
|
+
inset: auto -60px -60px auto;
|
|
546
|
+
width: 240px;
|
|
547
|
+
height: 240px;
|
|
548
|
+
border-radius: 999px;
|
|
549
|
+
background: radial-gradient(circle, rgba(250, 204, 21, 0.3), transparent 68%);
|
|
550
|
+
pointer-events: none;
|
|
551
|
+
}
|
|
552
|
+
.eyebrow {
|
|
553
|
+
color: var(--gold);
|
|
554
|
+
font-size: 0.78rem;
|
|
555
|
+
text-transform: uppercase;
|
|
556
|
+
letter-spacing: 0.18em;
|
|
557
|
+
margin-bottom: 10px;
|
|
558
|
+
}
|
|
559
|
+
.hero h2 {
|
|
560
|
+
margin: 0;
|
|
561
|
+
max-width: 12ch;
|
|
562
|
+
font-size: clamp(2.3rem, 4.8vw, 4.6rem);
|
|
563
|
+
letter-spacing: -0.06em;
|
|
564
|
+
line-height: 0.98;
|
|
565
|
+
}
|
|
566
|
+
.hero p {
|
|
567
|
+
max-width: 680px;
|
|
568
|
+
color: var(--muted);
|
|
569
|
+
line-height: 1.75;
|
|
570
|
+
font-size: 1rem;
|
|
571
|
+
margin: 18px 0 0;
|
|
572
|
+
white-space: pre-line;
|
|
573
|
+
}
|
|
574
|
+
.heroGrid {
|
|
575
|
+
display: grid;
|
|
576
|
+
grid-template-columns: minmax(0, 1.35fr) minmax(320px, 0.9fr);
|
|
577
|
+
gap: 24px;
|
|
578
|
+
align-items: end;
|
|
579
|
+
}
|
|
580
|
+
.heroStats {
|
|
581
|
+
display: grid;
|
|
582
|
+
gap: 14px;
|
|
583
|
+
}
|
|
584
|
+
.heroStat {
|
|
585
|
+
border-radius: 18px;
|
|
586
|
+
padding: 18px;
|
|
587
|
+
background: rgba(255, 255, 255, 0.04);
|
|
588
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
589
|
+
}
|
|
590
|
+
.heroStat strong {
|
|
591
|
+
display: block;
|
|
592
|
+
font-size: 1.65rem;
|
|
593
|
+
letter-spacing: -0.04em;
|
|
594
|
+
}
|
|
595
|
+
.heroStat span {
|
|
596
|
+
color: var(--muted);
|
|
597
|
+
display: block;
|
|
598
|
+
margin-top: 6px;
|
|
599
|
+
line-height: 1.5;
|
|
600
|
+
}
|
|
601
|
+
.sectionHeader {
|
|
602
|
+
display: flex;
|
|
603
|
+
justify-content: space-between;
|
|
604
|
+
gap: 16px;
|
|
605
|
+
align-items: end;
|
|
606
|
+
margin-bottom: 18px;
|
|
607
|
+
}
|
|
608
|
+
.sectionHeader h2 {
|
|
609
|
+
margin: 0;
|
|
610
|
+
font-size: 1.7rem;
|
|
611
|
+
letter-spacing: -0.04em;
|
|
612
|
+
}
|
|
613
|
+
.sectionHeader p {
|
|
614
|
+
margin: 0;
|
|
615
|
+
color: var(--muted);
|
|
616
|
+
max-width: 720px;
|
|
617
|
+
line-height: 1.6;
|
|
618
|
+
}
|
|
619
|
+
.panel {
|
|
620
|
+
padding: 28px;
|
|
621
|
+
}
|
|
622
|
+
.statusGrid,
|
|
623
|
+
.apiGrid,
|
|
624
|
+
.examplesGrid {
|
|
625
|
+
display: grid;
|
|
626
|
+
gap: 18px;
|
|
627
|
+
}
|
|
628
|
+
.statusGrid {
|
|
629
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
630
|
+
}
|
|
631
|
+
.statusCard,
|
|
632
|
+
.apiCard,
|
|
633
|
+
.exampleCard {
|
|
634
|
+
border-radius: 22px;
|
|
635
|
+
background: rgba(255, 255, 255, 0.04);
|
|
636
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
637
|
+
}
|
|
638
|
+
.statusCard {
|
|
639
|
+
padding: 18px;
|
|
640
|
+
}
|
|
641
|
+
.statusCard dt {
|
|
642
|
+
font-size: 0.76rem;
|
|
643
|
+
text-transform: uppercase;
|
|
644
|
+
letter-spacing: 0.16em;
|
|
645
|
+
color: var(--muted);
|
|
646
|
+
}
|
|
647
|
+
.statusCard dd {
|
|
648
|
+
margin: 12px 0 0;
|
|
649
|
+
font-size: 1.2rem;
|
|
650
|
+
letter-spacing: -0.03em;
|
|
651
|
+
}
|
|
652
|
+
.apiGrid {
|
|
653
|
+
grid-template-columns: minmax(0, 1fr);
|
|
654
|
+
}
|
|
655
|
+
.apiCard {
|
|
656
|
+
padding: 20px;
|
|
657
|
+
display: grid;
|
|
658
|
+
gap: 16px;
|
|
659
|
+
}
|
|
660
|
+
.apiHeader {
|
|
661
|
+
display: flex;
|
|
662
|
+
justify-content: space-between;
|
|
663
|
+
gap: 16px;
|
|
664
|
+
align-items: start;
|
|
665
|
+
}
|
|
666
|
+
.apiHeader h3,
|
|
667
|
+
.exampleTop h3 {
|
|
668
|
+
margin: 4px 0 0;
|
|
669
|
+
font-size: 1.3rem;
|
|
670
|
+
letter-spacing: -0.03em;
|
|
671
|
+
}
|
|
672
|
+
.signature,
|
|
673
|
+
.filename {
|
|
674
|
+
white-space: nowrap;
|
|
675
|
+
border-radius: 999px;
|
|
676
|
+
padding: 8px 12px;
|
|
677
|
+
background: rgba(34, 211, 238, 0.1);
|
|
678
|
+
border: 1px solid rgba(34, 211, 238, 0.24);
|
|
679
|
+
color: #baf7ff;
|
|
680
|
+
font-size: 0.84rem;
|
|
681
|
+
}
|
|
682
|
+
.summary,
|
|
683
|
+
.detailsBody p,
|
|
684
|
+
.outputMeta {
|
|
685
|
+
margin: 0;
|
|
686
|
+
color: var(--muted);
|
|
687
|
+
line-height: 1.6;
|
|
688
|
+
}
|
|
689
|
+
.contractGrid {
|
|
690
|
+
display: grid;
|
|
691
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
692
|
+
gap: 14px;
|
|
693
|
+
margin: 0;
|
|
694
|
+
}
|
|
695
|
+
.contractGrid div {
|
|
696
|
+
padding: 14px;
|
|
697
|
+
border-radius: 16px;
|
|
698
|
+
background: rgba(255, 255, 255, 0.03);
|
|
699
|
+
border: 1px solid rgba(255, 255, 255, 0.07);
|
|
700
|
+
}
|
|
701
|
+
.contractGrid dt {
|
|
702
|
+
color: var(--gold);
|
|
703
|
+
font-size: 0.78rem;
|
|
704
|
+
text-transform: uppercase;
|
|
705
|
+
letter-spacing: 0.14em;
|
|
706
|
+
margin-bottom: 8px;
|
|
707
|
+
}
|
|
708
|
+
.contractGrid dd {
|
|
709
|
+
margin: 0;
|
|
710
|
+
line-height: 1.55;
|
|
711
|
+
}
|
|
712
|
+
.notes {
|
|
713
|
+
margin: 0;
|
|
714
|
+
padding-left: 18px;
|
|
715
|
+
color: #d6e7fb;
|
|
716
|
+
display: grid;
|
|
717
|
+
gap: 8px;
|
|
718
|
+
}
|
|
719
|
+
.apiExample summary {
|
|
720
|
+
cursor: pointer;
|
|
721
|
+
color: #d6e7fb;
|
|
722
|
+
}
|
|
723
|
+
.apiExample pre,
|
|
724
|
+
.outputBlock,
|
|
725
|
+
.editor {
|
|
726
|
+
margin: 0;
|
|
727
|
+
background: rgba(3, 7, 13, 0.92);
|
|
728
|
+
border-radius: 18px;
|
|
729
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
730
|
+
color: #dbebff;
|
|
731
|
+
font-size: 0.9rem;
|
|
732
|
+
line-height: 1.55;
|
|
733
|
+
}
|
|
734
|
+
.apiExample pre,
|
|
735
|
+
.outputBlock {
|
|
736
|
+
padding: 16px;
|
|
737
|
+
overflow: auto;
|
|
738
|
+
}
|
|
739
|
+
.linkRow,
|
|
740
|
+
.exampleLinks,
|
|
741
|
+
.buttonRow {
|
|
742
|
+
display: flex;
|
|
743
|
+
flex-wrap: wrap;
|
|
744
|
+
gap: 10px;
|
|
745
|
+
align-items: center;
|
|
746
|
+
}
|
|
747
|
+
.linkRow a,
|
|
748
|
+
.exampleLinks a,
|
|
749
|
+
.ghostLink {
|
|
750
|
+
text-decoration: none;
|
|
751
|
+
color: #c7dbf7;
|
|
752
|
+
padding: 8px 12px;
|
|
753
|
+
border-radius: 999px;
|
|
754
|
+
background: rgba(255, 255, 255, 0.03);
|
|
755
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
756
|
+
}
|
|
757
|
+
.examplesGrid {
|
|
758
|
+
grid-template-columns: minmax(0, 1fr);
|
|
759
|
+
}
|
|
760
|
+
.exampleCard {
|
|
761
|
+
padding: 20px;
|
|
762
|
+
display: grid;
|
|
763
|
+
gap: 14px;
|
|
764
|
+
}
|
|
765
|
+
.exampleTop {
|
|
766
|
+
display: flex;
|
|
767
|
+
justify-content: space-between;
|
|
768
|
+
gap: 16px;
|
|
769
|
+
align-items: start;
|
|
770
|
+
}
|
|
771
|
+
.editor {
|
|
772
|
+
width: 100%;
|
|
773
|
+
min-height: 240px;
|
|
774
|
+
resize: vertical;
|
|
775
|
+
padding: 18px;
|
|
776
|
+
white-space: pre;
|
|
777
|
+
}
|
|
778
|
+
.runButton,
|
|
779
|
+
.ghostButton {
|
|
780
|
+
appearance: none;
|
|
781
|
+
border: none;
|
|
782
|
+
cursor: pointer;
|
|
783
|
+
border-radius: 999px;
|
|
784
|
+
padding: 11px 15px;
|
|
785
|
+
font: inherit;
|
|
786
|
+
color: var(--text);
|
|
787
|
+
}
|
|
788
|
+
.runButton {
|
|
789
|
+
background: linear-gradient(135deg, var(--cold), #60a5fa);
|
|
790
|
+
color: #041521;
|
|
791
|
+
font-weight: 700;
|
|
792
|
+
}
|
|
793
|
+
.ghostButton {
|
|
794
|
+
background: rgba(255, 255, 255, 0.06);
|
|
795
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
796
|
+
}
|
|
797
|
+
.outputShell {
|
|
798
|
+
display: grid;
|
|
799
|
+
gap: 12px;
|
|
800
|
+
}
|
|
801
|
+
.metricChips {
|
|
802
|
+
display: flex;
|
|
803
|
+
flex-wrap: wrap;
|
|
804
|
+
gap: 10px;
|
|
805
|
+
}
|
|
806
|
+
.metricChip,
|
|
807
|
+
.valuePill {
|
|
808
|
+
border-radius: 999px;
|
|
809
|
+
padding: 8px 12px;
|
|
810
|
+
background: var(--paper);
|
|
811
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
812
|
+
color: #d6e7fb;
|
|
813
|
+
font-size: 0.85rem;
|
|
814
|
+
}
|
|
815
|
+
.chartCanvas {
|
|
816
|
+
width: 100%;
|
|
817
|
+
height: 220px;
|
|
818
|
+
display: block;
|
|
819
|
+
border-radius: 18px;
|
|
820
|
+
background: rgba(3, 7, 13, 0.92);
|
|
821
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
822
|
+
}
|
|
823
|
+
.valueGrid {
|
|
824
|
+
display: flex;
|
|
825
|
+
flex-wrap: wrap;
|
|
826
|
+
gap: 10px;
|
|
827
|
+
}
|
|
828
|
+
.editorDetails {
|
|
829
|
+
border-radius: 18px;
|
|
830
|
+
background: var(--paper);
|
|
831
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
832
|
+
overflow: hidden;
|
|
833
|
+
}
|
|
834
|
+
.editorDetails summary {
|
|
835
|
+
cursor: pointer;
|
|
836
|
+
padding: 14px 16px;
|
|
837
|
+
color: #d6e7fb;
|
|
838
|
+
font-weight: 600;
|
|
839
|
+
}
|
|
840
|
+
.editorShell {
|
|
841
|
+
padding: 0 16px 16px;
|
|
842
|
+
}
|
|
843
|
+
.hidden {
|
|
844
|
+
display: none !important;
|
|
845
|
+
}
|
|
846
|
+
.footerNote {
|
|
847
|
+
color: var(--muted);
|
|
848
|
+
line-height: 1.7;
|
|
849
|
+
}
|
|
850
|
+
@media (max-width: 1180px) {
|
|
851
|
+
.shell {
|
|
852
|
+
grid-template-columns: 1fr;
|
|
853
|
+
}
|
|
854
|
+
.rail {
|
|
855
|
+
position: static;
|
|
856
|
+
height: auto;
|
|
857
|
+
}
|
|
858
|
+
.apiGrid,
|
|
859
|
+
.examplesGrid,
|
|
860
|
+
.statusGrid,
|
|
861
|
+
.heroGrid,
|
|
862
|
+
.contractGrid {
|
|
863
|
+
grid-template-columns: 1fr;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
</style>
|
|
867
|
+
</head>
|
|
868
|
+
<body>
|
|
869
|
+
<div class="shell">
|
|
870
|
+
<aside class="rail">
|
|
871
|
+
<div class="brand">
|
|
872
|
+
<div class="eyebrow">Doe API</div>
|
|
873
|
+
<h1>Interactive reference</h1>
|
|
874
|
+
<p>Generated from the real Doe source docs, type surface, and shipped examples.</p>
|
|
875
|
+
</div>
|
|
876
|
+
<input class="search" id="search" type="search" placeholder="Search API and examples" />
|
|
877
|
+
<nav class="navGroup">
|
|
878
|
+
<h2>Jump to</h2>
|
|
879
|
+
<a href="#overview">Overview</a>
|
|
880
|
+
<a href="#runtime">Runtime check</a>
|
|
881
|
+
<a href="#api">API reference</a>
|
|
882
|
+
<a href="#examples">Live examples</a>
|
|
883
|
+
</nav>
|
|
884
|
+
<div class="navGroup">
|
|
885
|
+
<h2>Primary APIs</h2>
|
|
886
|
+
${data.apiEntries.map((entry) => `<a href="#${escapeAttribute(entry.id)}">${escapeHtml(entry.title)}</a>`).join("")}
|
|
887
|
+
</div>
|
|
888
|
+
<div class="navMeta">
|
|
889
|
+
<div>Output: <code>docs/doe-api-reference.html</code></div>
|
|
890
|
+
<div>Source docs: <code>@simulatte/webgpu-doe/src/index.js</code></div>
|
|
891
|
+
<div>Type surface: <code>@simulatte/webgpu-doe/src/index.d.ts</code></div>
|
|
892
|
+
<div>Examples: <code>examples/doe-api/</code></div>
|
|
893
|
+
</div>
|
|
894
|
+
</aside>
|
|
895
|
+
<main>
|
|
896
|
+
<section class="hero" id="overview">
|
|
897
|
+
<div class="heroGrid">
|
|
898
|
+
<div>
|
|
899
|
+
<div class="eyebrow">2026 package docs</div>
|
|
900
|
+
<h2>Doe API, as code and as contract.</h2>
|
|
901
|
+
<p>${escapeHtml(data.intro)}</p>
|
|
902
|
+
</div>
|
|
903
|
+
<div class="heroStats">
|
|
904
|
+
<div class="heroStat">
|
|
905
|
+
<strong>${data.apiEntries.length}</strong>
|
|
906
|
+
<span>public Doe API entries documented from current JSDoc and type shape</span>
|
|
907
|
+
</div>
|
|
908
|
+
<div class="heroStat">
|
|
909
|
+
<strong>${data.examples.length}</strong>
|
|
910
|
+
<span>shipped Doe examples, live-editable and runnable in a browser with WebGPU</span>
|
|
911
|
+
</div>
|
|
912
|
+
<div class="heroStat">
|
|
913
|
+
<strong>1 page</strong>
|
|
914
|
+
<span>API reference, live examples, and runtime status in one self-contained artifact</span>
|
|
915
|
+
</div>
|
|
916
|
+
</div>
|
|
917
|
+
</div>
|
|
918
|
+
</section>
|
|
919
|
+
|
|
920
|
+
<section class="panel" id="runtime">
|
|
921
|
+
<div class="sectionHeader">
|
|
922
|
+
<div>
|
|
923
|
+
<div class="eyebrow">Runtime check</div>
|
|
924
|
+
<h2>Can this browser run the examples?</h2>
|
|
925
|
+
</div>
|
|
926
|
+
<p>The page executes the shipped Doe examples through a browser-side Doe demo adapter over WebGPU. It runs real WGSL and real GPU work when WebGPU is available.</p>
|
|
927
|
+
</div>
|
|
928
|
+
<div class="statusGrid">
|
|
929
|
+
<dl class="statusCard">
|
|
930
|
+
<dt>WebGPU</dt>
|
|
931
|
+
<dd id="status-webgpu">Checking…</dd>
|
|
932
|
+
</dl>
|
|
933
|
+
<dl class="statusCard">
|
|
934
|
+
<dt>Adapter</dt>
|
|
935
|
+
<dd id="status-adapter">Pending</dd>
|
|
936
|
+
</dl>
|
|
937
|
+
<dl class="statusCard">
|
|
938
|
+
<dt>Device</dt>
|
|
939
|
+
<dd id="status-device">Pending</dd>
|
|
940
|
+
</dl>
|
|
941
|
+
</div>
|
|
942
|
+
</section>
|
|
943
|
+
|
|
944
|
+
<section class="panel" id="api">
|
|
945
|
+
<div class="sectionHeader">
|
|
946
|
+
<div>
|
|
947
|
+
<div class="eyebrow">API reference</div>
|
|
948
|
+
<h2>Current shipped Doe surface</h2>
|
|
949
|
+
</div>
|
|
950
|
+
<p>Each card is generated from the public JSDoc in <code>@simulatte/webgpu-doe/src/index.js</code> and linked back to the implementation and type surface.</p>
|
|
951
|
+
</div>
|
|
952
|
+
<div class="apiGrid">
|
|
953
|
+
${apiCards}
|
|
954
|
+
</div>
|
|
955
|
+
</section>
|
|
956
|
+
|
|
957
|
+
<section class="panel" id="examples">
|
|
958
|
+
<div class="sectionHeader">
|
|
959
|
+
<div>
|
|
960
|
+
<div class="eyebrow">Live examples</div>
|
|
961
|
+
<h2>Shipped examples that actually run</h2>
|
|
962
|
+
</div>
|
|
963
|
+
<p>These editors start from the real files in <code>examples/doe-api/</code>. Run them as-is, tweak them inline, or use them to compare the explicit kernel path against the one-shot <code>gpu.compute(...)</code> helper.</p>
|
|
964
|
+
</div>
|
|
965
|
+
<div class="examplesGrid">
|
|
966
|
+
${exampleCards}
|
|
967
|
+
</div>
|
|
968
|
+
</section>
|
|
969
|
+
|
|
970
|
+
<section class="supportPanel panel">
|
|
971
|
+
<div class="sectionHeader">
|
|
972
|
+
<div>
|
|
973
|
+
<div class="eyebrow">Generated from</div>
|
|
974
|
+
<h2>Source-of-truth inputs</h2>
|
|
975
|
+
</div>
|
|
976
|
+
<p>The page is generated, not hand-maintained. When the Doe API changes, regenerate this artifact from the current source, types, examples, and README contract language.</p>
|
|
977
|
+
</div>
|
|
978
|
+
<p class="footerNote">
|
|
979
|
+
Inputs: <code>README.md</code>, <code>@simulatte/webgpu-doe/src/index.js</code>, <code>@simulatte/webgpu-doe/src/index.d.ts</code>, and
|
|
980
|
+
the shipped Doe example files in <code>examples/doe-api/</code>. Generated by
|
|
981
|
+
<code>scripts/generate-doe-api-docs.js</code>.
|
|
982
|
+
</p>
|
|
983
|
+
</section>
|
|
984
|
+
</main>
|
|
985
|
+
</div>
|
|
986
|
+
|
|
987
|
+
<script id="doe-api-data" type="application/json">${escapeHtml(dataJson)}</script>
|
|
988
|
+
<script type="module">
|
|
989
|
+
const DOC_DATA = JSON.parse(document.getElementById("doe-api-data").textContent);
|
|
990
|
+
const outputState = new Map();
|
|
991
|
+
let doePromise = null;
|
|
992
|
+
|
|
993
|
+
function setStatus(id, text) {
|
|
994
|
+
document.getElementById(id).textContent = text;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function resolveBufferUsageToken(token, combined = false) {
|
|
998
|
+
switch (token) {
|
|
999
|
+
case "upload":
|
|
1000
|
+
return GPUBufferUsage.COPY_DST;
|
|
1001
|
+
case "readback":
|
|
1002
|
+
return combined
|
|
1003
|
+
? GPUBufferUsage.COPY_SRC
|
|
1004
|
+
: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ;
|
|
1005
|
+
case "uniform":
|
|
1006
|
+
return GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST;
|
|
1007
|
+
case "storageRead":
|
|
1008
|
+
return GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST;
|
|
1009
|
+
case "storageReadWrite":
|
|
1010
|
+
return GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
|
|
1011
|
+
default:
|
|
1012
|
+
throw new Error(\`Unknown Doe buffer usage token: \${token}\`);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function resolveBufferUsage(usage) {
|
|
1017
|
+
if (typeof usage === "number") return usage;
|
|
1018
|
+
if (typeof usage === "string") return resolveBufferUsageToken(usage);
|
|
1019
|
+
if (Array.isArray(usage)) {
|
|
1020
|
+
const combined = usage.length > 1;
|
|
1021
|
+
return usage.reduce((mask, token) => mask | (
|
|
1022
|
+
typeof token === "number"
|
|
1023
|
+
? token
|
|
1024
|
+
: resolveBufferUsageToken(token, combined)
|
|
1025
|
+
), 0);
|
|
1026
|
+
}
|
|
1027
|
+
throw new Error("Doe buffer usage must be a number, string, or string array.");
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function inferBindingAccessToken(token) {
|
|
1031
|
+
switch (token) {
|
|
1032
|
+
case "uniform":
|
|
1033
|
+
return "uniform";
|
|
1034
|
+
case "storageRead":
|
|
1035
|
+
return "storageRead";
|
|
1036
|
+
case "storageReadWrite":
|
|
1037
|
+
return "storageReadWrite";
|
|
1038
|
+
default:
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function inferBindingAccess(usage) {
|
|
1044
|
+
if (typeof usage === "number" || usage == null) return null;
|
|
1045
|
+
const tokens = typeof usage === "string"
|
|
1046
|
+
? [usage]
|
|
1047
|
+
: Array.isArray(usage)
|
|
1048
|
+
? usage.filter((token) => typeof token !== "number")
|
|
1049
|
+
: null;
|
|
1050
|
+
if (!tokens) return null;
|
|
1051
|
+
const inferred = [...new Set(tokens.map(inferBindingAccessToken).filter(Boolean))];
|
|
1052
|
+
if (inferred.length > 1) {
|
|
1053
|
+
throw new Error(\`Doe buffer usage cannot imply multiple binding access modes: \${inferred.join(", ")}\`);
|
|
1054
|
+
}
|
|
1055
|
+
return inferred[0] ?? null;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function normalizeDataView(data) {
|
|
1059
|
+
if (ArrayBuffer.isView(data)) {
|
|
1060
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
1061
|
+
}
|
|
1062
|
+
if (data instanceof ArrayBuffer) {
|
|
1063
|
+
return new Uint8Array(data);
|
|
1064
|
+
}
|
|
1065
|
+
throw new Error("Doe buffer data must be an ArrayBuffer or ArrayBufferView.");
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function resolveBufferSize(source, meta) {
|
|
1069
|
+
if (source && typeof source === "object" && typeof source.size === "number") {
|
|
1070
|
+
return source.size;
|
|
1071
|
+
}
|
|
1072
|
+
if (meta.has(source)) {
|
|
1073
|
+
return meta.get(source).size;
|
|
1074
|
+
}
|
|
1075
|
+
if (ArrayBuffer.isView(source)) {
|
|
1076
|
+
return source.byteLength;
|
|
1077
|
+
}
|
|
1078
|
+
if (source instanceof ArrayBuffer) {
|
|
1079
|
+
return source.byteLength;
|
|
1080
|
+
}
|
|
1081
|
+
throw new Error("Doe buffer-like source must expose a byte size or be ArrayBuffer-backed data.");
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function normalizeWorkgroups(workgroups) {
|
|
1085
|
+
if (typeof workgroups === "number") return [workgroups, 1, 1];
|
|
1086
|
+
if (Array.isArray(workgroups) && workgroups.length === 2) return [workgroups[0], workgroups[1], 1];
|
|
1087
|
+
if (Array.isArray(workgroups) && workgroups.length === 3) return workgroups;
|
|
1088
|
+
throw new Error("Doe workgroups must be a number, [x, y], or [x, y, z].");
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function validatePositiveInteger(value, label) {
|
|
1092
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
1093
|
+
throw new Error(\`\${label} must be a positive integer.\`);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function createBrowserDoe(device) {
|
|
1098
|
+
const bufferMeta = new WeakMap();
|
|
1099
|
+
|
|
1100
|
+
function rememberBuffer(buffer, usage, size) {
|
|
1101
|
+
bufferMeta.set(buffer, {
|
|
1102
|
+
bindingAccess: inferBindingAccess(usage),
|
|
1103
|
+
size,
|
|
1104
|
+
});
|
|
1105
|
+
return buffer;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function inferredBindingAccessForBuffer(buffer) {
|
|
1109
|
+
return bufferMeta.get(buffer)?.bindingAccess ?? null;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function validateWorkgroups(workgroups) {
|
|
1113
|
+
const normalized = normalizeWorkgroups(workgroups);
|
|
1114
|
+
const [x, y, z] = normalized;
|
|
1115
|
+
validatePositiveInteger(x, "Doe workgroups.x");
|
|
1116
|
+
validatePositiveInteger(y, "Doe workgroups.y");
|
|
1117
|
+
validatePositiveInteger(z, "Doe workgroups.z");
|
|
1118
|
+
return normalized;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function normalizeBinding(binding, index) {
|
|
1122
|
+
const entry = binding && typeof binding === "object" && "buffer" in binding
|
|
1123
|
+
? binding
|
|
1124
|
+
: { buffer: binding };
|
|
1125
|
+
const access = entry.access ?? inferredBindingAccessForBuffer(entry.buffer);
|
|
1126
|
+
if (!access) {
|
|
1127
|
+
throw new Error(
|
|
1128
|
+
"Doe binding access is required for buffers without Doe helper usage metadata. " +
|
|
1129
|
+
"Pass { buffer, access } or create the buffer through gpu.buffer.create(...) with a bindable usage token."
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
return {
|
|
1133
|
+
binding: index,
|
|
1134
|
+
buffer: entry.buffer,
|
|
1135
|
+
access,
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function bindGroupLayoutEntry(binding) {
|
|
1140
|
+
const bufferType = binding.access === "uniform"
|
|
1141
|
+
? "uniform"
|
|
1142
|
+
: binding.access === "storageRead"
|
|
1143
|
+
? "read-only-storage"
|
|
1144
|
+
: "storage";
|
|
1145
|
+
return {
|
|
1146
|
+
binding: binding.binding,
|
|
1147
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
1148
|
+
buffer: { type: bufferType },
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function bindGroupEntry(binding) {
|
|
1153
|
+
return {
|
|
1154
|
+
binding: binding.binding,
|
|
1155
|
+
resource: { buffer: binding.buffer },
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
class BrowserDoeKernel {
|
|
1160
|
+
constructor(pipeline, layout) {
|
|
1161
|
+
this.pipeline = pipeline;
|
|
1162
|
+
this.layout = layout;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
async dispatch(options) {
|
|
1166
|
+
const bindings = (options.bindings ?? []).map(normalizeBinding);
|
|
1167
|
+
const workgroups = validateWorkgroups(options.workgroups);
|
|
1168
|
+
const bindGroup = device.createBindGroup({
|
|
1169
|
+
layout: this.layout,
|
|
1170
|
+
entries: bindings.map(bindGroupEntry),
|
|
1171
|
+
});
|
|
1172
|
+
const encoder = device.createCommandEncoder({ label: options.label ?? undefined });
|
|
1173
|
+
const pass = encoder.beginComputePass({ label: options.label ?? undefined });
|
|
1174
|
+
pass.setPipeline(this.pipeline);
|
|
1175
|
+
if (bindings.length > 0) {
|
|
1176
|
+
pass.setBindGroup(0, bindGroup);
|
|
1177
|
+
}
|
|
1178
|
+
pass.dispatchWorkgroups(workgroups[0], workgroups[1], workgroups[2]);
|
|
1179
|
+
pass.end();
|
|
1180
|
+
device.queue.submit([encoder.finish()]);
|
|
1181
|
+
await device.queue.onSubmittedWorkDone();
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function createBuffer(options) {
|
|
1186
|
+
if (!options || typeof options !== "object") {
|
|
1187
|
+
throw new Error("Doe buffer options must be an object.");
|
|
1188
|
+
}
|
|
1189
|
+
if (options.data != null) {
|
|
1190
|
+
const view = normalizeDataView(options.data);
|
|
1191
|
+
const usage = options.usage ?? "storageRead";
|
|
1192
|
+
const size = options.size ?? view.byteLength;
|
|
1193
|
+
const buffer = rememberBuffer(device.createBuffer({
|
|
1194
|
+
label: options.label ?? undefined,
|
|
1195
|
+
size,
|
|
1196
|
+
usage: resolveBufferUsage(usage),
|
|
1197
|
+
mappedAtCreation: false,
|
|
1198
|
+
}), usage, size);
|
|
1199
|
+
device.queue.writeBuffer(buffer, 0, view);
|
|
1200
|
+
return buffer;
|
|
1201
|
+
}
|
|
1202
|
+
validatePositiveInteger(options.size, "Doe buffer size");
|
|
1203
|
+
return rememberBuffer(device.createBuffer({
|
|
1204
|
+
label: options.label ?? undefined,
|
|
1205
|
+
size: options.size,
|
|
1206
|
+
usage: resolveBufferUsage(options.usage),
|
|
1207
|
+
mappedAtCreation: options.mappedAtCreation ?? false,
|
|
1208
|
+
}), options.usage, options.size);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
async function readBuffer(options) {
|
|
1212
|
+
if (!options || typeof options !== "object") {
|
|
1213
|
+
throw new Error("Doe buffer.read options must be an object.");
|
|
1214
|
+
}
|
|
1215
|
+
const buffer = options.buffer;
|
|
1216
|
+
const type = options.type;
|
|
1217
|
+
if (!buffer || typeof buffer !== "object") {
|
|
1218
|
+
throw new Error("Doe buffer.read requires a buffer.");
|
|
1219
|
+
}
|
|
1220
|
+
if (typeof type !== "function") {
|
|
1221
|
+
throw new Error("Doe buffer.read type must be a typed-array constructor.");
|
|
1222
|
+
}
|
|
1223
|
+
const fullSize = resolveBufferSize(buffer, bufferMeta);
|
|
1224
|
+
const offset = options.offset ?? 0;
|
|
1225
|
+
const size = options.size ?? Math.max(0, fullSize - offset);
|
|
1226
|
+
const staging = device.createBuffer({
|
|
1227
|
+
label: options.label ?? undefined,
|
|
1228
|
+
size,
|
|
1229
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
|
1230
|
+
});
|
|
1231
|
+
const encoder = device.createCommandEncoder({ label: options.label ?? undefined });
|
|
1232
|
+
encoder.copyBufferToBuffer(buffer, offset, staging, 0, size);
|
|
1233
|
+
device.queue.submit([encoder.finish()]);
|
|
1234
|
+
await device.queue.onSubmittedWorkDone();
|
|
1235
|
+
await staging.mapAsync(GPUMapMode.READ);
|
|
1236
|
+
const copy = staging.getMappedRange().slice(0);
|
|
1237
|
+
staging.unmap();
|
|
1238
|
+
staging.destroy();
|
|
1239
|
+
return new type(copy);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function createKernel(options) {
|
|
1243
|
+
const bindings = (options.bindings ?? []).map(normalizeBinding);
|
|
1244
|
+
const shader = device.createShaderModule({ code: options.code });
|
|
1245
|
+
const bindGroupLayout = device.createBindGroupLayout({
|
|
1246
|
+
entries: bindings.map(bindGroupLayoutEntry),
|
|
1247
|
+
});
|
|
1248
|
+
const pipelineLayout = device.createPipelineLayout({
|
|
1249
|
+
bindGroupLayouts: [bindGroupLayout],
|
|
1250
|
+
});
|
|
1251
|
+
const pipeline = device.createComputePipeline({
|
|
1252
|
+
layout: pipelineLayout,
|
|
1253
|
+
compute: {
|
|
1254
|
+
module: shader,
|
|
1255
|
+
entryPoint: options.entryPoint ?? "main",
|
|
1256
|
+
},
|
|
1257
|
+
});
|
|
1258
|
+
return new BrowserDoeKernel(pipeline, bindGroupLayout);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
async function runKernel(options) {
|
|
1262
|
+
const kernel = createKernel(options);
|
|
1263
|
+
await kernel.dispatch({
|
|
1264
|
+
bindings: options.bindings ?? [],
|
|
1265
|
+
workgroups: options.workgroups,
|
|
1266
|
+
label: options.label,
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function usesRawNumericFlags(usage) {
|
|
1271
|
+
return typeof usage === "number" || (Array.isArray(usage) && usage.some((token) => typeof token === "number"));
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function assertLayer3Usage(usage, access, path) {
|
|
1275
|
+
if (usesRawNumericFlags(usage) && !access) {
|
|
1276
|
+
throw new Error(\`Doe \${path} accepts raw numeric usage flags only when explicit access is also provided.\`);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function normalizeOnceInput(input, index) {
|
|
1281
|
+
if (ArrayBuffer.isView(input) || input instanceof ArrayBuffer) {
|
|
1282
|
+
const buffer = createBuffer({ data: input });
|
|
1283
|
+
return {
|
|
1284
|
+
binding: buffer,
|
|
1285
|
+
buffer,
|
|
1286
|
+
byteLength: resolveBufferSize(input, bufferMeta),
|
|
1287
|
+
owned: true,
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
if (input && typeof input === "object" && "data" in input) {
|
|
1291
|
+
assertLayer3Usage(input.usage, input.access, \`compute input \${index} usage\`);
|
|
1292
|
+
const buffer = createBuffer({
|
|
1293
|
+
data: input.data,
|
|
1294
|
+
usage: input.usage ?? "storageRead",
|
|
1295
|
+
label: input.label,
|
|
1296
|
+
});
|
|
1297
|
+
return {
|
|
1298
|
+
binding: input.access ? { buffer, access: input.access } : buffer,
|
|
1299
|
+
buffer,
|
|
1300
|
+
byteLength: resolveBufferSize(input.data, bufferMeta),
|
|
1301
|
+
owned: true,
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
if (input && typeof input === "object" && "buffer" in input) {
|
|
1305
|
+
return {
|
|
1306
|
+
binding: input,
|
|
1307
|
+
buffer: input.buffer,
|
|
1308
|
+
byteLength: resolveBufferSize(input.buffer, bufferMeta),
|
|
1309
|
+
owned: false,
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
if (input && typeof input === "object") {
|
|
1313
|
+
return {
|
|
1314
|
+
binding: input,
|
|
1315
|
+
buffer: input,
|
|
1316
|
+
byteLength: resolveBufferSize(input, bufferMeta),
|
|
1317
|
+
owned: false,
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
throw new Error(\`Doe compute input \${index} must be data, a Doe input spec, or a buffer.\`);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function normalizeOnceOutput(output, inputs) {
|
|
1324
|
+
if (!output || typeof output !== "object") {
|
|
1325
|
+
throw new Error("Doe compute output is required.");
|
|
1326
|
+
}
|
|
1327
|
+
if (typeof output.type !== "function") {
|
|
1328
|
+
throw new Error("Doe compute output.type must be a typed-array constructor.");
|
|
1329
|
+
}
|
|
1330
|
+
const fallbackInputIndex = inputs.length > 0 ? 0 : null;
|
|
1331
|
+
const likeInputIndex = output.likeInput ?? fallbackInputIndex;
|
|
1332
|
+
const size = output.size ?? (
|
|
1333
|
+
likeInputIndex != null && inputs[likeInputIndex]
|
|
1334
|
+
? inputs[likeInputIndex].byteLength
|
|
1335
|
+
: null
|
|
1336
|
+
);
|
|
1337
|
+
if (!(size > 0)) {
|
|
1338
|
+
throw new Error("Doe compute output size must be provided or derived from likeInput.");
|
|
1339
|
+
}
|
|
1340
|
+
assertLayer3Usage(output.usage, output.access, "compute output usage");
|
|
1341
|
+
const buffer = createBuffer({
|
|
1342
|
+
size,
|
|
1343
|
+
usage: output.usage ?? "storageReadWrite",
|
|
1344
|
+
label: output.label,
|
|
1345
|
+
});
|
|
1346
|
+
return {
|
|
1347
|
+
binding: output.access ? { buffer, access: output.access } : buffer,
|
|
1348
|
+
buffer,
|
|
1349
|
+
type: output.type,
|
|
1350
|
+
readOptions: output.read ?? {},
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
async function compute(options) {
|
|
1355
|
+
const inputs = (options.inputs ?? []).map((input, index) => normalizeOnceInput(input, index));
|
|
1356
|
+
const output = normalizeOnceOutput(options.output, inputs);
|
|
1357
|
+
validateWorkgroups(options.workgroups);
|
|
1358
|
+
try {
|
|
1359
|
+
await runKernel({
|
|
1360
|
+
code: options.code,
|
|
1361
|
+
entryPoint: options.entryPoint,
|
|
1362
|
+
bindings: [...inputs.map((input) => input.binding), output.binding],
|
|
1363
|
+
workgroups: options.workgroups,
|
|
1364
|
+
label: options.label,
|
|
1365
|
+
});
|
|
1366
|
+
return await readBuffer({ buffer: output.buffer, type: output.type, ...output.readOptions });
|
|
1367
|
+
} finally {
|
|
1368
|
+
output.buffer.destroy?.();
|
|
1369
|
+
for (const input of inputs) {
|
|
1370
|
+
if (input.owned) {
|
|
1371
|
+
input.buffer.destroy?.();
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
return {
|
|
1378
|
+
device,
|
|
1379
|
+
buffer: {
|
|
1380
|
+
create: createBuffer,
|
|
1381
|
+
read: readBuffer,
|
|
1382
|
+
},
|
|
1383
|
+
kernel: {
|
|
1384
|
+
run: runKernel,
|
|
1385
|
+
create: createKernel,
|
|
1386
|
+
},
|
|
1387
|
+
compute,
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
async function ensureDoe() {
|
|
1392
|
+
if (doePromise) return doePromise;
|
|
1393
|
+
if (!("gpu" in navigator)) {
|
|
1394
|
+
throw new Error("WebGPU is unavailable in this browser.");
|
|
1395
|
+
}
|
|
1396
|
+
setStatus("status-webgpu", "WebGPU available");
|
|
1397
|
+
doePromise = (async () => {
|
|
1398
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
1399
|
+
if (!adapter) {
|
|
1400
|
+
throw new Error("No WebGPU adapter was returned.");
|
|
1401
|
+
}
|
|
1402
|
+
setStatus("status-adapter", adapter.name || "Adapter ready");
|
|
1403
|
+
const device = await adapter.requestDevice();
|
|
1404
|
+
setStatus("status-device", "Device ready");
|
|
1405
|
+
return {
|
|
1406
|
+
requestDevice: async () => createBrowserDoe(device),
|
|
1407
|
+
bind: (rawDevice) => createBrowserDoe(rawDevice),
|
|
1408
|
+
};
|
|
1409
|
+
})();
|
|
1410
|
+
return doePromise;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function parseConsoleOutput(lines) {
|
|
1414
|
+
const trimmed = lines.join("\\n").trim();
|
|
1415
|
+
if (!trimmed) return { text: "No console output.", data: null };
|
|
1416
|
+
try {
|
|
1417
|
+
return {
|
|
1418
|
+
text: trimmed,
|
|
1419
|
+
data: JSON.parse(trimmed),
|
|
1420
|
+
};
|
|
1421
|
+
} catch {
|
|
1422
|
+
return { text: trimmed, data: null };
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function clearChart(canvas) {
|
|
1427
|
+
const context = canvas.getContext("2d");
|
|
1428
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function renderViz(statsTarget, canvas, valuesTarget, data) {
|
|
1432
|
+
statsTarget.innerHTML = "";
|
|
1433
|
+
valuesTarget.innerHTML = "";
|
|
1434
|
+
clearChart(canvas);
|
|
1435
|
+
if (!Array.isArray(data) || data.length === 0 || !data.every((value) => typeof value === "number")) {
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
const count = data.length;
|
|
1440
|
+
const min = Math.min(...data);
|
|
1441
|
+
const max = Math.max(...data);
|
|
1442
|
+
const mean = data.reduce((sum, value) => sum + value, 0) / count;
|
|
1443
|
+
const stats = [
|
|
1444
|
+
\`count \${count}\`,
|
|
1445
|
+
\`min \${Number(min.toFixed(4))}\`,
|
|
1446
|
+
\`max \${Number(max.toFixed(4))}\`,
|
|
1447
|
+
\`mean \${Number(mean.toFixed(4))}\`,
|
|
1448
|
+
];
|
|
1449
|
+
for (const stat of stats) {
|
|
1450
|
+
const chip = document.createElement("div");
|
|
1451
|
+
chip.className = "metricChip";
|
|
1452
|
+
chip.textContent = stat;
|
|
1453
|
+
statsTarget.appendChild(chip);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
for (const value of data.slice(0, 16)) {
|
|
1457
|
+
const pill = document.createElement("div");
|
|
1458
|
+
pill.className = "valuePill";
|
|
1459
|
+
pill.textContent = Number(value.toFixed(4)).toString();
|
|
1460
|
+
valuesTarget.appendChild(pill);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
const context = canvas.getContext("2d");
|
|
1464
|
+
const { width, height } = canvas;
|
|
1465
|
+
const padX = 26;
|
|
1466
|
+
const padY = 24;
|
|
1467
|
+
const range = max - min || 1;
|
|
1468
|
+
const stepX = count > 1 ? (width - padX * 2) / (count - 1) : 0;
|
|
1469
|
+
|
|
1470
|
+
context.clearRect(0, 0, width, height);
|
|
1471
|
+
context.strokeStyle = "rgba(255,255,255,0.12)";
|
|
1472
|
+
context.lineWidth = 1;
|
|
1473
|
+
context.beginPath();
|
|
1474
|
+
context.moveTo(padX, height - padY);
|
|
1475
|
+
context.lineTo(width - padX, height - padY);
|
|
1476
|
+
context.moveTo(padX, padY);
|
|
1477
|
+
context.lineTo(padX, height - padY);
|
|
1478
|
+
context.stroke();
|
|
1479
|
+
|
|
1480
|
+
context.beginPath();
|
|
1481
|
+
data.forEach((value, index) => {
|
|
1482
|
+
const x = padX + index * stepX;
|
|
1483
|
+
const y = height - padY - ((value - min) / range) * (height - padY * 2);
|
|
1484
|
+
if (index === 0) {
|
|
1485
|
+
context.moveTo(x, y);
|
|
1486
|
+
} else {
|
|
1487
|
+
context.lineTo(x, y);
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
context.strokeStyle = "rgba(34, 211, 238, 0.95)";
|
|
1491
|
+
context.lineWidth = 3;
|
|
1492
|
+
context.stroke();
|
|
1493
|
+
|
|
1494
|
+
context.lineTo(width - padX, height - padY);
|
|
1495
|
+
context.lineTo(padX, height - padY);
|
|
1496
|
+
context.closePath();
|
|
1497
|
+
context.fillStyle = "rgba(34, 211, 238, 0.12)";
|
|
1498
|
+
context.fill();
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function attachSearch() {
|
|
1502
|
+
const input = document.getElementById("search");
|
|
1503
|
+
const targets = [...document.querySelectorAll(".searchTarget")];
|
|
1504
|
+
input.addEventListener("input", () => {
|
|
1505
|
+
const query = input.value.trim().toLowerCase();
|
|
1506
|
+
for (const target of targets) {
|
|
1507
|
+
const haystack = target.dataset.search || "";
|
|
1508
|
+
target.classList.toggle("hidden", query !== "" && !haystack.includes(query));
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function attachExampleControls() {
|
|
1514
|
+
for (const example of DOC_DATA.examples) {
|
|
1515
|
+
const editor = document.querySelector(\`[data-example-editor="\${CSS.escape(example.filename)}"]\`);
|
|
1516
|
+
const meta = document.querySelector(\`[data-output-meta="\${CSS.escape(example.filename)}"]\`);
|
|
1517
|
+
const text = document.querySelector(\`[data-output-text="\${CSS.escape(example.filename)}"]\`);
|
|
1518
|
+
const stats = document.querySelector(\`[data-output-stats="\${CSS.escape(example.filename)}"]\`);
|
|
1519
|
+
const chart = document.querySelector(\`[data-output-chart="\${CSS.escape(example.filename)}"]\`);
|
|
1520
|
+
const values = document.querySelector(\`[data-output-values="\${CSS.escape(example.filename)}"]\`);
|
|
1521
|
+
|
|
1522
|
+
document.querySelector(\`[data-reset-example="\${CSS.escape(example.filename)}"]\`).addEventListener("click", () => {
|
|
1523
|
+
editor.value = example.source;
|
|
1524
|
+
meta.textContent = "Reset to shipped example.";
|
|
1525
|
+
text.textContent = "";
|
|
1526
|
+
stats.innerHTML = "";
|
|
1527
|
+
values.innerHTML = "";
|
|
1528
|
+
clearChart(chart);
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
document.querySelector(\`[data-copy-example="\${CSS.escape(example.filename)}"]\`).addEventListener("click", async () => {
|
|
1532
|
+
await navigator.clipboard.writeText(editor.value);
|
|
1533
|
+
meta.textContent = "Copied example source.";
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
document.querySelector(\`[data-run-example="\${CSS.escape(example.filename)}"]\`).addEventListener("click", async () => {
|
|
1537
|
+
meta.textContent = "Running…";
|
|
1538
|
+
text.textContent = "";
|
|
1539
|
+
stats.innerHTML = "";
|
|
1540
|
+
values.innerHTML = "";
|
|
1541
|
+
clearChart(chart);
|
|
1542
|
+
try {
|
|
1543
|
+
const doe = await ensureDoe();
|
|
1544
|
+
const logs = [];
|
|
1545
|
+
const scopedConsole = {
|
|
1546
|
+
log(...args) {
|
|
1547
|
+
logs.push(args.map((value) => typeof value === "string" ? value : JSON.stringify(value)).join(" "));
|
|
1548
|
+
},
|
|
1549
|
+
};
|
|
1550
|
+
const runnable = editor.value.replace(/^import\\s+\\{\\s*doe\\s*\\}\\s+from\\s+"@simulatte\\/webgpu\\/compute";\\n\\n?/, "");
|
|
1551
|
+
const fn = new Function("doe", "console", \`return (async () => {\\n\${runnable}\\n})();\`);
|
|
1552
|
+
await fn(doe, scopedConsole);
|
|
1553
|
+
const parsed = parseConsoleOutput(logs);
|
|
1554
|
+
meta.textContent = "Ran successfully.";
|
|
1555
|
+
text.textContent = parsed.text;
|
|
1556
|
+
renderViz(stats, chart, values, parsed.data);
|
|
1557
|
+
} catch (error) {
|
|
1558
|
+
meta.textContent = "Run failed.";
|
|
1559
|
+
text.textContent = error && error.stack ? error.stack : String(error);
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
async function initRuntimeStatus() {
|
|
1566
|
+
if (!("gpu" in navigator)) {
|
|
1567
|
+
setStatus("status-webgpu", "Unavailable");
|
|
1568
|
+
setStatus("status-adapter", "No WebGPU");
|
|
1569
|
+
setStatus("status-device", "No WebGPU");
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
setStatus("status-webgpu", "Available");
|
|
1573
|
+
try {
|
|
1574
|
+
await ensureDoe();
|
|
1575
|
+
} catch (error) {
|
|
1576
|
+
setStatus("status-adapter", "Failed");
|
|
1577
|
+
setStatus("status-device", error?.message || "Unavailable");
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
attachSearch();
|
|
1582
|
+
attachExampleControls();
|
|
1583
|
+
initRuntimeStatus();
|
|
1584
|
+
</script>
|
|
1585
|
+
</body>
|
|
1586
|
+
</html>`;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function main() {
|
|
1590
|
+
const intro = extractIntro(readUtf8(README_PATH));
|
|
1591
|
+
const doeSource = readUtf8(DOE_JS_PATH);
|
|
1592
|
+
const dtsSource = readUtf8(DOE_DTS_PATH);
|
|
1593
|
+
const apiEntries = buildApiEntries(doeSource, dtsSource);
|
|
1594
|
+
const examples = readExamples();
|
|
1595
|
+
|
|
1596
|
+
const html = buildPage({
|
|
1597
|
+
intro,
|
|
1598
|
+
apiEntries,
|
|
1599
|
+
examples,
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
1603
|
+
writeFileSync(OUTPUT_PATH, html);
|
|
1604
|
+
process.stdout.write(`${OUTPUT_PATH}\n`);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
main();
|