@field/cascade 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +14 -9
- package/ARCHITECTURE.md +22 -6
- package/CHANGELOG.md +51 -1
- package/CONTRIBUTING.md +71 -0
- package/DESIGN.md +19 -13
- package/README.md +42 -11
- package/dist/assets/{CodeEditor-D22fVi-4.js → CodeEditor-BNOJIDzL.js} +4 -4
- package/dist/assets/{CodePanel-DPNEYfcR.js → CodePanel-2wp2G0Fj.js} +2 -2
- package/dist/assets/{CustomNodeDialog-DeHCAvgb.js → CustomNodeDialog-DNqaVBqA.js} +1 -1
- package/dist/assets/{ProjectSettings-DWpVtWEx.js → ProjectSettings-DcdYF2E1.js} +1 -1
- package/dist/assets/{SettingsDialog-za00IS3L.js → SettingsDialog-D2xVvyiR.js} +1 -1
- package/dist/assets/{VersionHistoryDialog-DTMGhI0_.js → VersionHistoryDialog-DW288ykF.js} +1 -1
- package/dist/assets/{core-JsyKMO1k.js → core-B1y38i95.js} +1 -1
- package/dist/assets/{cssMode-emdRheFK.js → cssMode-BLucnaln.js} +1 -1
- package/dist/assets/{editor.api-xHX-fMSo.js → editor.api-B3Et3Pph.js} +1 -1
- package/dist/assets/{freemarker2-C3M8_Gs3.js → freemarker2-DVlPoano.js} +1 -1
- package/dist/assets/geo-DhyFdBXX.js +1 -0
- package/dist/assets/{handlebars-CBMurtFk.js → handlebars-B5FZBAMR.js} +1 -1
- package/dist/assets/{html-xbLQwknw.js → html-FodD8KbR.js} +1 -1
- package/dist/assets/{htmlMode-2wKdDTon.js → htmlMode-CsG57VCD.js} +1 -1
- package/dist/assets/{image-i1j6nWKe.js → image-B3F-nq25.js} +1 -1
- package/dist/assets/index-CC6YQw2n.js +212 -0
- package/dist/assets/index-Y2ock1cw.css +1 -0
- package/dist/assets/{javascript-DJaOo_2I.js → javascript-B3gonF_H.js} +1 -1
- package/dist/assets/{jsonMode-Bheas8_0.js → jsonMode-Y3w1ahZg.js} +1 -1
- package/dist/assets/{liquid-BCj_BGbM.js → liquid-CTpWXIYO.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-BfIsx_xz.js → lspLanguageFeatures-CODBdKEZ.js} +1 -1
- package/dist/assets/{mdx-DwiQDiVO.js → mdx-CDAQJZIz.js} +1 -1
- package/dist/assets/{python-BjrB5nqj.js → python-LOIlxJU-.js} +1 -1
- package/dist/assets/{razor-CfoBmGDs.js → razor-CllPQSHo.js} +1 -1
- package/dist/assets/{tsMode-BcEoMSxp.js → tsMode-CO28ut8t.js} +1 -1
- package/dist/assets/{typescript-DIg7TEmQ.js → typescript-ruU9wKQ_.js} +1 -1
- package/dist/assets/{workers-vXYuwtPw.js → workers-Bc1Ywp4-.js} +1 -1
- package/dist/assets/{xml-DexLQIQy.js → xml-DoXP2Dh1.js} +1 -1
- package/dist/assets/{yaml-DUnD_1dE.js → yaml-D9YPyGiJ.js} +1 -1
- package/dist/cli/index.js +999 -185
- package/dist/cli/runtime/gpu.ts +71 -0
- 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 +41 -2
- package/dist/contracts/index.d.ts +2 -0
- package/dist/contracts/index.js +2 -0
- package/dist/contracts/schema.js +25 -2
- package/dist/contracts/series.d.ts +75 -0
- package/dist/contracts/series.js +2 -0
- package/dist/contracts/validate.js +19 -3
- package/dist/contracts/values.d.ts +3 -2
- package/dist/contracts/values.js +1 -0
- package/dist/gpu.d.ts +17 -0
- package/dist/gpu.js +61 -1
- package/dist/index.html +2 -2
- 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 +2 -1
- package/dist/runtime/builtins/core/index.js +3 -1
- 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/gpu.d.ts +33 -0
- package/dist/runtime/gpu.js +218 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/runtime.js +63 -6
- 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/doc/HEADLESS_GPU.md +160 -0
- package/doc/NODE_REFERENCE.md +36 -0
- package/doc/PROJECT_AUTHORING.md +106 -20
- package/package.json +10 -2
- package/dist/assets/geo-BAR6lQp5.js +0 -1
- package/dist/assets/index-BDV5xGBZ.css +0 -1
- package/dist/assets/index-CMcH8AgP.js +0 -212
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything derived from a `camera`, so that no renderer derives it twice.
|
|
3
|
+
*
|
|
4
|
+
* The type stores intent — a transform, a lens, a frame — and every consumer
|
|
5
|
+
* wants something else: a rasteriser wants matrices, a raymarcher wants the
|
|
6
|
+
* basis vectors directly, and a UI wants the angle in degrees. Deriving those
|
|
7
|
+
* here is the same argument as `resolvePropBinding`: one implementation, or the
|
|
8
|
+
* second one disagrees with the first about handedness in a way that takes an
|
|
9
|
+
* afternoon to find.
|
|
10
|
+
*
|
|
11
|
+
* Houdini's conventions throughout: the camera looks down its own **-Z** with
|
|
12
|
+
* **+Y** up, rotation is degrees applied in **XYZ** order, and the vertical
|
|
13
|
+
* aperture is derived from the horizontal one and the frame shape rather than
|
|
14
|
+
* stored.
|
|
15
|
+
*
|
|
16
|
+
* **Matching a frame is one number, never two.** Both angles come from the same
|
|
17
|
+
* `focal`, `aperture` and resolution ratio, so a `focal` chosen to reproduce a
|
|
18
|
+
* known horizontal angle reproduces the vertical one at the same time. Worked
|
|
19
|
+
* example, from converting `cloud-volumes` off its hand-rolled projection on
|
|
20
|
+
* 2026-09-08: that sketch pinned its **vertical** half-tangent at `0.36` and
|
|
21
|
+
* scaled the horizontal by aspect, which on a 1098×1512 page gave a 29.3°
|
|
22
|
+
* horizontal angle; `focal = (aperture / 2) / 0.261 ≈ 79.2 mm` reproduces it,
|
|
23
|
+
* and the vertical lands back on `0.36` without being asked. Which is also the
|
|
24
|
+
* reason that sketch's `fit` parameter had to go — it backed the camera off to
|
|
25
|
+
* win back horizontal coverage a portrait page had lost, and that is the same
|
|
26
|
+
* compensation this module already does in the projection.
|
|
27
|
+
*/
|
|
28
|
+
const DEG = Math.PI / 180;
|
|
29
|
+
function cross(a, b) {
|
|
30
|
+
return [
|
|
31
|
+
a[1] * b[2] - a[2] * b[1],
|
|
32
|
+
a[2] * b[0] - a[0] * b[2],
|
|
33
|
+
a[0] * b[1] - a[1] * b[0],
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
function normalize(v) {
|
|
37
|
+
const length = Math.hypot(v[0], v[1], v[2]);
|
|
38
|
+
if (length === 0)
|
|
39
|
+
return [0, 0, 0];
|
|
40
|
+
return [v[0] / length, v[1] / length, v[2] / length];
|
|
41
|
+
}
|
|
42
|
+
function subtract(a, b) {
|
|
43
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
44
|
+
}
|
|
45
|
+
function dot(a, b) {
|
|
46
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The rotation the camera needs to look at a point, as Houdini's look-at does.
|
|
50
|
+
*
|
|
51
|
+
* Returned as degrees in XYZ order so it can be written straight into
|
|
52
|
+
* `camera.rotate` — which is what makes look-at an override of the transform
|
|
53
|
+
* rather than a second, parallel way to orient a camera.
|
|
54
|
+
*
|
|
55
|
+
* **`up` chooses the roll, and only the roll.** Yaw and pitch are fixed the
|
|
56
|
+
* moment a direction is given; all an up vector can still decide is the
|
|
57
|
+
* rotation about that direction. The default `[0, 1, 0]` is Houdini's own
|
|
58
|
+
* `upvector` default and gives roll zero for any horizontal up, which is why
|
|
59
|
+
* every ordinary orbit never needs to think about it.
|
|
60
|
+
*
|
|
61
|
+
* Written first with `roll = 0` and the parameter ignored, which was wrong in
|
|
62
|
+
* the way this codebase keeps finding: the node exposes `up` as an input, so a
|
|
63
|
+
* value set there did nothing at all and nothing said so.
|
|
64
|
+
*/
|
|
65
|
+
export function lookAtRotation(from, to, up = [0, 1, 0]) {
|
|
66
|
+
const forward = normalize(subtract(to, from));
|
|
67
|
+
if (forward[0] === 0 && forward[1] === 0 && forward[2] === 0)
|
|
68
|
+
return [0, 0, 0];
|
|
69
|
+
// Pitch first, because yaw is measured in the horizontal plane and the
|
|
70
|
+
// horizontal length is what is left of the vector once pitch is taken out.
|
|
71
|
+
const horizontal = Math.hypot(forward[0], forward[2]);
|
|
72
|
+
const pitch = Math.atan2(forward[1], horizontal);
|
|
73
|
+
// Looking down -Z at rest, so a forward of (0, 0, -1) must give a yaw of 0.
|
|
74
|
+
const yaw = Math.atan2(-forward[0], -forward[2]);
|
|
75
|
+
return [pitch / DEG, yaw / DEG, rollTowards(forward, pitch, yaw, up) / DEG];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The rotation about `forward` that takes the world-up-locked up vector onto
|
|
79
|
+
* the requested one.
|
|
80
|
+
*
|
|
81
|
+
* Only the component of `up` perpendicular to `forward` can be honoured — the
|
|
82
|
+
* parallel component is asking the camera to look along its own up, which is
|
|
83
|
+
* not a rotation. So it is projected out, and an up parallel to forward leaves
|
|
84
|
+
* nothing to aim at and returns zero rather than a NaN.
|
|
85
|
+
*/
|
|
86
|
+
function rollTowards(forward, pitch, yaw, up) {
|
|
87
|
+
const desired = normalize([
|
|
88
|
+
up[0] - forward[0] * dot(up, forward),
|
|
89
|
+
up[1] - forward[1] * dot(up, forward),
|
|
90
|
+
up[2] - forward[2] * dot(up, forward),
|
|
91
|
+
]);
|
|
92
|
+
if (desired[0] === 0 && desired[1] === 0 && desired[2] === 0)
|
|
93
|
+
return 0;
|
|
94
|
+
// The up this camera has with no roll, taken from the basis rather than
|
|
95
|
+
// rebuilt, so the two cannot drift apart.
|
|
96
|
+
const locked = cameraBasis({
|
|
97
|
+
...CAMERA_ROTATION_ONLY,
|
|
98
|
+
rotate: [pitch / DEG, yaw / DEG, 0],
|
|
99
|
+
}).up;
|
|
100
|
+
// `cross(desired, locked)` rather than the other way round: a positive
|
|
101
|
+
// `rotate.z` carries the camera's up toward its own -X, so the angle that
|
|
102
|
+
// takes `locked` onto `desired` runs the opposite way.
|
|
103
|
+
return Math.atan2(dot(cross(desired, locked), forward), dot(locked, desired));
|
|
104
|
+
}
|
|
105
|
+
/** Only `rotate` is read by `cameraBasis`, so the rest is filler rather than a
|
|
106
|
+
* claim about any real camera. */
|
|
107
|
+
const CAMERA_ROTATION_ONLY = {
|
|
108
|
+
translate: [0, 0, 0],
|
|
109
|
+
focal: 50,
|
|
110
|
+
aperture: 41.4214,
|
|
111
|
+
near: 0.001,
|
|
112
|
+
far: 10000,
|
|
113
|
+
resolution: [1, 1],
|
|
114
|
+
aspect: 1,
|
|
115
|
+
projection: "perspective",
|
|
116
|
+
orthowidth: 2,
|
|
117
|
+
};
|
|
118
|
+
/** The camera's axes, applying Houdini's XYZ rotation order to a camera that
|
|
119
|
+
* looks down -Z at rest. */
|
|
120
|
+
export function cameraBasis(camera) {
|
|
121
|
+
const [rx, ry, rz] = camera.rotate;
|
|
122
|
+
const sx = Math.sin(rx * DEG);
|
|
123
|
+
const cx = Math.cos(rx * DEG);
|
|
124
|
+
const sy = Math.sin(ry * DEG);
|
|
125
|
+
const cy = Math.cos(ry * DEG);
|
|
126
|
+
const sz = Math.sin(rz * DEG);
|
|
127
|
+
const cz = Math.cos(rz * DEG);
|
|
128
|
+
/**
|
|
129
|
+
* `R = Ry * Rx * Rz` on a column vector: yaw about world +Y, then pitch
|
|
130
|
+
* about the camera's own right, then roll about its own view direction.
|
|
131
|
+
*
|
|
132
|
+
* This is the third order tried and the reason is worth writing down, because
|
|
133
|
+
* two of them are indistinguishable by almost every test. With `rotate.z` at
|
|
134
|
+
* zero all three agree exactly — so yaw, pitch, look-at and orthonormality
|
|
135
|
+
* passed under each, and only the roll separates them. Under `Rz * Ry * Rx`,
|
|
136
|
+
* written first, `rotate.z` turns the camera about the WORLD z axis, which
|
|
137
|
+
* looks like a roll for as long as the camera faces -Z and stops being one
|
|
138
|
+
* the moment it does not. Under `Rx * Ry * Rz` the yaw stops being a yaw
|
|
139
|
+
* about world up, which is what an orbit needs. This order is the one every
|
|
140
|
+
* camera rig uses, and it is what banking a camera means.
|
|
141
|
+
*/
|
|
142
|
+
const m00 = cy * cz + sy * sx * sz;
|
|
143
|
+
const m01 = -cy * sz + sy * sx * cz;
|
|
144
|
+
const m02 = sy * cx;
|
|
145
|
+
const m10 = cx * sz;
|
|
146
|
+
const m11 = cx * cz;
|
|
147
|
+
const m12 = -sx;
|
|
148
|
+
const m20 = -sy * cz + cy * sx * sz;
|
|
149
|
+
const m21 = sy * sz + cy * sx * cz;
|
|
150
|
+
const m22 = cy * cx;
|
|
151
|
+
const right = [m00, m10, m20];
|
|
152
|
+
const up = [m01, m11, m21];
|
|
153
|
+
// Its own -Z, which is the whole convention in one line.
|
|
154
|
+
const forward = [-m02, -m12, -m22];
|
|
155
|
+
return { eye: camera.translate, forward, right, up };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The horizontal field of view in degrees, from the lens rather than stored.
|
|
159
|
+
*
|
|
160
|
+
* Houdini's defaults of 50 mm and 41.4214 mm are exactly 45°, which is the
|
|
161
|
+
* check worth having in a test: it is the one value that proves the formula and
|
|
162
|
+
* the defaults agree.
|
|
163
|
+
*/
|
|
164
|
+
export function horizontalFov(camera) {
|
|
165
|
+
if (camera.focal === 0)
|
|
166
|
+
return 0;
|
|
167
|
+
return (2 * Math.atan(camera.aperture / (2 * camera.focal))) / DEG;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The VERTICAL field of view in degrees.
|
|
171
|
+
*
|
|
172
|
+
* Houdini does not store a vertical aperture; it derives one from the
|
|
173
|
+
* horizontal aperture, the resolution ratio and the pixel aspect. Which is why
|
|
174
|
+
* `resolution` is on the camera at all — without it there is no vertical angle
|
|
175
|
+
* to be had.
|
|
176
|
+
*/
|
|
177
|
+
export function verticalFov(camera) {
|
|
178
|
+
if (camera.focal === 0)
|
|
179
|
+
return 0;
|
|
180
|
+
const [width, height] = camera.resolution;
|
|
181
|
+
if (width === 0 || camera.aspect === 0)
|
|
182
|
+
return 0;
|
|
183
|
+
const verticalAperture = (camera.aperture * height) / (width * camera.aspect);
|
|
184
|
+
return (2 * Math.atan(verticalAperture / (2 * camera.focal))) / DEG;
|
|
185
|
+
}
|
|
186
|
+
/** Frame aspect — the shape of the picture, pixel aspect included. Distinct
|
|
187
|
+
* from `camera.aspect`, which is the aspect of one pixel. */
|
|
188
|
+
export function frameAspect(camera) {
|
|
189
|
+
const [width, height] = camera.resolution;
|
|
190
|
+
if (height === 0)
|
|
191
|
+
return 1;
|
|
192
|
+
return (width * camera.aspect) / height;
|
|
193
|
+
}
|
|
194
|
+
/** Column-major, GLSL convention, matching `MATRIX_TYPES` in Studio: element 0
|
|
195
|
+
* starts the first COLUMN. */
|
|
196
|
+
export function viewMatrix(camera) {
|
|
197
|
+
const { eye, forward, right, up } = cameraBasis(camera);
|
|
198
|
+
// The view matrix is the inverse of the camera's transform, and for a
|
|
199
|
+
// rotation that inverse is the transpose — so the basis vectors become rows.
|
|
200
|
+
const back = [-forward[0], -forward[1], -forward[2]];
|
|
201
|
+
return [
|
|
202
|
+
right[0], up[0], back[0], 0,
|
|
203
|
+
right[1], up[1], back[1], 0,
|
|
204
|
+
right[2], up[2], back[2], 0,
|
|
205
|
+
-(right[0] * eye[0] + right[1] * eye[1] + right[2] * eye[2]),
|
|
206
|
+
-(up[0] * eye[0] + up[1] * eye[1] + up[2] * eye[2]),
|
|
207
|
+
-(back[0] * eye[0] + back[1] * eye[1] + back[2] * eye[2]),
|
|
208
|
+
1,
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
/** Column-major, and orthographic when the camera says so — `orthowidth` is a
|
|
212
|
+
* width in world units, so the height comes from the frame aspect. */
|
|
213
|
+
export function projectionMatrix(camera) {
|
|
214
|
+
const { near, far } = camera;
|
|
215
|
+
const aspect = frameAspect(camera);
|
|
216
|
+
if (camera.projection === "orthographic") {
|
|
217
|
+
const halfWidth = camera.orthowidth / 2;
|
|
218
|
+
const halfHeight = aspect === 0 ? halfWidth : halfWidth / aspect;
|
|
219
|
+
return [
|
|
220
|
+
1 / halfWidth, 0, 0, 0,
|
|
221
|
+
0, 1 / halfHeight, 0, 0,
|
|
222
|
+
0, 0, -2 / (far - near), 0,
|
|
223
|
+
0, 0, -(far + near) / (far - near), 1,
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
const focalScale = (2 * camera.focal) / camera.aperture;
|
|
227
|
+
return [
|
|
228
|
+
focalScale, 0, 0, 0,
|
|
229
|
+
0, focalScale * aspect, 0, 0,
|
|
230
|
+
0, 0, -(far + near) / (far - near), -1,
|
|
231
|
+
0, 0, (-2 * far * near) / (far - near), 0,
|
|
232
|
+
];
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -16,9 +16,24 @@ const AMBIENT_STATE = new Set([
|
|
|
16
16
|
export function validateNodeModuleArchitecture(source, fileName, ts) {
|
|
17
17
|
const file = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
18
18
|
const diagnostics = [];
|
|
19
|
+
const exportsDefinition = file.statements.some((statement) => ts.isVariableStatement(statement) && isDefinition(statement));
|
|
19
20
|
for (const statement of file.statements) {
|
|
20
21
|
if (ts.isVariableStatement(statement) && !isDefinition(statement)) {
|
|
21
|
-
|
|
22
|
+
// `icon` and `runsOn` get their own sentence, because the general
|
|
23
|
+
// message sends you looking for the wrong thing. A dynamic node declares
|
|
24
|
+
// them at module level and must — they are read by a pass over the
|
|
25
|
+
// source. A definition-v1 node declares them *inside* the definition,
|
|
26
|
+
// and the moment it does, the module-level copies become exactly the
|
|
27
|
+
// module state this rule forbids.
|
|
28
|
+
//
|
|
29
|
+
// Found the hard way on 2026-09-08: the first literal stub written for a
|
|
30
|
+
// converted shared node failed here, and the message talks about pure
|
|
31
|
+
// functions and capabilities while the actual fix is to move two lines
|
|
32
|
+
// into the object above them. That is not guessable from the error.
|
|
33
|
+
const superseded = supersededByDefinition(statement);
|
|
34
|
+
add(statement, "module-state", superseded
|
|
35
|
+
? `\`${superseded}\` belongs inside the definition, not beside it — a definition-v1 node declares it as \`${superseded}: ...\` in \`export const definition\`, and a module-level copy is the module state this rule forbids. A dynamic node without a definition still declares it here.`
|
|
36
|
+
: "Node modules may not declare module-level values; use a pure function or pass state through ports, props, and declared capabilities");
|
|
22
37
|
}
|
|
23
38
|
else if (ts.isClassDeclaration(statement) ||
|
|
24
39
|
ts.isEnumDeclaration(statement) ||
|
|
@@ -62,6 +77,26 @@ export function validateNodeModuleArchitecture(source, fileName, ts) {
|
|
|
62
77
|
(ts.isMethodDeclaration(parent) && parent.name === node) ||
|
|
63
78
|
(ts.isPropertyDeclaration(parent) && parent.name === node));
|
|
64
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* The name of a declaration a `definition` has taken over, or null.
|
|
82
|
+
*
|
|
83
|
+
* Only reported when the file actually exports a definition: without one,
|
|
84
|
+
* `export const icon` is the correct and only way to state it, and telling
|
|
85
|
+
* somebody to move it into an object that does not exist would be worse than
|
|
86
|
+
* the general message.
|
|
87
|
+
*/
|
|
88
|
+
function supersededByDefinition(statement) {
|
|
89
|
+
if (!exportsDefinition)
|
|
90
|
+
return null;
|
|
91
|
+
const declaration = statement.declarationList.declarations[0];
|
|
92
|
+
if (statement.declarationList.declarations.length !== 1)
|
|
93
|
+
return null;
|
|
94
|
+
if (!declaration || !ts.isIdentifier(declaration.name))
|
|
95
|
+
return null;
|
|
96
|
+
return declaration.name.text === "icon" || declaration.name.text === "runsOn"
|
|
97
|
+
? declaration.name.text
|
|
98
|
+
: null;
|
|
99
|
+
}
|
|
65
100
|
function isDefinition(statement) {
|
|
66
101
|
return Boolean(statement.declarationList.flags & ts.NodeFlags.Const) &&
|
|
67
102
|
Boolean(statement.modifiers?.some((item) => item.kind === ts.SyntaxKind.ExportKeyword)) &&
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { GpuCapability } from "@field/cascade/contracts";
|
|
2
|
+
export interface GpuHostAcquisition {
|
|
3
|
+
readonly device: GpuCapability["device"];
|
|
4
|
+
readonly info?: Partial<GpuCapability["adapterInfo"]>;
|
|
5
|
+
/** May be a getter-backed `GPUSupportedLimits`; normalized by the host. */
|
|
6
|
+
readonly limits: unknown;
|
|
7
|
+
/**
|
|
8
|
+
* Release provider-owned references after the device is no longer usable.
|
|
9
|
+
* Native providers need this in addition to `GPUDevice.destroy()` so their
|
|
10
|
+
* bindings can stop background work and allow the process to exit.
|
|
11
|
+
*/
|
|
12
|
+
readonly release?: () => void | Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export interface GpuHostOptions {
|
|
15
|
+
readonly acquire: () => Promise<GpuHostAcquisition>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A host-owned GPU device with page/process-wide and per-node cache scopes.
|
|
19
|
+
*
|
|
20
|
+
* The runtime can await `ensure()` before a cook while node implementations
|
|
21
|
+
* continue to receive the synchronous `GpuCapability` contract. Ownership
|
|
22
|
+
* remains with the injecting host: runtimes and graphs never dispose it.
|
|
23
|
+
*/
|
|
24
|
+
export interface GpuHost extends GpuCapability {
|
|
25
|
+
ensure(): Promise<void>;
|
|
26
|
+
forNode(nodeId: string): GpuCapability;
|
|
27
|
+
dispose(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare function isGpuHost(value: unknown): value is GpuHost;
|
|
30
|
+
export declare function createGpuHost(options: GpuHostOptions): GpuHost;
|
|
31
|
+
/** Getter-backed supported-limits objects cannot be normalized with spread. */
|
|
32
|
+
export declare function readGpuLimits(source: unknown): Record<string, number>;
|
|
33
|
+
//# sourceMappingURL=gpu.d.ts.map
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
const HOST_SCOPE = "\u0000host";
|
|
2
|
+
const MISSING_DEVICE = "The gpu capability was used before its device was ready — the host must await ensure() before a cook.";
|
|
3
|
+
const DISPOSED = "The gpu capability has been disposed.";
|
|
4
|
+
export function isGpuHost(value) {
|
|
5
|
+
return (!!value &&
|
|
6
|
+
typeof value.ensure === "function" &&
|
|
7
|
+
typeof value.forNode === "function" &&
|
|
8
|
+
typeof value.dispose === "function");
|
|
9
|
+
}
|
|
10
|
+
export function createGpuHost(options) {
|
|
11
|
+
let active = null;
|
|
12
|
+
let starting = null;
|
|
13
|
+
let disposal = null;
|
|
14
|
+
let disposed = false;
|
|
15
|
+
const caches = new Map();
|
|
16
|
+
const retiring = new Set();
|
|
17
|
+
const retirementErrors = [];
|
|
18
|
+
function dropCaches() {
|
|
19
|
+
const errors = [];
|
|
20
|
+
const scopes = [...caches.values()];
|
|
21
|
+
caches.clear();
|
|
22
|
+
for (const entries of scopes) {
|
|
23
|
+
for (const entry of entries.values()) {
|
|
24
|
+
try {
|
|
25
|
+
entry.destroy?.(entry.value);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
errors.push(error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return errors;
|
|
33
|
+
}
|
|
34
|
+
async function release(acquired, destroyDevice) {
|
|
35
|
+
if (acquired.released)
|
|
36
|
+
return [];
|
|
37
|
+
acquired.released = true;
|
|
38
|
+
const errors = [];
|
|
39
|
+
if (destroyDevice) {
|
|
40
|
+
try {
|
|
41
|
+
acquired.device.destroy();
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
errors.push(error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
await acquired.release?.();
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
errors.push(error);
|
|
52
|
+
}
|
|
53
|
+
return errors;
|
|
54
|
+
}
|
|
55
|
+
function throwCleanupErrors(errors) {
|
|
56
|
+
if (errors.length === 1)
|
|
57
|
+
throw errors[0];
|
|
58
|
+
if (errors.length > 1) {
|
|
59
|
+
const combined = new Error("GPU host cleanup failed");
|
|
60
|
+
Object.assign(combined, { errors });
|
|
61
|
+
throw combined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function watchLoss(acquired) {
|
|
65
|
+
void acquired.device.lost.then(() => {
|
|
66
|
+
if (disposed || active !== acquired)
|
|
67
|
+
return;
|
|
68
|
+
active = null;
|
|
69
|
+
retirementErrors.push(...dropCaches());
|
|
70
|
+
const cleanup = release(acquired, false).then((errors) => {
|
|
71
|
+
retirementErrors.push(...errors);
|
|
72
|
+
});
|
|
73
|
+
retiring.add(cleanup);
|
|
74
|
+
void cleanup.finally(() => retiring.delete(cleanup));
|
|
75
|
+
}).catch(() => {
|
|
76
|
+
// A rejected loss notification cannot be acted on safely. The active
|
|
77
|
+
// device remains usable until WebGPU reports otherwise or the owner
|
|
78
|
+
// disposes the host.
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async function start() {
|
|
82
|
+
const result = await options.acquire();
|
|
83
|
+
const acquired = {
|
|
84
|
+
device: result.device,
|
|
85
|
+
info: {
|
|
86
|
+
vendor: result.info?.vendor ?? "",
|
|
87
|
+
architecture: result.info?.architecture ?? "",
|
|
88
|
+
device: result.info?.device ?? "",
|
|
89
|
+
description: result.info?.description ?? "",
|
|
90
|
+
},
|
|
91
|
+
limits: readGpuLimits(result.limits),
|
|
92
|
+
release: result.release,
|
|
93
|
+
released: false,
|
|
94
|
+
};
|
|
95
|
+
if (disposed) {
|
|
96
|
+
const errors = await release(acquired, true);
|
|
97
|
+
throwCleanupErrors(errors);
|
|
98
|
+
throw new Error(DISPOSED);
|
|
99
|
+
}
|
|
100
|
+
active = acquired;
|
|
101
|
+
watchLoss(acquired);
|
|
102
|
+
}
|
|
103
|
+
const host = {
|
|
104
|
+
get device() {
|
|
105
|
+
if (!active)
|
|
106
|
+
throw new Error(MISSING_DEVICE);
|
|
107
|
+
return active.device;
|
|
108
|
+
},
|
|
109
|
+
get adapterInfo() {
|
|
110
|
+
if (!active)
|
|
111
|
+
throw new Error(MISSING_DEVICE);
|
|
112
|
+
return active.info;
|
|
113
|
+
},
|
|
114
|
+
get limits() {
|
|
115
|
+
return active?.limits ?? {};
|
|
116
|
+
},
|
|
117
|
+
cache(key, create, destroy) {
|
|
118
|
+
return host.forNode(HOST_SCOPE).cache(key, create, destroy);
|
|
119
|
+
},
|
|
120
|
+
async ensure() {
|
|
121
|
+
if (disposed)
|
|
122
|
+
throw new Error(DISPOSED);
|
|
123
|
+
if (active)
|
|
124
|
+
return;
|
|
125
|
+
if (!starting) {
|
|
126
|
+
const attempt = start();
|
|
127
|
+
const tracked = attempt.finally(() => {
|
|
128
|
+
if (starting === tracked)
|
|
129
|
+
starting = null;
|
|
130
|
+
});
|
|
131
|
+
starting = tracked;
|
|
132
|
+
}
|
|
133
|
+
await starting;
|
|
134
|
+
},
|
|
135
|
+
forNode(nodeId) {
|
|
136
|
+
return {
|
|
137
|
+
get device() {
|
|
138
|
+
if (!active)
|
|
139
|
+
throw new Error(MISSING_DEVICE);
|
|
140
|
+
return active.device;
|
|
141
|
+
},
|
|
142
|
+
get adapterInfo() {
|
|
143
|
+
if (!active)
|
|
144
|
+
throw new Error(MISSING_DEVICE);
|
|
145
|
+
return active.info;
|
|
146
|
+
},
|
|
147
|
+
get limits() {
|
|
148
|
+
return active?.limits ?? {};
|
|
149
|
+
},
|
|
150
|
+
cache(key, create, destroy) {
|
|
151
|
+
if (!active)
|
|
152
|
+
throw new Error(MISSING_DEVICE);
|
|
153
|
+
let entries = caches.get(nodeId);
|
|
154
|
+
if (!entries) {
|
|
155
|
+
entries = new Map();
|
|
156
|
+
caches.set(nodeId, entries);
|
|
157
|
+
}
|
|
158
|
+
const existing = entries.get(key);
|
|
159
|
+
if (existing)
|
|
160
|
+
return existing.value;
|
|
161
|
+
const value = create(active.device);
|
|
162
|
+
entries.set(key, {
|
|
163
|
+
value,
|
|
164
|
+
destroy: destroy,
|
|
165
|
+
});
|
|
166
|
+
return value;
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
dispose() {
|
|
171
|
+
disposal ??= performDispose();
|
|
172
|
+
return disposal;
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
async function performDispose() {
|
|
176
|
+
disposed = true;
|
|
177
|
+
const errors = dropCaches();
|
|
178
|
+
const pending = starting;
|
|
179
|
+
if (pending) {
|
|
180
|
+
try {
|
|
181
|
+
await pending;
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (!(error instanceof Error && error.message === DISPOSED))
|
|
185
|
+
errors.push(error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const acquired = active;
|
|
189
|
+
active = null;
|
|
190
|
+
if (acquired)
|
|
191
|
+
errors.push(...(await release(acquired, true)));
|
|
192
|
+
await Promise.all([...retiring]);
|
|
193
|
+
errors.push(...retirementErrors);
|
|
194
|
+
retirementErrors.length = 0;
|
|
195
|
+
throwCleanupErrors(errors);
|
|
196
|
+
}
|
|
197
|
+
return host;
|
|
198
|
+
}
|
|
199
|
+
/** Getter-backed supported-limits objects cannot be normalized with spread. */
|
|
200
|
+
export function readGpuLimits(source) {
|
|
201
|
+
if (!source || typeof source !== "object")
|
|
202
|
+
return {};
|
|
203
|
+
const names = new Set(Object.keys(source));
|
|
204
|
+
for (let cursor = Object.getPrototypeOf(source); cursor && cursor !== Object.prototype; cursor = Object.getPrototypeOf(cursor)) {
|
|
205
|
+
for (const key of Object.getOwnPropertyNames(cursor)) {
|
|
206
|
+
if (key !== "constructor")
|
|
207
|
+
names.add(key);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const limits = {};
|
|
211
|
+
for (const key of names) {
|
|
212
|
+
const value = source[key];
|
|
213
|
+
if (typeof value === "number")
|
|
214
|
+
limits[key] = value;
|
|
215
|
+
}
|
|
216
|
+
return limits;
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=gpu.js.map
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export { CascadeRuntimeError } from "./error.js";
|
|
2
2
|
export * from "./animation/index.js";
|
|
3
|
+
export * from "./camera/index.js";
|
|
4
|
+
export * from "./series/index.js";
|
|
3
5
|
export * from "./expressions/index.js";
|
|
4
6
|
export * from "./geometry/index.js";
|
|
7
|
+
export * from "./gpu.js";
|
|
5
8
|
export * from "./params/index.js";
|
|
6
9
|
export { createRuntime, PREFLIGHT_WARNING_CODES } from "./runtime.js";
|
|
7
10
|
export type * from "./types.js";
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export { CascadeRuntimeError } from "./error.js";
|
|
2
2
|
export * from "./animation/index.js";
|
|
3
|
+
export * from "./camera/index.js";
|
|
4
|
+
export * from "./series/index.js";
|
|
3
5
|
export * from "./expressions/index.js";
|
|
4
6
|
export * from "./geometry/index.js";
|
|
7
|
+
export * from "./gpu.js";
|
|
5
8
|
export * from "./params/index.js";
|
|
6
9
|
export { createRuntime, PREFLIGHT_WARNING_CODES } from "./runtime.js";
|
|
7
10
|
//# sourceMappingURL=index.js.map
|