@multiplekex/shallot 0.1.3 → 0.1.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplekex/shallot",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -13,7 +13,7 @@
13
13
  "rust/transforms/pkg"
14
14
  ],
15
15
  "scripts": {
16
- "build:wasm": "./rust/transforms/build.sh",
16
+ "build:wasm": "bash ./rust/transforms/build.sh",
17
17
  "test": "bun test"
18
18
  },
19
19
  "dependencies": {
@@ -2,6 +2,7 @@ import { State } from "./state";
2
2
  import type { Plugin, Loading } from "./types";
3
3
  import { toposort, type System } from "./scheduler";
4
4
  import { registerComponent } from "./component";
5
+ import { initRuntime } from "./runtime";
5
6
 
6
7
  export class StateBuilder {
7
8
  static defaultPlugins: readonly Plugin[] = [];
@@ -118,7 +119,6 @@ export class StateBuilder {
118
119
  }
119
120
 
120
121
  if (this._scenes.length > 0) {
121
- const { initRuntime } = await import("./runtime");
122
122
  const { loadSceneFile } = await import("./xml");
123
123
  const runtime = await initRuntime();
124
124
  for (const scenePath of this._scenes) {
package/src/core/math.ts CHANGED
@@ -1,9 +1,6 @@
1
1
  const DEG_TO_RAD = Math.PI / 180;
2
2
  const RAD_TO_DEG = 180 / Math.PI;
3
3
 
4
- const _quat = { x: 0, y: 0, z: 0, w: 1 };
5
- const _euler = { x: 0, y: 0, z: 0 };
6
-
7
4
  export function clamp(value: number, min: number, max: number): number {
8
5
  return value < min ? min : value > max ? max : value;
9
6
  }
@@ -46,11 +43,12 @@ export function slerp(
46
43
  s1 = sinTheta / sinTheta0;
47
44
  }
48
45
 
49
- _quat.x = s0 * fromX + s1 * toX;
50
- _quat.y = s0 * fromY + s1 * toY;
51
- _quat.z = s0 * fromZ + s1 * toZ;
52
- _quat.w = s0 * fromW + s1 * toW;
53
- return _quat;
46
+ return {
47
+ x: s0 * fromX + s1 * toX,
48
+ y: s0 * fromY + s1 * toY,
49
+ z: s0 * fromZ + s1 * toZ,
50
+ w: s0 * fromW + s1 * toW,
51
+ };
54
52
  }
55
53
 
56
54
  export function rotateQuaternion(
@@ -77,11 +75,12 @@ export function rotateQuaternion(
77
75
  const bz = cx * cy * sz + sx * sy * cz;
78
76
  const bw = cx * cy * cz - sx * sy * sz;
79
77
 
80
- _quat.x = qw * bx + qx * bw + qy * bz - qz * by;
81
- _quat.y = qw * by - qx * bz + qy * bw + qz * bx;
82
- _quat.z = qw * bz + qx * by - qy * bx + qz * bw;
83
- _quat.w = qw * bw - qx * bx - qy * by - qz * bz;
84
- return _quat;
78
+ return {
79
+ x: qw * bx + qx * bw + qy * bz - qz * by,
80
+ y: qw * by - qx * bz + qy * bw + qz * bx,
81
+ z: qw * bz + qx * by - qy * bx + qz * bw,
82
+ w: qw * bw - qx * bx - qy * by - qz * bz,
83
+ };
85
84
  }
86
85
 
87
86
  export function eulerToQuaternion(
@@ -99,11 +98,12 @@ export function eulerToQuaternion(
99
98
  const cz = Math.cos(hz),
100
99
  sz = Math.sin(hz);
101
100
 
102
- _quat.x = sx * cy * cz + cx * sy * sz;
103
- _quat.y = cx * sy * cz - sx * cy * sz;
104
- _quat.z = cx * cy * sz + sx * sy * cz;
105
- _quat.w = cx * cy * cz - sx * sy * sz;
106
- return _quat;
101
+ return {
102
+ x: sx * cy * cz + cx * sy * sz,
103
+ y: cx * sy * cz - sx * cy * sz,
104
+ z: cx * cy * sz + sx * sy * cz,
105
+ w: cx * cy * cz - sx * sy * sz,
106
+ };
107
107
  }
108
108
 
109
109
  export function quaternionToEuler(
@@ -129,14 +129,18 @@ export function quaternionToEuler(
129
129
  const ey = Math.asin(m13 < -1 ? -1 : m13 > 1 ? 1 : m13);
130
130
 
131
131
  if (m13 > -0.9999999 && m13 < 0.9999999) {
132
- _euler.x = Math.atan2(wx - yz, 1 - (xx + yy)) * RAD_TO_DEG;
133
- _euler.z = Math.atan2(wz - xy, 1 - (yy + zz)) * RAD_TO_DEG;
132
+ return {
133
+ x: Math.atan2(wx - yz, 1 - (xx + yy)) * RAD_TO_DEG,
134
+ y: ey * RAD_TO_DEG,
135
+ z: Math.atan2(wz - xy, 1 - (yy + zz)) * RAD_TO_DEG,
136
+ };
134
137
  } else {
135
- _euler.x = Math.atan2(yz + wx, 1 - (xx + zz)) * RAD_TO_DEG;
136
- _euler.z = 0;
138
+ return {
139
+ x: Math.atan2(yz + wx, 1 - (xx + zz)) * RAD_TO_DEG,
140
+ y: ey * RAD_TO_DEG,
141
+ z: 0,
142
+ };
137
143
  }
138
- _euler.y = ey * RAD_TO_DEG;
139
- return _euler;
140
144
  }
141
145
 
142
146
  export function lookAt(
@@ -150,59 +154,82 @@ export function lookAt(
150
154
  upY = 1,
151
155
  upZ = 0
152
156
  ): { x: number; y: number; z: number; w: number } {
153
- let fwdX = targetX - eyeX;
154
- let fwdY = targetY - eyeY;
155
- let fwdZ = targetZ - eyeZ;
156
- const fwdLenSq = fwdX * fwdX + fwdY * fwdY + fwdZ * fwdZ;
157
- if (fwdLenSq > 0) {
158
- const invLen = 1 / Math.sqrt(fwdLenSq);
159
- fwdX *= invLen;
160
- fwdY *= invLen;
161
- fwdZ *= invLen;
157
+ let zx = eyeX - targetX;
158
+ let zy = eyeY - targetY;
159
+ let zz = eyeZ - targetZ;
160
+ let zLen = Math.sqrt(zx * zx + zy * zy + zz * zz);
161
+
162
+ if (zLen === 0) {
163
+ zz = 1;
164
+ } else {
165
+ zLen = 1 / zLen;
166
+ zx *= zLen;
167
+ zy *= zLen;
168
+ zz *= zLen;
162
169
  }
163
170
 
164
- let rightX = upY * fwdZ - upZ * fwdY;
165
- let rightY = upZ * fwdX - upX * fwdZ;
166
- let rightZ = upX * fwdY - upY * fwdX;
167
- const rightLenSq = rightX * rightX + rightY * rightY + rightZ * rightZ;
168
- if (rightLenSq > 0) {
169
- const invLen = 1 / Math.sqrt(rightLenSq);
170
- rightX *= invLen;
171
- rightY *= invLen;
172
- rightZ *= invLen;
171
+ let xx = upY * zz - upZ * zy;
172
+ let xy = upZ * zx - upX * zz;
173
+ let xz = upX * zy - upY * zx;
174
+ let xLen = Math.sqrt(xx * xx + xy * xy + xz * xz);
175
+
176
+ if (xLen === 0) {
177
+ if (upZ !== 0) {
178
+ upX += 1e-4 * upX;
179
+ upY += 1e-4 * upY;
180
+ upZ += 1e-4 * upZ;
181
+ } else if (upY !== 0) {
182
+ upX += 1e-4 * upX;
183
+ upY += 1e-4 * upY;
184
+ upZ += 1e-4 * upZ;
185
+ } else {
186
+ upX += 1e-4 * upX;
187
+ upY += 1e-4 * upY;
188
+ upZ += 1e-4 * upZ;
189
+ }
190
+ xx = upY * zz - upZ * zy;
191
+ xy = upZ * zx - upX * zz;
192
+ xz = upX * zy - upY * zx;
193
+ xLen = Math.sqrt(xx * xx + xy * xy + xz * xz);
173
194
  }
174
195
 
175
- const newUpX = fwdY * rightZ - fwdZ * rightY;
176
- const newUpY = fwdZ * rightX - fwdX * rightZ;
177
- const newUpZ = fwdX * rightY - fwdY * rightX;
196
+ xLen = 1 / xLen;
197
+ xx *= xLen;
198
+ xy *= xLen;
199
+ xz *= xLen;
200
+
201
+ const yx = zy * xz - zz * xy;
202
+ const yy = zz * xx - zx * xz;
203
+ const yz = zx * xy - zy * xx;
178
204
 
179
- const trace = rightX + newUpY + fwdZ;
205
+ const trace = xx + yy + zz;
206
+ let qw: number, qx: number, qy: number, qz: number;
180
207
 
181
208
  if (trace > 0) {
182
209
  const s = 0.5 / Math.sqrt(trace + 1);
183
- _quat.w = 0.25 / s;
184
- _quat.x = (newUpZ - fwdY) * s;
185
- _quat.y = (fwdX - rightZ) * s;
186
- _quat.z = (rightY - newUpX) * s;
187
- } else if (rightX > newUpY && rightX > fwdZ) {
188
- const s = 2 * Math.sqrt(1 + rightX - newUpY - fwdZ);
189
- _quat.w = (newUpZ - fwdY) / s;
190
- _quat.x = 0.25 * s;
191
- _quat.y = (newUpX + rightY) / s;
192
- _quat.z = (fwdX + rightZ) / s;
193
- } else if (newUpY > fwdZ) {
194
- const s = 2 * Math.sqrt(1 + newUpY - rightX - fwdZ);
195
- _quat.w = (fwdX - rightZ) / s;
196
- _quat.x = (newUpX + rightY) / s;
197
- _quat.y = 0.25 * s;
198
- _quat.z = (fwdY + newUpZ) / s;
210
+ qw = 0.25 / s;
211
+ qx = (yz - zy) * s;
212
+ qy = (zx - xz) * s;
213
+ qz = (xy - yx) * s;
214
+ } else if (xx > yy && xx > zz) {
215
+ const s = 2 * Math.sqrt(1 + xx - yy - zz);
216
+ qw = (yz - zy) / s;
217
+ qx = 0.25 * s;
218
+ qy = (yx + xy) / s;
219
+ qz = (zx + xz) / s;
220
+ } else if (yy > zz) {
221
+ const s = 2 * Math.sqrt(1 + yy - xx - zz);
222
+ qw = (zx - xz) / s;
223
+ qx = (yx + xy) / s;
224
+ qy = 0.25 * s;
225
+ qz = (yz + zy) / s;
199
226
  } else {
200
- const s = 2 * Math.sqrt(1 + fwdZ - rightX - newUpY);
201
- _quat.w = (rightY - newUpX) / s;
202
- _quat.x = (fwdX + rightZ) / s;
203
- _quat.y = (fwdY + newUpZ) / s;
204
- _quat.z = 0.25 * s;
227
+ const s = 2 * Math.sqrt(1 + zz - xx - yy);
228
+ qw = (xy - yx) / s;
229
+ qx = (zx + xz) / s;
230
+ qy = (yz + zy) / s;
231
+ qz = 0.25 * s;
205
232
  }
206
233
 
207
- return _quat;
234
+ return { x: qx, y: qy, z: qz, w: qw };
208
235
  }
@@ -1,10 +1,16 @@
1
1
  import { setTraits } from "../../core/component";
2
- import { clamp, lookAt, type State, type System, type Plugin } from "../../core";
2
+ import { clamp, lookAt, type State, type System, type Plugin, type MouseState } from "../../core";
3
3
  import { Transform } from "../../standard/transforms";
4
4
  import { Input, InputPlugin } from "../../standard/input";
5
5
 
6
6
  const Tau = Math.PI * 2;
7
7
 
8
+ export const OrbitButton = {
9
+ Left: 0,
10
+ Middle: 1,
11
+ Right: 2,
12
+ } as const;
13
+
8
14
  export const Orbit = {
9
15
  target: [] as number[],
10
16
  yaw: [] as number[],
@@ -20,6 +26,7 @@ export const Orbit = {
20
26
  smoothness: [] as number[],
21
27
  sensitivity: [] as number[],
22
28
  zoomSpeed: [] as number[],
29
+ button: [] as number[],
23
30
  };
24
31
 
25
32
  setTraits(Orbit, {
@@ -38,6 +45,7 @@ setTraits(Orbit, {
38
45
  smoothness: 0.3,
39
46
  sensitivity: 0.005,
40
47
  zoomSpeed: 0.025,
48
+ button: OrbitButton.Right,
41
49
  }),
42
50
  });
43
51
 
@@ -54,6 +62,12 @@ function angleDiff(from: number, to: number): number {
54
62
  return diff > Math.PI ? diff - Tau : diff;
55
63
  }
56
64
 
65
+ function isOrbitButton(mouse: Readonly<MouseState>, button: number): boolean {
66
+ if (button === OrbitButton.Left) return mouse.left;
67
+ if (button === OrbitButton.Middle) return mouse.middle;
68
+ return mouse.right;
69
+ }
70
+
57
71
  export const OrbitSystem: System = {
58
72
  group: "simulation",
59
73
 
@@ -70,7 +84,7 @@ export const OrbitSystem: System = {
70
84
  const maxDistance = Orbit.maxDistance[eid];
71
85
  const smoothness = Orbit.smoothness[eid];
72
86
 
73
- if (input?.mouse.right) {
87
+ if (input && isOrbitButton(input.mouse, Orbit.button[eid])) {
74
88
  Orbit.targetYaw[eid] -= input.mouse.deltaX * sensitivity;
75
89
  Orbit.targetPitch[eid] = clamp(
76
90
  Orbit.targetPitch[eid] + input.mouse.deltaY * sensitivity,
@@ -1,4 +1,5 @@
1
1
  import { CycleError } from "../../core";
2
+ import { inspect as inspectGraph, type GraphInspection } from "./inspect";
2
3
 
3
4
  export type ResourceId = string;
4
5
  export type NodeId = string;
@@ -162,4 +163,8 @@ export class ComputeGraph {
162
163
  }
163
164
  return this._plan;
164
165
  }
166
+
167
+ inspect(): GraphInspection {
168
+ return inspectGraph(Array.from(this.nodes.values()));
169
+ }
165
170
  }
@@ -2,6 +2,9 @@ import { resource, type Plugin, type State, type System } from "../../core";
2
2
  import { ComputeGraph, type ExecutionContext, type ResourceId } from "./graph";
3
3
 
4
4
  export * from "./graph";
5
+ export * from "./inspect";
6
+ export * from "./readback";
7
+ export * from "./timing";
5
8
 
6
9
  const MIN_CANVAS_SIZE = 1;
7
10
 
@@ -0,0 +1,205 @@
1
+ import type { ComputeNode, NodeId, Phase, ResourceId, ResourceRef } from "./graph";
2
+
3
+ export interface NodeInfo {
4
+ readonly id: NodeId;
5
+ readonly phase: Phase;
6
+ readonly inputs: readonly ResourceRef[];
7
+ readonly outputs: readonly ResourceRef[];
8
+ }
9
+
10
+ export interface EdgeInfo {
11
+ readonly from: NodeId;
12
+ readonly to: NodeId;
13
+ readonly resource: ResourceId;
14
+ }
15
+
16
+ export interface GraphInspection {
17
+ readonly nodes: readonly NodeInfo[];
18
+ readonly edges: readonly EdgeInfo[];
19
+ readonly executionOrder: readonly NodeId[];
20
+ readonly byPhase: ReadonlyMap<Phase, readonly NodeId[]>;
21
+ }
22
+
23
+ const PHASE_ORDER: Phase[] = ["render", "overlay", "postprocess"];
24
+
25
+ export function inspect(nodes: readonly ComputeNode[]): GraphInspection {
26
+ const nodeInfos: NodeInfo[] = nodes.map((n) => ({
27
+ id: n.id,
28
+ phase: n.phase ?? "render",
29
+ inputs: n.inputs,
30
+ outputs: n.outputs,
31
+ }));
32
+
33
+ const producers = new Map<ResourceId, NodeId>();
34
+ for (const n of nodes) {
35
+ for (const out of n.outputs) {
36
+ producers.set(out.id, n.id);
37
+ }
38
+ }
39
+
40
+ const edges: EdgeInfo[] = [];
41
+ for (const n of nodes) {
42
+ for (const inp of n.inputs) {
43
+ const producer = producers.get(inp.id);
44
+ if (producer) {
45
+ edges.push({
46
+ from: producer,
47
+ to: n.id,
48
+ resource: inp.id,
49
+ });
50
+ }
51
+ }
52
+ }
53
+
54
+ const byPhase = new Map<Phase, NodeId[]>();
55
+ for (const phase of PHASE_ORDER) {
56
+ byPhase.set(phase, []);
57
+ }
58
+ for (const n of nodeInfos) {
59
+ byPhase.get(n.phase)!.push(n.id);
60
+ }
61
+
62
+ const executionOrder = topoSortIds(nodes);
63
+
64
+ return { nodes: nodeInfos, edges, executionOrder, byPhase };
65
+ }
66
+
67
+ function topoSortIds(nodes: readonly ComputeNode[]): NodeId[] {
68
+ if (nodes.length === 0) return [];
69
+
70
+ const producers = new Map<ResourceId, ComputeNode>();
71
+ for (const node of nodes) {
72
+ for (const output of node.outputs) {
73
+ producers.set(output.id, node);
74
+ }
75
+ }
76
+
77
+ const adjacency = new Map<ComputeNode, ComputeNode[]>();
78
+ const inDegree = new Map<ComputeNode, number>();
79
+
80
+ for (const node of nodes) {
81
+ adjacency.set(node, []);
82
+ inDegree.set(node, 0);
83
+ }
84
+
85
+ for (const node of nodes) {
86
+ for (const input of node.inputs) {
87
+ const producer = producers.get(input.id);
88
+ if (producer) {
89
+ adjacency.get(producer)!.push(node);
90
+ inDegree.set(node, inDegree.get(node)! + 1);
91
+ }
92
+ }
93
+ }
94
+
95
+ const byPhase = new Map<Phase, ComputeNode[]>();
96
+ for (const phase of PHASE_ORDER) {
97
+ byPhase.set(phase, []);
98
+ }
99
+ for (const node of nodes) {
100
+ const phase = node.phase ?? "render";
101
+ byPhase.get(phase)!.push(node);
102
+ }
103
+
104
+ const sorted: NodeId[] = [];
105
+ for (const phase of PHASE_ORDER) {
106
+ const phaseNodes = byPhase.get(phase)!;
107
+ const phaseInDegree = new Map<ComputeNode, number>();
108
+
109
+ for (const node of phaseNodes) {
110
+ let degree = 0;
111
+ for (const input of node.inputs) {
112
+ const producer = producers.get(input.id);
113
+ if (producer && phaseNodes.includes(producer)) {
114
+ degree++;
115
+ }
116
+ }
117
+ phaseInDegree.set(node, degree);
118
+ }
119
+
120
+ const queue: ComputeNode[] = [];
121
+ for (const node of phaseNodes) {
122
+ if (phaseInDegree.get(node) === 0) {
123
+ queue.push(node);
124
+ }
125
+ }
126
+
127
+ let i = 0;
128
+ while (i < queue.length) {
129
+ const node = queue[i++];
130
+ sorted.push(node.id);
131
+
132
+ for (const dep of phaseNodes) {
133
+ for (const input of dep.inputs) {
134
+ const producer = producers.get(input.id);
135
+ if (producer === node) {
136
+ const newDegree = phaseInDegree.get(dep)! - 1;
137
+ phaseInDegree.set(dep, newDegree);
138
+ if (newDegree === 0) {
139
+ queue.push(dep);
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ return sorted;
148
+ }
149
+
150
+ export function formatGraph(info: GraphInspection): string {
151
+ const lines: string[] = [];
152
+ lines.push("=== Compute Graph ===");
153
+ lines.push("");
154
+
155
+ const nodeMap = new Map(info.nodes.map((n) => [n.id, n]));
156
+
157
+ for (const phase of PHASE_ORDER) {
158
+ const nodeIds = info.byPhase.get(phase) ?? [];
159
+ if (nodeIds.length === 0) continue;
160
+
161
+ lines.push(`[${phase}]`);
162
+ for (const id of nodeIds) {
163
+ const node = nodeMap.get(id)!;
164
+ const ins = node.inputs.map((r) => r.id).join(", ") || "none";
165
+ const outs = node.outputs.map((r) => r.id).join(", ") || "none";
166
+ lines.push(` ${id}: ${ins} -> ${outs}`);
167
+ }
168
+ lines.push("");
169
+ }
170
+
171
+ lines.push("Execution Order:");
172
+ info.executionOrder.forEach((id, i) => {
173
+ lines.push(` ${i + 1}. ${id}`);
174
+ });
175
+
176
+ return lines.join("\n");
177
+ }
178
+
179
+ export function toDot(info: GraphInspection): string {
180
+ const lines: string[] = ["digraph ComputeGraph {"];
181
+ lines.push(" rankdir=LR;");
182
+ lines.push(" node [shape=box];");
183
+ lines.push("");
184
+
185
+ for (const phase of PHASE_ORDER) {
186
+ const nodeIds = info.byPhase.get(phase) ?? [];
187
+ if (nodeIds.length === 0) continue;
188
+
189
+ lines.push(` subgraph cluster_${phase} {`);
190
+ lines.push(` label="${phase}";`);
191
+ for (const id of nodeIds) {
192
+ lines.push(` "${id}";`);
193
+ }
194
+ lines.push(" }");
195
+ }
196
+
197
+ lines.push("");
198
+
199
+ for (const edge of info.edges) {
200
+ lines.push(` "${edge.from}" -> "${edge.to}" [label="${edge.resource}"];`);
201
+ }
202
+
203
+ lines.push("}");
204
+ return lines.join("\n");
205
+ }
@@ -0,0 +1,88 @@
1
+ export async function readBuffer(
2
+ device: GPUDevice,
3
+ source: GPUBuffer,
4
+ size: number
5
+ ): Promise<ArrayBuffer> {
6
+ const staging = device.createBuffer({
7
+ label: "staging-readback",
8
+ size,
9
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
10
+ });
11
+
12
+ const encoder = device.createCommandEncoder();
13
+ encoder.copyBufferToBuffer(source, 0, staging, 0, size);
14
+ device.queue.submit([encoder.finish()]);
15
+
16
+ await staging.mapAsync(GPUMapMode.READ);
17
+ const data = staging.getMappedRange().slice(0);
18
+ staging.unmap();
19
+ staging.destroy();
20
+
21
+ return data;
22
+ }
23
+
24
+ export async function readFloat32(
25
+ device: GPUDevice,
26
+ buffer: GPUBuffer,
27
+ count: number
28
+ ): Promise<Float32Array> {
29
+ const data = await readBuffer(device, buffer, count * 4);
30
+ return new Float32Array(data);
31
+ }
32
+
33
+ export async function readUint32(
34
+ device: GPUDevice,
35
+ buffer: GPUBuffer,
36
+ count: number
37
+ ): Promise<Uint32Array> {
38
+ const data = await readBuffer(device, buffer, count * 4);
39
+ return new Uint32Array(data);
40
+ }
41
+
42
+ function nextPowerOf2(n: number): number {
43
+ const min = 256;
44
+ return Math.pow(2, Math.ceil(Math.log2(Math.max(n, min))));
45
+ }
46
+
47
+ export class StagingPool {
48
+ private readonly pool = new Map<number, GPUBuffer[]>();
49
+ private readonly device: GPUDevice;
50
+
51
+ constructor(device: GPUDevice) {
52
+ this.device = device;
53
+ }
54
+
55
+ acquire(size: number): GPUBuffer {
56
+ const bucketSize = nextPowerOf2(size);
57
+ const bucket = this.pool.get(bucketSize);
58
+
59
+ if (bucket && bucket.length > 0) {
60
+ return bucket.pop()!;
61
+ }
62
+
63
+ return this.device.createBuffer({
64
+ label: `staging-pool-${bucketSize}`,
65
+ size: bucketSize,
66
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
67
+ });
68
+ }
69
+
70
+ release(buffer: GPUBuffer): void {
71
+ const bucketSize = buffer.size;
72
+ let bucket = this.pool.get(bucketSize);
73
+ if (!bucket) {
74
+ bucket = [];
75
+ this.pool.set(bucketSize, bucket);
76
+ }
77
+ bucket.push(buffer);
78
+ }
79
+
80
+ dispose(): void {
81
+ for (const bucket of this.pool.values()) {
82
+ for (const buffer of bucket) {
83
+ buffer.destroy();
84
+ }
85
+ }
86
+ this.pool.clear();
87
+ }
88
+ }
@@ -0,0 +1,139 @@
1
+ import type { NodeId } from "./graph";
2
+
3
+ export interface NodeTiming {
4
+ readonly nodeId: NodeId;
5
+ readonly cpuMs: number;
6
+ readonly gpuNs?: number;
7
+ }
8
+
9
+ export interface FrameTiming {
10
+ readonly frameIndex: number;
11
+ readonly nodes: readonly NodeTiming[];
12
+ readonly totalCpuMs: number;
13
+ readonly totalGpuNs?: number;
14
+ }
15
+
16
+ export interface TimingConfig {
17
+ readonly enabled: boolean;
18
+ readonly gpuTimestamps: boolean;
19
+ readonly historySize: number;
20
+ }
21
+
22
+ export interface TimingState {
23
+ readonly config: TimingConfig;
24
+ readonly history: FrameTiming[];
25
+ readonly querySet?: GPUQuerySet;
26
+ readonly resolveBuffer?: GPUBuffer;
27
+ readonly readBuffer?: GPUBuffer;
28
+ }
29
+
30
+ export function supportsTimestampQuery(device: GPUDevice): boolean {
31
+ return device.features.has("timestamp-query");
32
+ }
33
+
34
+ export function createTimingState(
35
+ device: GPUDevice,
36
+ maxNodes: number,
37
+ historySize = 60
38
+ ): TimingState {
39
+ const gpuTimestamps = supportsTimestampQuery(device);
40
+
41
+ let querySet: GPUQuerySet | undefined;
42
+ let resolveBuffer: GPUBuffer | undefined;
43
+ let readBuffer: GPUBuffer | undefined;
44
+
45
+ if (gpuTimestamps) {
46
+ const queryCount = maxNodes * 2;
47
+ querySet = device.createQuerySet({
48
+ label: "timing-queries",
49
+ type: "timestamp",
50
+ count: queryCount,
51
+ });
52
+
53
+ resolveBuffer = device.createBuffer({
54
+ label: "timing-resolve",
55
+ size: queryCount * 8,
56
+ usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
57
+ });
58
+
59
+ readBuffer = device.createBuffer({
60
+ label: "timing-read",
61
+ size: queryCount * 8,
62
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
63
+ });
64
+ }
65
+
66
+ return {
67
+ config: { enabled: true, gpuTimestamps, historySize },
68
+ history: [],
69
+ querySet,
70
+ resolveBuffer,
71
+ readBuffer,
72
+ };
73
+ }
74
+
75
+ export async function readTimestamps(
76
+ device: GPUDevice,
77
+ timing: TimingState,
78
+ nodeCount: number
79
+ ): Promise<BigUint64Array | null> {
80
+ if (!timing.querySet || !timing.resolveBuffer || !timing.readBuffer) {
81
+ return null;
82
+ }
83
+
84
+ const encoder = device.createCommandEncoder();
85
+ encoder.resolveQuerySet(timing.querySet, 0, nodeCount * 2, timing.resolveBuffer, 0);
86
+ encoder.copyBufferToBuffer(timing.resolveBuffer, 0, timing.readBuffer, 0, nodeCount * 2 * 8);
87
+ device.queue.submit([encoder.finish()]);
88
+
89
+ await timing.readBuffer.mapAsync(GPUMapMode.READ);
90
+ const data = new BigUint64Array(timing.readBuffer.getMappedRange().slice(0));
91
+ timing.readBuffer.unmap();
92
+
93
+ return data;
94
+ }
95
+
96
+ export function disposeTimingState(timing: TimingState): void {
97
+ timing.querySet?.destroy();
98
+ timing.resolveBuffer?.destroy();
99
+ timing.readBuffer?.destroy();
100
+ }
101
+
102
+ export interface TimingCollector {
103
+ readonly nodeTimings: Map<NodeId, { start: number; end: number }>;
104
+ beginNode(nodeId: NodeId): void;
105
+ endNode(nodeId: NodeId): void;
106
+ finish(frameIndex: number): FrameTiming;
107
+ }
108
+
109
+ export function createTimingCollector(): TimingCollector {
110
+ const nodeTimings = new Map<NodeId, { start: number; end: number }>();
111
+
112
+ return {
113
+ nodeTimings,
114
+
115
+ beginNode(nodeId: NodeId): void {
116
+ nodeTimings.set(nodeId, { start: performance.now(), end: 0 });
117
+ },
118
+
119
+ endNode(nodeId: NodeId): void {
120
+ const timing = nodeTimings.get(nodeId);
121
+ if (timing) {
122
+ timing.end = performance.now();
123
+ }
124
+ },
125
+
126
+ finish(frameIndex: number): FrameTiming {
127
+ const nodes: NodeTiming[] = [];
128
+ let totalCpuMs = 0;
129
+
130
+ for (const [nodeId, timing] of nodeTimings) {
131
+ const cpuMs = timing.end - timing.start;
132
+ totalCpuMs += cpuMs;
133
+ nodes.push({ nodeId, cpuMs });
134
+ }
135
+
136
+ return { frameIndex, nodes, totalCpuMs };
137
+ },
138
+ };
139
+ }
@@ -1,7 +1,7 @@
1
1
  import { setTraits } from "../../core/component";
2
2
  import { WorldTransform } from "../transforms";
3
3
  import { clearColor } from "./forward";
4
- import { perspective, multiply, invert } from "./scene";
4
+ import { perspective, orthographic, multiply, invert } from "./scene";
5
5
 
6
6
  export const RenderMode = {
7
7
  Raster: 0,
@@ -16,6 +16,11 @@ export const DebugMode = {
16
16
  Hit: 4,
17
17
  } as const;
18
18
 
19
+ export const CameraMode = {
20
+ Perspective: 0,
21
+ Orthographic: 1,
22
+ } as const;
23
+
19
24
  export const Camera = {
20
25
  fov: [] as number[],
21
26
  near: [] as number[],
@@ -24,6 +29,8 @@ export const Camera = {
24
29
  clearColor: [] as number[],
25
30
  renderMode: [] as number[],
26
31
  debugMode: [] as number[],
32
+ mode: [] as number[],
33
+ size: [] as number[],
27
34
  };
28
35
 
29
36
  setTraits(Camera, {
@@ -35,6 +42,8 @@ setTraits(Camera, {
35
42
  clearColor: 0x1a1a1a,
36
43
  renderMode: RenderMode.Raster,
37
44
  debugMode: DebugMode.Color,
45
+ mode: CameraMode.Perspective,
46
+ size: 5,
38
47
  }),
39
48
  });
40
49
 
@@ -77,7 +86,10 @@ export function uploadCamera(
77
86
  clearColor.g = color.g;
78
87
  clearColor.b = color.b;
79
88
 
80
- const proj = perspective(Camera.fov[eid], aspect, Camera.near[eid], Camera.far[eid]);
89
+ const proj =
90
+ Camera.mode[eid] === CameraMode.Orthographic
91
+ ? orthographic(Camera.size[eid], aspect, Camera.near[eid], Camera.far[eid])
92
+ : perspective(Camera.fov[eid], aspect, Camera.near[eid], Camera.far[eid]);
81
93
  const world = WorldTransform.data.subarray(eid * 16, eid * 16 + 16);
82
94
  const view = invert(world);
83
95
  const viewProj = multiply(proj, view);
@@ -66,6 +66,35 @@ export function perspective(fov: number, aspect: number, near: number, far: numb
66
66
  ]);
67
67
  }
68
68
 
69
+ export function orthographic(
70
+ size: number,
71
+ aspect: number,
72
+ near: number,
73
+ far: number
74
+ ): Float32Array {
75
+ const lr = 1 / (size * aspect);
76
+ const bt = 1 / size;
77
+ const nf = 1 / (near - far);
78
+ return new Float32Array([
79
+ lr,
80
+ 0,
81
+ 0,
82
+ 0,
83
+ 0,
84
+ bt,
85
+ 0,
86
+ 0,
87
+ 0,
88
+ 0,
89
+ nf,
90
+ 0,
91
+ 0,
92
+ 0,
93
+ near * nf,
94
+ 1,
95
+ ]);
96
+ }
97
+
69
98
  export function multiply(a: Float32Array, b: Float32Array): Float32Array {
70
99
  const out = new Float32Array(16);
71
100
  for (let i = 0; i < 4; i++) {
@@ -8,6 +8,6 @@ export {
8
8
  type TweenOptions,
9
9
  } from "./tween";
10
10
 
11
- export { Sequence, SequenceState, Pause, computeTweenDelays, finalizeSequences } from "./sequence";
11
+ export { Sequence, SequenceState, Pause, finalizeSequences } from "./sequence";
12
12
 
13
13
  export { EASING_FUNCTIONS, getEasing, getEasingIndex, type EasingFn } from "./easing";
@@ -64,6 +64,10 @@ function updateSequencePlayheads(state: State, dt: number): void {
64
64
  if (Sequence.state[seqEid] !== SequenceState.PLAYING) continue;
65
65
 
66
66
  const prevElapsed = Sequence.elapsed[seqEid] ?? 0;
67
+
68
+ if (prevElapsed === 0) {
69
+ computeTweenDelays(state, seqEid);
70
+ }
67
71
  const elapsed = prevElapsed + dt;
68
72
  Sequence.elapsed[seqEid] = elapsed;
69
73
 
@@ -135,7 +135,6 @@ export function finalizePendingTweens(state: State, context: ParseContext): void
135
135
  if (!binding) continue;
136
136
 
137
137
  state.addRelation(pending.tweenEid, TweenTarget, targetEid);
138
-
139
138
  Tween.to[pending.tweenEid] = parseFloat(pending.to);
140
139
  }
141
140
  pendingXmlTweens = [];