@field/cascade 0.3.1 → 0.5.1
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/AGENTS.md +32 -9
- package/ARCHITECTURE.md +75 -13
- package/CHANGELOG.md +98 -1
- package/CONTRIBUTING.md +104 -0
- package/DESIGN.md +24 -15
- package/README.md +121 -14
- package/dist/assets/{CodeEditor-D22fVi-4.js → CodeEditor-dOrU3aQg.js} +7 -7
- package/dist/assets/{CodePanel-DPNEYfcR.js → CodePanel-Bqy85qen.js} +2 -2
- package/dist/assets/{CustomNodeDialog-DeHCAvgb.js → CustomNodeDialog-DkZlOgjs.js} +1 -1
- package/dist/assets/{ProjectSettings-DWpVtWEx.js → ProjectSettings-D192mr6g.js} +2 -2
- package/dist/assets/{SettingsDialog-za00IS3L.js → SettingsDialog-BTeeIE3T.js} +1 -1
- package/dist/assets/{VersionHistoryDialog-DTMGhI0_.js → VersionHistoryDialog-TZQKkn3Y.js} +1 -1
- package/dist/assets/{core-JsyKMO1k.js → core-DwbcGL0o.js} +2 -2
- package/dist/assets/{cssMode-emdRheFK.js → cssMode-Dcjn356x.js} +1 -1
- package/dist/assets/definitions-C_SY6Bhg.js +1 -0
- package/dist/assets/{editor.api-xHX-fMSo.js → editor.api-D7Q83606.js} +1 -1
- package/dist/assets/{freemarker2-C3M8_Gs3.js → freemarker2-DCCiHlV6.js} +1 -1
- package/dist/assets/geo-CT-wtvaE.js +1 -0
- package/dist/assets/{handlebars-CBMurtFk.js → handlebars-BCxC8xu1.js} +1 -1
- package/dist/assets/{html-xbLQwknw.js → html-COwepzSd.js} +1 -1
- package/dist/assets/{htmlMode-2wKdDTon.js → htmlMode-Dpg7H0rx.js} +1 -1
- package/dist/assets/{image-i1j6nWKe.js → image-CMfMMxlC.js} +3 -3
- package/dist/assets/index-De1cIZ9N.js +212 -0
- package/dist/assets/index-ilgbOu6G.css +1 -0
- package/dist/assets/{javascript-DJaOo_2I.js → javascript-CSwWxfLr.js} +1 -1
- package/dist/assets/{jsonMode-Bheas8_0.js → jsonMode-BXRriTpj.js} +1 -1
- package/dist/assets/{liquid-BCj_BGbM.js → liquid-Cmkfyogc.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-BfIsx_xz.js → lspLanguageFeatures-xH9JbZGO.js} +1 -1
- package/dist/assets/{mdx-DwiQDiVO.js → mdx-BzrNjNvK.js} +1 -1
- package/dist/assets/nodeTypeUtils-BDJsc2Ks.js +1 -0
- package/dist/assets/pop-BdKEHslJ.js +1 -0
- package/dist/assets/{python-BjrB5nqj.js → python-D4E9jbx7.js} +1 -1
- package/dist/assets/{razor-CfoBmGDs.js → razor-B3x9cIEM.js} +1 -1
- package/dist/assets/{tsMode-BcEoMSxp.js → tsMode-dhQYPNMZ.js} +1 -1
- package/dist/assets/{typescript-DIg7TEmQ.js → typescript-CITkDxAE.js} +1 -1
- package/dist/assets/{workers-vXYuwtPw.js → workers-BQvF9bbO.js} +1 -1
- package/dist/assets/{xml-DexLQIQy.js → xml-DvHlU3A8.js} +1 -1
- package/dist/assets/{yaml-DUnD_1dE.js → yaml-uznsLgs8.js} +1 -1
- package/dist/cli/index.js +16465 -12096
- package/dist/cli/runtime/gpu.ts +71 -0
- package/dist/cli/runtime/io.ts +31 -25
- package/dist/contracts/camera.d.ts +70 -0
- package/dist/contracts/camera.js +27 -0
- package/dist/contracts/capabilities.d.ts +6 -6
- package/dist/contracts/definition.d.ts +62 -4
- package/dist/contracts/index.d.ts +4 -0
- package/dist/contracts/index.js +4 -0
- package/dist/contracts/particles.d.ts +54 -0
- package/dist/contracts/particles.js +167 -0
- package/dist/contracts/scene.d.ts +129 -0
- package/dist/contracts/scene.js +73 -0
- package/dist/contracts/schema.js +26 -3
- package/dist/contracts/series.d.ts +75 -0
- package/dist/contracts/series.js +2 -0
- package/dist/contracts/validate.js +23 -5
- package/dist/contracts/values.d.ts +4 -2
- package/dist/contracts/values.js +10 -0
- package/dist/gpu.d.ts +17 -0
- package/dist/gpu.js +61 -1
- package/dist/index.html +3 -3
- package/dist/io.d.ts +4 -16
- package/dist/io.js +26 -8
- package/dist/player/app.js +810 -0
- package/dist/player/embed.d.ts +4 -0
- package/dist/player/embed.js +77 -0
- package/dist/player/types.d.ts +35 -0
- package/dist/runtime/builtins/core/camera.d.ts +252 -0
- package/dist/runtime/builtins/core/camera.js +149 -0
- package/dist/runtime/builtins/core/index.d.ts +4 -2
- package/dist/runtime/builtins/core/index.js +8 -2
- package/dist/runtime/builtins/core/structural.d.ts +168 -0
- package/dist/runtime/builtins/core/structural.js +175 -0
- package/dist/runtime/builtins/core/time.d.ts +165 -0
- package/dist/runtime/builtins/core/time.js +110 -0
- package/dist/runtime/builtins/geo/definitions.d.ts +247 -0
- package/dist/runtime/builtins/geo/definitions.js +70 -0
- package/dist/runtime/builtins/geo/index.d.ts +2 -1
- package/dist/runtime/builtins/geo/index.js +3 -1
- package/dist/runtime/builtins/geo/render.d.ts +87 -0
- package/dist/runtime/builtins/geo/render.js +87 -0
- package/dist/runtime/builtins/geo/svgExport.d.ts +11 -0
- package/dist/runtime/builtins/geo/svgExport.js +10 -0
- package/dist/runtime/builtins/index.d.ts +5 -2
- package/dist/runtime/builtins/index.js +13 -3
- package/dist/runtime/builtins/pop/definitions.d.ts +2120 -0
- package/dist/runtime/builtins/pop/definitions.js +815 -0
- package/dist/runtime/builtins/pop/index.d.ts +2 -0
- package/dist/runtime/builtins/pop/index.js +2 -0
- package/dist/runtime/builtins/scene/definitions.d.ts +356 -0
- package/dist/runtime/builtins/scene/definitions.js +224 -0
- package/dist/runtime/builtins/scene/index.d.ts +2 -0
- package/dist/runtime/builtins/scene/index.js +2 -0
- package/dist/runtime/camera/index.d.ts +60 -0
- package/dist/runtime/camera/index.js +234 -0
- package/dist/runtime/definition/architecture.js +36 -1
- package/dist/runtime/geometry/render.d.ts +109 -0
- package/dist/runtime/geometry/render.js +145 -0
- package/dist/runtime/geometry/svg.d.ts +29 -0
- package/dist/runtime/geometry/svg.js +11 -5
- package/dist/runtime/gpu.d.ts +33 -0
- package/dist/runtime/gpu.js +218 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/pop/cache.d.ts +27 -0
- package/dist/runtime/pop/cache.js +123 -0
- package/dist/runtime/pop/forces.d.ts +202 -0
- package/dist/runtime/pop/forces.js +519 -0
- package/dist/runtime/pop/index.d.ts +5 -0
- package/dist/runtime/pop/index.js +5 -0
- package/dist/runtime/pop/state.d.ts +136 -0
- package/dist/runtime/pop/state.js +283 -0
- package/dist/runtime/pop/trail.d.ts +62 -0
- package/dist/runtime/pop/trail.js +142 -0
- package/dist/runtime/runtime.js +284 -9
- package/dist/runtime/scene/index.d.ts +50 -0
- package/dist/runtime/scene/index.js +94 -0
- package/dist/runtime/series/index.d.ts +41 -0
- package/dist/runtime/series/index.js +167 -0
- package/dist/runtime/types.d.ts +1 -1
- package/dist/studio/panel.d.ts +68 -1
- package/doc/HEADLESS_GPU.md +160 -0
- package/doc/NODE_REFERENCE.md +408 -1
- package/doc/PROJECT_AUTHORING.md +118 -20
- package/doc/WEB_PLAYER.md +131 -0
- package/package.json +25 -3
- package/dist/assets/geo-BAR6lQp5.js +0 -1
- package/dist/assets/index-BDV5xGBZ.css +0 -1
- package/dist/assets/index-CMcH8AgP.js +0 -212
- package/dist/assets/nodeTypeUtils-BBA1JqHx.js +0 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cooking a graph once per parameter set.
|
|
3
|
+
*
|
|
4
|
+
* Step 1 of `PLAN series.md`, and headless on purpose: it is the foundation the
|
|
5
|
+
* gallery and the scheduler stand on, and it is verifiable without a picture.
|
|
6
|
+
*
|
|
7
|
+
* **The whole difficulty is that a parameter set is sparse.** `applyPreset`
|
|
8
|
+
* mutates the runtime, and instance 3 overriding `seed` while instance 4 does
|
|
9
|
+
* not means instance 4 inherits instance 3's seed unless something puts it
|
|
10
|
+
* back. That is accidental feedback — in the one mechanism specified as having
|
|
11
|
+
* none — and it would show up as a series where the pictures are subtly wrong
|
|
12
|
+
* in an order-dependent way, which is close to the worst failure this system
|
|
13
|
+
* could have: re-run it and the same instance renders differently, so it reads
|
|
14
|
+
* as a flaky renderer rather than as a bug.
|
|
15
|
+
*
|
|
16
|
+
* So every instance is cooked from the same baseline. The baseline is itself a
|
|
17
|
+
* `CascadePreset`, holding the graph's current value for **every key any
|
|
18
|
+
* instance touches** — the union, computed once — which means restoring is the
|
|
19
|
+
* same validated code path as applying, rather than a second one that can
|
|
20
|
+
* disagree with it.
|
|
21
|
+
*/
|
|
22
|
+
/** Every `node.input` and `node.prop` any instance in the series overrides. */
|
|
23
|
+
function overriddenKeys(series) {
|
|
24
|
+
const keys = new Map();
|
|
25
|
+
for (const instance of series.instances) {
|
|
26
|
+
for (const [nodeId, values] of Object.entries(instance.parameters.nodes)) {
|
|
27
|
+
let entry = keys.get(nodeId);
|
|
28
|
+
if (!entry) {
|
|
29
|
+
entry = { inputs: new Set(), props: new Set() };
|
|
30
|
+
keys.set(nodeId, entry);
|
|
31
|
+
}
|
|
32
|
+
for (const name of Object.keys(values.inputs ?? {}))
|
|
33
|
+
entry.inputs.add(name);
|
|
34
|
+
for (const name of Object.keys(values.props ?? {}))
|
|
35
|
+
entry.props.add(name);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return keys;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The graph's current values for those keys, as a preset.
|
|
42
|
+
*
|
|
43
|
+
* Read through `inspect()` rather than from the nodes directly, so the values
|
|
44
|
+
* are the same snapshots every other reader gets. A key the graph does not
|
|
45
|
+
* have is left out rather than defaulted: `applyPreset` is the thing that
|
|
46
|
+
* decides an unknown key is a misuse, and it should keep deciding that.
|
|
47
|
+
*/
|
|
48
|
+
export function seriesBaseline(graph, series) {
|
|
49
|
+
const wanted = overriddenKeys(series);
|
|
50
|
+
const inspection = graph.inspect();
|
|
51
|
+
const nodes = {};
|
|
52
|
+
for (const node of inspection.nodes) {
|
|
53
|
+
const entry = wanted.get(node.id);
|
|
54
|
+
if (!entry)
|
|
55
|
+
continue;
|
|
56
|
+
const inputs = {};
|
|
57
|
+
const props = {};
|
|
58
|
+
for (const name of entry.inputs) {
|
|
59
|
+
if (node.inputs[name] !== undefined)
|
|
60
|
+
inputs[name] = node.inputs[name].value;
|
|
61
|
+
}
|
|
62
|
+
for (const name of entry.props) {
|
|
63
|
+
if (node.props[name] !== undefined)
|
|
64
|
+
props[name] = node.props[name].value;
|
|
65
|
+
}
|
|
66
|
+
if (Object.keys(inputs).length > 0 || Object.keys(props).length > 0) {
|
|
67
|
+
nodes[node.id] = {
|
|
68
|
+
...(Object.keys(inputs).length > 0 ? { inputs } : {}),
|
|
69
|
+
...(Object.keys(props).length > 0 ? { props } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { version: 1, nodes };
|
|
74
|
+
}
|
|
75
|
+
/** Ids must be unique, or an instance has no address at all — and every
|
|
76
|
+
* judgement, override and cache key in the plan hangs off that address. */
|
|
77
|
+
export function validateSeries(series) {
|
|
78
|
+
if (series.version !== 1) {
|
|
79
|
+
throw new Error("Series version must be 1");
|
|
80
|
+
}
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
for (const instance of series.instances) {
|
|
83
|
+
if (!instance.id)
|
|
84
|
+
throw new Error("Every series instance needs an id");
|
|
85
|
+
if (seen.has(instance.id)) {
|
|
86
|
+
throw new Error(`Duplicate series instance id ${instance.id}`);
|
|
87
|
+
}
|
|
88
|
+
seen.add(instance.id);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function collect(graph, collection) {
|
|
92
|
+
return "graphOutput" in collection
|
|
93
|
+
? graph.getGraphOutput(collection.graphOutput)
|
|
94
|
+
: graph.getOutput(collection.nodeId, collection.output);
|
|
95
|
+
}
|
|
96
|
+
/** The cook order: the selected instance, then the rest in the series' order. */
|
|
97
|
+
function cookOrder(series, first) {
|
|
98
|
+
const order = series.instances.map((_, index) => index);
|
|
99
|
+
if (first === undefined)
|
|
100
|
+
return order;
|
|
101
|
+
const selected = series.instances.findIndex(instance => instance.id === first);
|
|
102
|
+
if (selected < 0)
|
|
103
|
+
return order;
|
|
104
|
+
return [selected, ...order.filter(index => index !== selected)];
|
|
105
|
+
}
|
|
106
|
+
export async function runSeries(graph, series, options = {}) {
|
|
107
|
+
validateSeries(series);
|
|
108
|
+
// Before the first `applyPreset` and not after, or the baseline is already
|
|
109
|
+
// instance one's.
|
|
110
|
+
const baseline = seriesBaseline(graph, series);
|
|
111
|
+
const results = new Array(series.instances.length);
|
|
112
|
+
let cancelled = false;
|
|
113
|
+
try {
|
|
114
|
+
for (const index of cookOrder(series, options.first)) {
|
|
115
|
+
if (options.signal?.aborted) {
|
|
116
|
+
cancelled = true;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
const instance = series.instances[index];
|
|
120
|
+
// Baseline first, every time. A sparse set does not clear what the
|
|
121
|
+
// previous instance changed, and inheriting it is accidental feedback.
|
|
122
|
+
await graph.applyPreset(baseline);
|
|
123
|
+
await graph.applyPreset(instance.parameters);
|
|
124
|
+
const run = await graph.run({
|
|
125
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
126
|
+
...(options.frame === undefined ? {} : { frame: options.frame }),
|
|
127
|
+
...(options.fps === undefined ? {} : { fps: options.fps }),
|
|
128
|
+
});
|
|
129
|
+
const result = {
|
|
130
|
+
id: instance.id,
|
|
131
|
+
index,
|
|
132
|
+
status: run.status,
|
|
133
|
+
// A run that did not complete has no value worth collecting, and
|
|
134
|
+
// reading the port anyway would hand back the previous instance's
|
|
135
|
+
// picture under this instance's id.
|
|
136
|
+
value: run.status === "completed" ? collect(graph, series.collect) : undefined,
|
|
137
|
+
diagnostics: run.diagnostics,
|
|
138
|
+
durationMs: run.timings.durationMs,
|
|
139
|
+
};
|
|
140
|
+
results[index] = result;
|
|
141
|
+
options.onInstance?.(result);
|
|
142
|
+
if (run.status === "cancelled") {
|
|
143
|
+
cancelled = true;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
// Leave the graph as it was found. A series is a way of looking at a graph,
|
|
150
|
+
// not an edit to it, and a Studio session that ran one and was left on the
|
|
151
|
+
// last instance's parameters would be indistinguishable from one the author
|
|
152
|
+
// had changed by hand.
|
|
153
|
+
await graph.applyPreset(baseline);
|
|
154
|
+
}
|
|
155
|
+
const completed = results.filter((result) => Boolean(result));
|
|
156
|
+
const status = cancelled
|
|
157
|
+
? "cancelled"
|
|
158
|
+
: completed.some(result => result.status === "failed")
|
|
159
|
+
? "failed"
|
|
160
|
+
: "completed";
|
|
161
|
+
return {
|
|
162
|
+
instances: completed,
|
|
163
|
+
byId: new Map(completed.map(result => [result.id, result])),
|
|
164
|
+
status,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=index.js.map
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -246,7 +246,7 @@ export interface LoadedCascadeGraph {
|
|
|
246
246
|
getOutputs(nodeId: string): ReadonlyMap<string, unknown>;
|
|
247
247
|
dispose(): Promise<void>;
|
|
248
248
|
}
|
|
249
|
-
export interface NodeRuntimeHostOptions extends
|
|
249
|
+
export interface NodeRuntimeHostOptions extends RuntimeCapabilities {
|
|
250
250
|
readonly modules: RuntimeHost["modules"];
|
|
251
251
|
readonly legacyShell?: LegacyShellBindingFactory & Readonly<{
|
|
252
252
|
kind: "server-direct";
|
package/dist/studio/panel.d.ts
CHANGED
|
@@ -8,9 +8,76 @@ export interface ProjectPanelMeta {
|
|
|
8
8
|
|
|
9
9
|
export interface ProjectPanelApi {
|
|
10
10
|
runStage<T = unknown>(stage: string, args: Record<string, unknown>): Promise<T>;
|
|
11
|
-
|
|
11
|
+
/** `version` busts a cache when a re-render reuses a path. It existed in
|
|
12
|
+
* core and not in this type, so a panel could not reach it. */
|
|
13
|
+
mediaUrl(path: string, options?: { width?: number; raw?: boolean; version?: string | number }): string;
|
|
14
|
+
/**
|
|
15
|
+
* A parameter's RESOLVED value — a keyframe channel first, then an
|
|
16
|
+
* expression, then the stored value. What the node's `execute` sees.
|
|
17
|
+
*
|
|
18
|
+
* Which means it is **not** the inverse of `setParam` when the parameter
|
|
19
|
+
* carries a binding. Use `getRawParam` to read back a write.
|
|
20
|
+
*/
|
|
12
21
|
getParam<T = unknown>(nodeId: string, name: string): T | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* A parameter's STORED value, before a channel or an expression resolves it.
|
|
24
|
+
*
|
|
25
|
+
* This is what `setParam` writes, so it is the one to read back after a
|
|
26
|
+
* write. Added 2026-09-09: a panel that wrote a value and re-read it with
|
|
27
|
+
* `getParam` got the resolved value, concluded its write had failed, and
|
|
28
|
+
* re-rendered a whole series at the parameter it had just replaced. Nothing
|
|
29
|
+
* threw and nothing logged.
|
|
30
|
+
*/
|
|
31
|
+
getRawParam<T = unknown>(nodeId: string, name: string): T | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Write a parameter's stored value.
|
|
34
|
+
*
|
|
35
|
+
* Clears a *declared default* expression, because a default expression is a
|
|
36
|
+
* default and the written value wins. **Throws when a channel or an
|
|
37
|
+
* author-written expression would shadow the write**, naming which — those
|
|
38
|
+
* legitimately win, and silently storing a value nobody will ever read is
|
|
39
|
+
* how a panel comes to believe a parameter is broken. Clear the binding
|
|
40
|
+
* first if the write is meant to take effect.
|
|
41
|
+
*/
|
|
13
42
|
setParam(nodeId: string, name: string, value: unknown): void;
|
|
43
|
+
/**
|
|
44
|
+
* A node's output value, read-only.
|
|
45
|
+
*
|
|
46
|
+
* Added 2026-09-09, and it is the one whose absence distorted a whole
|
|
47
|
+
* sketch. A panel could set parameters and not read results, so the series
|
|
48
|
+
* panel reached its own outputs through **two extra nodes and a stage**: a
|
|
49
|
+
* node whose only purpose was to write a value to disk, and a Python stage
|
|
50
|
+
* that cooked a graph so the panel could fetch it back. Both delete with
|
|
51
|
+
* this. A panel that can write and not read is half a surface.
|
|
52
|
+
*
|
|
53
|
+
* A snapshot rather than a live handle, and read-only on purpose: a panel
|
|
54
|
+
* that could mutate an output would be writing into the middle of the
|
|
55
|
+
* dependency graph, where nothing downstream would know.
|
|
56
|
+
*/
|
|
57
|
+
getOutput<T = unknown>(nodeId: string, outputName: string): T | undefined;
|
|
58
|
+
/** Every output of a node, by name. For a panel that does not know the
|
|
59
|
+
* shape of what it is looking at. */
|
|
60
|
+
getOutputs(nodeId: string): Readonly<Record<string, unknown>>;
|
|
61
|
+
/**
|
|
62
|
+
* Cook the graph, or one node's subtree, and resolve when it settles.
|
|
63
|
+
*
|
|
64
|
+
* A panel could ignore work through `api.signal` and could not start or stop
|
|
65
|
+
* any, so `PLAN series`' cancellable queue was not implementable from a panel
|
|
66
|
+
* at all.
|
|
67
|
+
*
|
|
68
|
+
* **There is deliberately no `background` option**, and finding out why cost
|
|
69
|
+
* an hour: cooking *the graph* in the background is incoherent, because the
|
|
70
|
+
* graph has one state. A background cook either merges with interactive work
|
|
71
|
+
* or fights it — and concretely, routing one through `scheduler.flush` made
|
|
72
|
+
* it cancel itself, since `flush` preempts the background lane by design.
|
|
73
|
+
* Measured, not reasoned: every such call returned `'preempted'`.
|
|
74
|
+
*
|
|
75
|
+
* The background lane is for cooking a *different parameter set*, which means
|
|
76
|
+
* driving nodes directly rather than through the shared queue. That belongs
|
|
77
|
+
* to the series mechanism rather than to a panel, and a panel will reach it
|
|
78
|
+
* through the series API instead.
|
|
79
|
+
*/
|
|
80
|
+
cook(options?: { readonly nodeId?: string }): Promise<void>;
|
|
14
81
|
selectedNodeId(): string | null;
|
|
15
82
|
readonly sourceNodeId: string | null;
|
|
16
83
|
readonly signal: AbortSignal;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Headless WebGPU with Dawn
|
|
2
|
+
|
|
3
|
+
The CLI can run definition-v1 nodes declaring `runsOn: 'portable'` or
|
|
4
|
+
`'server'` and `capabilities: ['gpu']` through optional `webgpu@0.6.0` (Dawn).
|
|
5
|
+
Studio uses browser WebGPU through the same device/cache lifecycle mechanism.
|
|
6
|
+
Browser-only declarations remain browser-only; installing Dawn does not supply
|
|
7
|
+
DOM, WebGL, canvas presentation, image decoding, or video APIs.
|
|
8
|
+
|
|
9
|
+
## Decision and release status
|
|
10
|
+
|
|
11
|
+
Implemented on 2026-09-08 as source version `0.3.2` and included in the 0.5
|
|
12
|
+
release; see the [changelog](../CHANGELOG.md). Use the project-local CLI and
|
|
13
|
+
record its version when comparing renders. A source checkout may contain
|
|
14
|
+
changes beyond the installed npm package.
|
|
15
|
+
|
|
16
|
+
The decision is to use optional native Dawn for browser-independent GPU
|
|
17
|
+
execution and agent image feedback. Browser automation remains useful for
|
|
18
|
+
Studio interaction/presentation tests; Skia remains the Canvas2D renderer.
|
|
19
|
+
Neither is replaced by Dawn. This first slice shares WGSL/rendering logic and
|
|
20
|
+
device/cache ownership without introducing a new graph texture pipeline or
|
|
21
|
+
requiring a browser process for production CLI rendering.
|
|
22
|
+
|
|
23
|
+
## Render for a test or agent
|
|
24
|
+
|
|
25
|
+
From a compatible project, using a Cascade build containing this feature:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npx --no-install cascade run index.cascade --frames 1 --json --timeout 60000
|
|
29
|
+
npx --no-install cascade run index.cascade --frames 1-24 --fps 24 --out renders --json --timeout 120000
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`--json` requires a fully definition-v1 frame render; use `--frames 1` for a
|
|
33
|
+
still. Stdout contains one success manifest with absolute graph path,
|
|
34
|
+
project-relative image `files`, `frames`, actual `fps`, elapsed `durationMs`,
|
|
35
|
+
and Dawn adapter metadata when a GPU was used. Logs and failure diagnostics go
|
|
36
|
+
to stderr; failures exit nonzero and do not emit a success manifest. This is a
|
|
37
|
+
local command contract, not a chat-provider integration or an untrusted-code sandbox.
|
|
38
|
+
|
|
39
|
+
Illustrative manifest for the `cloud-volumes/dawn.cascade` fixture:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"status": "completed",
|
|
44
|
+
"graph": "/path/to/cloud-volumes/dawn.cascade",
|
|
45
|
+
"frames": [1, 2],
|
|
46
|
+
"files": ["renders/dawn/volume.0001.png", "renders/dawn/volume.0002.png"],
|
|
47
|
+
"aborted": false,
|
|
48
|
+
"fps": 30,
|
|
49
|
+
"durationMs": 115.1,
|
|
50
|
+
"gpu": {
|
|
51
|
+
"renderer": "dawn",
|
|
52
|
+
"adapter": {
|
|
53
|
+
"vendor": "apple",
|
|
54
|
+
"architecture": "metal-3",
|
|
55
|
+
"device": "apple-m3-max",
|
|
56
|
+
"description": "Metal driver on macOS"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Resolve `files` relative to the directory containing `graph`. `gpu` is omitted
|
|
63
|
+
if no GPU node executed. `durationMs` measures the frame-rendering section and
|
|
64
|
+
cleanup, excluding CLI startup and module preparation; it is not GPU-pass time.
|
|
65
|
+
|
|
66
|
+
For agent callers: set a finite timeout, check the exit code, parse stdout only
|
|
67
|
+
on success, then attach the listed images to the conversation. Keep stderr for
|
|
68
|
+
diagnosis. A manifest records outputs, not a complete reproducibility bundle:
|
|
69
|
+
save the graph, code/assets and authored settings separately. The CLI does not
|
|
70
|
+
upload images or contact a model provider.
|
|
71
|
+
|
|
72
|
+
`--timeout` supervises a separate render process and returns exit 124 when its
|
|
73
|
+
deadline expires. On POSIX, termination covers that process group; on Windows,
|
|
74
|
+
the current supervisor terminates the direct child only. Interrupts are forwarded
|
|
75
|
+
with a short grace period before forced termination. A forced stop can leave
|
|
76
|
+
partial output files; consumers must check the exit code and manifest. Ordinary
|
|
77
|
+
successful runs destroy cached resources and release the native GPU reference.
|
|
78
|
+
|
|
79
|
+
Dawn loads lazily, only when a GPU node executes. Static `check`/`validate`
|
|
80
|
+
inspect the declared host contract without initializing hardware; they do not
|
|
81
|
+
prove that a native binary, driver, or adapter is usable. Missing native support
|
|
82
|
+
produces an installation diagnostic at execution. Dawn itself may be omitted
|
|
83
|
+
for CPU-only environments; retain the compiler's own platform dependencies.
|
|
84
|
+
Skia remains the independent Canvas2D path.
|
|
85
|
+
|
|
86
|
+
## Authoring contract
|
|
87
|
+
|
|
88
|
+
Use `context.capabilities.gpu.device` and `.cache(key, create, destroy)`.
|
|
89
|
+
Cache keys are scoped per node; destroy callbacks release textures and buffers.
|
|
90
|
+
The application owns the host's disposal, not an individual graph sharing it.
|
|
91
|
+
Use `createRenderPipelineAsync` to surface shader/pipeline validation failures.
|
|
92
|
+
`readTexture` captures validation failures within its own operation. Arbitrary
|
|
93
|
+
GPU commands issued before it need their own error scopes or async validation;
|
|
94
|
+
run-aware routing of all uncaptured device errors is not implemented yet.
|
|
95
|
+
|
|
96
|
+
Render into a single-sample `rgba8unorm` 2D texture with
|
|
97
|
+
`TextureUsage.RENDER_ATTACHMENT | TextureUsage.COPY_SRC`. Then:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { readTexture } from 'cascade/gpu';
|
|
101
|
+
|
|
102
|
+
const pixels = await readTexture(gpu.device, target, { signal: context.signal });
|
|
103
|
+
// pixels: { data: Uint8Array, width, height }, tightly packed top-left RGBA8
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Readback strips the WebGPU buffer row padding and copies bytes before unmapping.
|
|
107
|
+
It releases its staging buffer on success, mapping failure, or cancellation.
|
|
108
|
+
It does not encode PNGs or perform colour conversion. Encode through the existing
|
|
109
|
+
image/IO host and publish an `image` value. Graph-wide texture transport, HDR
|
|
110
|
+
readback, and browser video uploads are outside this first implementation.
|
|
111
|
+
|
|
112
|
+
## Verification and limits
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
CASCADE_TEST_DAWN=1 npx vitest run tests/dawn-native.test.ts
|
|
116
|
+
CASCADE_TEST_DAWN=1 npm run test:package
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
These opt-in hardware gates fail if Dawn cannot initialize. The first checks
|
|
120
|
+
repeated creation, rendering, padded readback and natural process exit. The
|
|
121
|
+
second installs a packed tarball into an empty project, renders animated PNGs
|
|
122
|
+
through the CLI and checks decoded pixels and the JSON manifest.
|
|
123
|
+
It also removes Dawn from that temporary installation to verify that a CPU
|
|
124
|
+
graph still runs and a GPU graph fails with an actionable missing-renderer
|
|
125
|
+
diagnostic. No silent CPU or browser fallback is performed.
|
|
126
|
+
|
|
127
|
+
Recorded implementation verification (2026-09-08):
|
|
128
|
+
|
|
129
|
+
- Main test suite: 1,322 passed; the opt-in hardware test was skipped there and
|
|
130
|
+
passed separately with `CASCADE_TEST_DAWN=1`.
|
|
131
|
+
- Typechecking: zero errors, eight existing Svelte warnings.
|
|
132
|
+
- Contracts/runtime package checks and tests, server/Studio/CLI builds, and
|
|
133
|
+
packed-package imports/types and rendering checks passed.
|
|
134
|
+
- The real `cloud-volumes/dawn.cascade` fixture passed static checks and wrote
|
|
135
|
+
two distinct animated PNGs with an adapter-bearing JSON manifest.
|
|
136
|
+
|
|
137
|
+
These are recorded results, not a claim that every future checkout or platform
|
|
138
|
+
passes. Rerun the gates after changes; Windows/Linux hardware has not yet been
|
|
139
|
+
verified. Windows process-tree cancellation remains follow-up work.
|
|
140
|
+
|
|
141
|
+
Do not require identical pixels across GPUs/drivers. Pin reference environments,
|
|
142
|
+
record adapter information and use measured image tolerances. Agent callers
|
|
143
|
+
should set a timeout, cap dimensions/frame counts, and preserve graph/code,
|
|
144
|
+
assets, seed and frame settings alongside selected outputs.
|
|
145
|
+
|
|
146
|
+
Initial proof (2026-09-08): the `cloud-volumes` shader at 130 × 96, 32 steps,
|
|
147
|
+
with a fixed opaque 17 × 73 column and 144-byte uniform block produced zero
|
|
148
|
+
changed channels across 49,920 channels for both original browser canvas →
|
|
149
|
+
migrated browser texture and migrated browser → native Dawn. All outputs were
|
|
150
|
+
nonblank. Both hosts used Apple Metal (native: M3 Max). This establishes that
|
|
151
|
+
specific migration's parity, not universal GPU bit-exactness. The sketch's
|
|
152
|
+
`dawn.cascade` additionally exercises the real Aoos input, image IO and animation
|
|
153
|
+
through the CLI at 256 × 320.
|
|
154
|
+
|
|
155
|
+
Native packaging and GPU/driver support vary by platform. The package chooses
|
|
156
|
+
its default backend; Cascade does not enable unsafe Dawn toggles or silently
|
|
157
|
+
configure software drivers. Software Vulkan requires separate host setup.
|
|
158
|
+
See [Dawn Node bindings](https://github.com/dawn-gpu/node-webgpu),
|
|
159
|
+
[software backend setup](https://dawn.googlesource.com/dawn/+/refs/heads/main/src/dawn/node/README.md),
|
|
160
|
+
and [WGSL floating-point rules](https://gpuweb.github.io/gpuweb/wgsl/#floating-point-evaluation).
|