@motion-core/motion-gpu 0.1.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/LICENSE +21 -0
- package/README.md +325 -0
- package/dist/FragCanvas.svelte +511 -0
- package/dist/FragCanvas.svelte.d.ts +26 -0
- package/dist/MotionGPUErrorOverlay.svelte +394 -0
- package/dist/MotionGPUErrorOverlay.svelte.d.ts +7 -0
- package/dist/Portal.svelte +46 -0
- package/dist/Portal.svelte.d.ts +8 -0
- package/dist/advanced-scheduler.d.ts +44 -0
- package/dist/advanced-scheduler.js +58 -0
- package/dist/advanced.d.ts +14 -0
- package/dist/advanced.js +9 -0
- package/dist/core/error-diagnostics.d.ts +40 -0
- package/dist/core/error-diagnostics.js +111 -0
- package/dist/core/error-report.d.ts +67 -0
- package/dist/core/error-report.js +190 -0
- package/dist/core/material-preprocess.d.ts +63 -0
- package/dist/core/material-preprocess.js +166 -0
- package/dist/core/material.d.ts +157 -0
- package/dist/core/material.js +358 -0
- package/dist/core/recompile-policy.d.ts +27 -0
- package/dist/core/recompile-policy.js +15 -0
- package/dist/core/render-graph.d.ts +55 -0
- package/dist/core/render-graph.js +73 -0
- package/dist/core/render-targets.d.ts +39 -0
- package/dist/core/render-targets.js +63 -0
- package/dist/core/renderer.d.ts +9 -0
- package/dist/core/renderer.js +1097 -0
- package/dist/core/shader.d.ts +42 -0
- package/dist/core/shader.js +196 -0
- package/dist/core/texture-loader.d.ts +129 -0
- package/dist/core/texture-loader.js +295 -0
- package/dist/core/textures.d.ts +114 -0
- package/dist/core/textures.js +136 -0
- package/dist/core/types.d.ts +523 -0
- package/dist/core/types.js +4 -0
- package/dist/core/uniforms.d.ts +48 -0
- package/dist/core/uniforms.js +222 -0
- package/dist/current-writable.d.ts +31 -0
- package/dist/current-writable.js +27 -0
- package/dist/frame-context.d.ts +287 -0
- package/dist/frame-context.js +731 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +11 -0
- package/dist/motiongpu-context.d.ts +77 -0
- package/dist/motiongpu-context.js +26 -0
- package/dist/passes/BlitPass.d.ts +32 -0
- package/dist/passes/BlitPass.js +158 -0
- package/dist/passes/CopyPass.d.ts +25 -0
- package/dist/passes/CopyPass.js +53 -0
- package/dist/passes/ShaderPass.d.ts +40 -0
- package/dist/passes/ShaderPass.js +182 -0
- package/dist/passes/index.d.ts +3 -0
- package/dist/passes/index.js +3 -0
- package/dist/use-motiongpu-user-context.d.ts +35 -0
- package/dist/use-motiongpu-user-context.js +74 -0
- package/dist/use-texture.d.ts +35 -0
- package/dist/use-texture.js +147 -0
- package/package.json +94 -0
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
import { getContext, onDestroy, setContext } from 'svelte';
|
|
2
|
+
import { writable } from 'svelte/store';
|
|
3
|
+
/**
|
|
4
|
+
* Svelte context key for the active frame registry.
|
|
5
|
+
*/
|
|
6
|
+
const FRAME_CONTEXT_KEY = Symbol('motiongpu.frame-context');
|
|
7
|
+
/**
|
|
8
|
+
* Default stage key used when task stage is not explicitly specified.
|
|
9
|
+
*/
|
|
10
|
+
const MAIN_STAGE_KEY = Symbol('motiongpu-main-stage');
|
|
11
|
+
const RENDER_MODE_INVALIDATION_TOKEN = Symbol('motiongpu-render-mode-change');
|
|
12
|
+
/**
|
|
13
|
+
* Default stage callback that runs tasks immediately.
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_STAGE_CALLBACK = (_state, runTasks) => runTasks();
|
|
16
|
+
/**
|
|
17
|
+
* Normalizes scalar-or-array options to array form.
|
|
18
|
+
*/
|
|
19
|
+
function asArray(value) {
|
|
20
|
+
if (!value) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
return Array.isArray(value) ? value : [value];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Normalizes frame keys to readable string labels.
|
|
27
|
+
*/
|
|
28
|
+
function frameKeyToString(key) {
|
|
29
|
+
return typeof key === 'symbol' ? key.toString() : key;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Extracts task key from either direct key or task reference.
|
|
33
|
+
*/
|
|
34
|
+
function toTaskKey(reference) {
|
|
35
|
+
if (typeof reference === 'string' || typeof reference === 'symbol') {
|
|
36
|
+
return reference;
|
|
37
|
+
}
|
|
38
|
+
return reference.key;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Extracts stage key from either direct key or stage reference.
|
|
42
|
+
*/
|
|
43
|
+
function toStageKey(reference) {
|
|
44
|
+
if (typeof reference === 'string' || typeof reference === 'symbol') {
|
|
45
|
+
return reference;
|
|
46
|
+
}
|
|
47
|
+
return reference.key;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolves invalidation token from static value or resolver callback.
|
|
51
|
+
*/
|
|
52
|
+
function resolveInvalidationToken(token) {
|
|
53
|
+
if (token === undefined) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const resolved = typeof token === 'function' ? token() : token;
|
|
57
|
+
if (resolved === null || resolved === undefined) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return resolved;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Normalizes task invalidation options to runtime representation.
|
|
64
|
+
*/
|
|
65
|
+
function normalizeTaskInvalidation(key, options) {
|
|
66
|
+
const explicit = options.invalidation;
|
|
67
|
+
if (explicit === undefined) {
|
|
68
|
+
if (options.autoInvalidate === false) {
|
|
69
|
+
return {
|
|
70
|
+
mode: 'never',
|
|
71
|
+
lastToken: null,
|
|
72
|
+
hasToken: false
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
mode: 'always',
|
|
77
|
+
token: key,
|
|
78
|
+
lastToken: null,
|
|
79
|
+
hasToken: false
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (explicit === 'never' || explicit === 'always') {
|
|
83
|
+
if (explicit === 'never') {
|
|
84
|
+
return {
|
|
85
|
+
mode: explicit,
|
|
86
|
+
lastToken: null,
|
|
87
|
+
hasToken: false
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
mode: explicit,
|
|
92
|
+
token: key,
|
|
93
|
+
lastToken: null,
|
|
94
|
+
hasToken: false
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const mode = explicit.mode ?? 'always';
|
|
98
|
+
const token = explicit.token;
|
|
99
|
+
if (mode === 'on-change' && token === undefined) {
|
|
100
|
+
throw new Error('Task invalidation mode "on-change" requires a token');
|
|
101
|
+
}
|
|
102
|
+
if (mode === 'never') {
|
|
103
|
+
return {
|
|
104
|
+
mode,
|
|
105
|
+
lastToken: null,
|
|
106
|
+
hasToken: false
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (mode === 'on-change') {
|
|
110
|
+
return {
|
|
111
|
+
mode,
|
|
112
|
+
token: token,
|
|
113
|
+
lastToken: null,
|
|
114
|
+
hasToken: false
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
mode,
|
|
119
|
+
token: token ?? key,
|
|
120
|
+
lastToken: null,
|
|
121
|
+
hasToken: false
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Computes aggregate timing stats from sampled durations.
|
|
126
|
+
*/
|
|
127
|
+
function buildTimingStats(samples, last) {
|
|
128
|
+
if (samples.length === 0) {
|
|
129
|
+
return {
|
|
130
|
+
last,
|
|
131
|
+
avg: 0,
|
|
132
|
+
min: 0,
|
|
133
|
+
max: 0,
|
|
134
|
+
count: 0
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
let sum = 0;
|
|
138
|
+
let min = Number.POSITIVE_INFINITY;
|
|
139
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
140
|
+
for (const value of samples) {
|
|
141
|
+
sum += value;
|
|
142
|
+
if (value < min) {
|
|
143
|
+
min = value;
|
|
144
|
+
}
|
|
145
|
+
if (value > max) {
|
|
146
|
+
max = value;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
last,
|
|
151
|
+
avg: sum / samples.length,
|
|
152
|
+
min,
|
|
153
|
+
max,
|
|
154
|
+
count: samples.length
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Deterministically sorts dependency keys for stable traversal and diagnostics.
|
|
159
|
+
*/
|
|
160
|
+
function sortDependencyKeys(keys) {
|
|
161
|
+
return Array.from(keys).sort((a, b) => frameKeyToString(a).localeCompare(frameKeyToString(b)));
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Finds one deterministic cycle path in the directed dependency graph.
|
|
165
|
+
*/
|
|
166
|
+
function findDependencyCycle(items, edges) {
|
|
167
|
+
const visitState = new Map();
|
|
168
|
+
const stack = [];
|
|
169
|
+
let cycle = null;
|
|
170
|
+
const sortedItems = [...items].sort((a, b) => a.order - b.order);
|
|
171
|
+
const visit = (key) => {
|
|
172
|
+
visitState.set(key, 1);
|
|
173
|
+
stack.push(key);
|
|
174
|
+
for (const childKey of sortDependencyKeys(edges.get(key) ?? [])) {
|
|
175
|
+
const state = visitState.get(childKey) ?? 0;
|
|
176
|
+
if (state === 0) {
|
|
177
|
+
if (visit(childKey)) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (state === 1) {
|
|
183
|
+
const cycleStartIndex = stack.findIndex((entry) => entry === childKey);
|
|
184
|
+
const cyclePath = cycleStartIndex === -1 ? [childKey] : stack.slice(cycleStartIndex);
|
|
185
|
+
cycle = [...cyclePath, childKey];
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
stack.pop();
|
|
190
|
+
visitState.set(key, 2);
|
|
191
|
+
return false;
|
|
192
|
+
};
|
|
193
|
+
for (const item of sortedItems) {
|
|
194
|
+
if ((visitState.get(item.key) ?? 0) !== 0) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (visit(item.key)) {
|
|
198
|
+
return cycle;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Topologically sorts items by `before`/`after` dependencies.
|
|
205
|
+
*
|
|
206
|
+
* Throws deterministic errors when dependencies are missing or cyclic.
|
|
207
|
+
*/
|
|
208
|
+
function sortByDependencies(items, getBefore, getAfter, options) {
|
|
209
|
+
const itemsByKey = new Map();
|
|
210
|
+
for (const item of items) {
|
|
211
|
+
itemsByKey.set(item.key, item);
|
|
212
|
+
}
|
|
213
|
+
const indegree = new Map();
|
|
214
|
+
const edges = new Map();
|
|
215
|
+
for (const item of items) {
|
|
216
|
+
indegree.set(item.key, 0);
|
|
217
|
+
edges.set(item.key, new Set());
|
|
218
|
+
}
|
|
219
|
+
for (const item of items) {
|
|
220
|
+
for (const dependencyKey of getAfter(item)) {
|
|
221
|
+
if (!itemsByKey.has(dependencyKey)) {
|
|
222
|
+
if (options.isKnownExternalDependency?.(dependencyKey)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
throw new Error(`${options.graphName} dependency error: ${options.getItemLabel(item)} references missing dependency "${frameKeyToString(dependencyKey)}" in "after".`);
|
|
226
|
+
}
|
|
227
|
+
edges.get(dependencyKey)?.add(item.key);
|
|
228
|
+
indegree.set(item.key, (indegree.get(item.key) ?? 0) + 1);
|
|
229
|
+
}
|
|
230
|
+
for (const dependencyKey of getBefore(item)) {
|
|
231
|
+
if (!itemsByKey.has(dependencyKey)) {
|
|
232
|
+
if (options.isKnownExternalDependency?.(dependencyKey)) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
throw new Error(`${options.graphName} dependency error: ${options.getItemLabel(item)} references missing dependency "${frameKeyToString(dependencyKey)}" in "before".`);
|
|
236
|
+
}
|
|
237
|
+
edges.get(item.key)?.add(dependencyKey);
|
|
238
|
+
indegree.set(dependencyKey, (indegree.get(dependencyKey) ?? 0) + 1);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const queue = items.filter((item) => (indegree.get(item.key) ?? 0) === 0);
|
|
242
|
+
queue.sort((a, b) => a.order - b.order);
|
|
243
|
+
const ordered = [];
|
|
244
|
+
while (queue.length > 0) {
|
|
245
|
+
const current = queue.shift();
|
|
246
|
+
if (!current) {
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
ordered.push(current);
|
|
250
|
+
for (const childKey of edges.get(current.key) ?? []) {
|
|
251
|
+
const nextDegree = (indegree.get(childKey) ?? 0) - 1;
|
|
252
|
+
indegree.set(childKey, nextDegree);
|
|
253
|
+
if (nextDegree === 0) {
|
|
254
|
+
const child = itemsByKey.get(childKey);
|
|
255
|
+
if (child) {
|
|
256
|
+
queue.push(child);
|
|
257
|
+
queue.sort((a, b) => a.order - b.order);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (ordered.length !== items.length) {
|
|
263
|
+
const cycle = findDependencyCycle(items, edges);
|
|
264
|
+
if (cycle) {
|
|
265
|
+
throw new Error(`${options.graphName} dependency cycle detected: ${cycle.map((key) => frameKeyToString(key)).join(' -> ')}`);
|
|
266
|
+
}
|
|
267
|
+
throw new Error(`${options.graphName} dependency resolution failed.`);
|
|
268
|
+
}
|
|
269
|
+
return ordered;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Creates a frame registry used by `FragCanvas` and `useFrame`.
|
|
273
|
+
*
|
|
274
|
+
* @param options - Initial scheduler options.
|
|
275
|
+
* @returns Mutable frame registry instance.
|
|
276
|
+
*/
|
|
277
|
+
export function createFrameRegistry(options) {
|
|
278
|
+
let renderMode = options?.renderMode ?? 'always';
|
|
279
|
+
let autoRender = options?.autoRender ?? true;
|
|
280
|
+
let maxDelta = options?.maxDelta ?? 0.1;
|
|
281
|
+
let profilingEnabled = options?.profilingEnabled ?? options?.diagnosticsEnabled ?? false;
|
|
282
|
+
let profilingWindow = options?.profilingWindow ?? 120;
|
|
283
|
+
let lastRunTimings = null;
|
|
284
|
+
const profilingHistory = [];
|
|
285
|
+
let hasUntokenizedInvalidation = true;
|
|
286
|
+
const invalidationTokens = new Set();
|
|
287
|
+
let shouldAdvance = false;
|
|
288
|
+
let orderCounter = 0;
|
|
289
|
+
const assertMaxDelta = (value) => {
|
|
290
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
291
|
+
throw new Error('maxDelta must be a finite number greater than 0');
|
|
292
|
+
}
|
|
293
|
+
return value;
|
|
294
|
+
};
|
|
295
|
+
const assertProfilingWindow = (value) => {
|
|
296
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
297
|
+
throw new Error('profilingWindow must be a finite number greater than 0');
|
|
298
|
+
}
|
|
299
|
+
return Math.floor(value);
|
|
300
|
+
};
|
|
301
|
+
maxDelta = assertMaxDelta(maxDelta);
|
|
302
|
+
profilingWindow = assertProfilingWindow(profilingWindow);
|
|
303
|
+
const stages = new Map();
|
|
304
|
+
let scheduleDirty = true;
|
|
305
|
+
let sortedStages = [];
|
|
306
|
+
const sortedTasksByStage = new Map();
|
|
307
|
+
let scheduleSnapshot = { stages: [] };
|
|
308
|
+
const markScheduleDirty = () => {
|
|
309
|
+
scheduleDirty = true;
|
|
310
|
+
};
|
|
311
|
+
const syncSchedule = () => {
|
|
312
|
+
if (!scheduleDirty) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const stageList = sortByDependencies(Array.from(stages.values()), (stage) => stage.before, (stage) => stage.after, {
|
|
316
|
+
graphName: 'Frame stage graph',
|
|
317
|
+
getItemLabel: (stage) => `stage "${frameKeyToString(stage.key)}"`
|
|
318
|
+
});
|
|
319
|
+
const nextTasksByStage = new Map();
|
|
320
|
+
const globalTaskKeys = new Set();
|
|
321
|
+
for (const stage of stageList) {
|
|
322
|
+
for (const task of stage.tasks.values()) {
|
|
323
|
+
globalTaskKeys.add(task.task.key);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
for (const stage of stageList) {
|
|
327
|
+
const taskList = sortByDependencies(Array.from(stage.tasks.values()).map((task) => ({
|
|
328
|
+
key: task.task.key,
|
|
329
|
+
order: task.order,
|
|
330
|
+
task
|
|
331
|
+
})), (task) => task.task.before, (task) => task.task.after, {
|
|
332
|
+
graphName: `Frame task graph for stage "${frameKeyToString(stage.key)}"`,
|
|
333
|
+
getItemLabel: (task) => `task "${frameKeyToString(task.key)}"`,
|
|
334
|
+
isKnownExternalDependency: (key) => globalTaskKeys.has(key)
|
|
335
|
+
}).map((task) => task.task);
|
|
336
|
+
nextTasksByStage.set(stage.key, taskList);
|
|
337
|
+
}
|
|
338
|
+
sortedStages = stageList;
|
|
339
|
+
sortedTasksByStage.clear();
|
|
340
|
+
for (const [stageKey, taskList] of nextTasksByStage) {
|
|
341
|
+
sortedTasksByStage.set(stageKey, taskList);
|
|
342
|
+
}
|
|
343
|
+
scheduleSnapshot = {
|
|
344
|
+
stages: sortedStages.map((stage) => ({
|
|
345
|
+
key: frameKeyToString(stage.key),
|
|
346
|
+
tasks: (sortedTasksByStage.get(stage.key) ?? []).map((task) => frameKeyToString(task.task.key))
|
|
347
|
+
}))
|
|
348
|
+
};
|
|
349
|
+
scheduleDirty = false;
|
|
350
|
+
};
|
|
351
|
+
const pushProfile = (timings) => {
|
|
352
|
+
profilingHistory.push(timings);
|
|
353
|
+
while (profilingHistory.length > profilingWindow) {
|
|
354
|
+
profilingHistory.shift();
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
const clearProfiling = () => {
|
|
358
|
+
profilingHistory.length = 0;
|
|
359
|
+
lastRunTimings = null;
|
|
360
|
+
};
|
|
361
|
+
const buildProfilingSnapshot = () => {
|
|
362
|
+
if (!profilingEnabled) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
const stageBuckets = new Map();
|
|
366
|
+
const totalDurations = [];
|
|
367
|
+
for (const frame of profilingHistory) {
|
|
368
|
+
totalDurations.push(frame.total);
|
|
369
|
+
for (const [stageKey, stageTiming] of Object.entries(frame.stages)) {
|
|
370
|
+
const stageBucket = stageBuckets.get(stageKey) ?? {
|
|
371
|
+
durations: [],
|
|
372
|
+
taskDurations: new Map()
|
|
373
|
+
};
|
|
374
|
+
stageBucket.durations.push(stageTiming.duration);
|
|
375
|
+
for (const [taskKey, taskDuration] of Object.entries(stageTiming.tasks)) {
|
|
376
|
+
const bucket = stageBucket.taskDurations.get(taskKey) ?? [];
|
|
377
|
+
bucket.push(taskDuration);
|
|
378
|
+
stageBucket.taskDurations.set(taskKey, bucket);
|
|
379
|
+
}
|
|
380
|
+
stageBuckets.set(stageKey, stageBucket);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const stagesSnapshot = {};
|
|
384
|
+
for (const [stageKey, stageBucket] of stageBuckets) {
|
|
385
|
+
const lastStageDuration = lastRunTimings?.stages[stageKey]?.duration ?? 0;
|
|
386
|
+
const taskSnapshot = {};
|
|
387
|
+
for (const [taskKey, taskDurations] of stageBucket.taskDurations) {
|
|
388
|
+
const lastTaskDuration = lastRunTimings?.stages[stageKey]?.tasks[taskKey] ?? 0;
|
|
389
|
+
taskSnapshot[taskKey] = buildTimingStats(taskDurations, lastTaskDuration);
|
|
390
|
+
}
|
|
391
|
+
stagesSnapshot[stageKey] = {
|
|
392
|
+
timings: buildTimingStats(stageBucket.durations, lastStageDuration),
|
|
393
|
+
tasks: taskSnapshot
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
window: profilingWindow,
|
|
398
|
+
frameCount: profilingHistory.length,
|
|
399
|
+
lastFrame: lastRunTimings,
|
|
400
|
+
total: buildTimingStats(totalDurations, lastRunTimings?.total ?? 0),
|
|
401
|
+
stages: stagesSnapshot
|
|
402
|
+
};
|
|
403
|
+
};
|
|
404
|
+
const ensureStage = (stageReference, stageOptions) => {
|
|
405
|
+
const stageKey = toStageKey(stageReference);
|
|
406
|
+
const existing = stages.get(stageKey);
|
|
407
|
+
if (existing) {
|
|
408
|
+
if (stageOptions?.before !== undefined) {
|
|
409
|
+
existing.before = new Set(stageOptions.before.map((entry) => toStageKey(entry)));
|
|
410
|
+
markScheduleDirty();
|
|
411
|
+
}
|
|
412
|
+
if (stageOptions?.after !== undefined) {
|
|
413
|
+
existing.after = new Set(stageOptions.after.map((entry) => toStageKey(entry)));
|
|
414
|
+
markScheduleDirty();
|
|
415
|
+
}
|
|
416
|
+
if (stageOptions && Object.prototype.hasOwnProperty.call(stageOptions, 'callback')) {
|
|
417
|
+
existing.callback = stageOptions.callback ?? DEFAULT_STAGE_CALLBACK;
|
|
418
|
+
}
|
|
419
|
+
return existing;
|
|
420
|
+
}
|
|
421
|
+
const stage = {
|
|
422
|
+
key: stageKey,
|
|
423
|
+
order: orderCounter++,
|
|
424
|
+
started: true,
|
|
425
|
+
before: new Set((stageOptions?.before ?? []).map((entry) => toStageKey(entry))),
|
|
426
|
+
after: new Set((stageOptions?.after ?? []).map((entry) => toStageKey(entry))),
|
|
427
|
+
callback: stageOptions?.callback ?? DEFAULT_STAGE_CALLBACK,
|
|
428
|
+
tasks: new Map()
|
|
429
|
+
};
|
|
430
|
+
stages.set(stageKey, stage);
|
|
431
|
+
markScheduleDirty();
|
|
432
|
+
return stage;
|
|
433
|
+
};
|
|
434
|
+
ensureStage(MAIN_STAGE_KEY);
|
|
435
|
+
const resolveEffectiveRunning = (task) => {
|
|
436
|
+
const running = task.started && (task.running?.() ?? true);
|
|
437
|
+
if (task.lastRunning !== running) {
|
|
438
|
+
task.lastRunning = running;
|
|
439
|
+
task.startedStoreSet(running);
|
|
440
|
+
}
|
|
441
|
+
return running;
|
|
442
|
+
};
|
|
443
|
+
const hasPendingInvalidation = () => {
|
|
444
|
+
return hasUntokenizedInvalidation || invalidationTokens.size > 0;
|
|
445
|
+
};
|
|
446
|
+
const invalidateWithToken = (token) => {
|
|
447
|
+
if (token === undefined) {
|
|
448
|
+
hasUntokenizedInvalidation = true;
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
invalidationTokens.add(token);
|
|
452
|
+
};
|
|
453
|
+
const applyTaskInvalidation = (task) => {
|
|
454
|
+
const config = task.invalidation;
|
|
455
|
+
if (config.mode === 'never') {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (config.mode === 'always') {
|
|
459
|
+
const token = resolveInvalidationToken(config.token);
|
|
460
|
+
invalidateWithToken(token ?? task.task.key);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const token = resolveInvalidationToken(config.token);
|
|
464
|
+
if (token === null) {
|
|
465
|
+
config.hasToken = false;
|
|
466
|
+
config.lastToken = null;
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const changed = !config.hasToken || config.lastToken !== token;
|
|
470
|
+
config.hasToken = true;
|
|
471
|
+
config.lastToken = token;
|
|
472
|
+
if (changed) {
|
|
473
|
+
invalidateWithToken(token);
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
return {
|
|
477
|
+
register(keyOrCallback, callbackOrOptions, maybeOptions) {
|
|
478
|
+
const key = typeof keyOrCallback === 'function'
|
|
479
|
+
? Symbol('motiongpu-task')
|
|
480
|
+
: keyOrCallback;
|
|
481
|
+
const callback = typeof keyOrCallback === 'function' ? keyOrCallback : callbackOrOptions;
|
|
482
|
+
const taskOptions = typeof keyOrCallback === 'function'
|
|
483
|
+
? (callbackOrOptions ?? {})
|
|
484
|
+
: (maybeOptions ?? {});
|
|
485
|
+
if (typeof callback !== 'function') {
|
|
486
|
+
throw new Error('useFrame requires a callback');
|
|
487
|
+
}
|
|
488
|
+
const before = asArray(taskOptions.before);
|
|
489
|
+
const after = asArray(taskOptions.after);
|
|
490
|
+
const inferredStage = [...before, ...after].find((entry) => typeof entry === 'object' && entry !== null && 'stage' in entry);
|
|
491
|
+
const stageKey = taskOptions.stage
|
|
492
|
+
? toStageKey(taskOptions.stage)
|
|
493
|
+
: (inferredStage?.stage ?? MAIN_STAGE_KEY);
|
|
494
|
+
const stage = ensureStage(stageKey);
|
|
495
|
+
const startedWritable = writable(taskOptions.autoStart ?? true);
|
|
496
|
+
const internalTask = {
|
|
497
|
+
task: { key, stage: stage.key },
|
|
498
|
+
callback,
|
|
499
|
+
order: orderCounter++,
|
|
500
|
+
started: taskOptions.autoStart ?? true,
|
|
501
|
+
lastRunning: taskOptions.autoStart ?? true,
|
|
502
|
+
startedStoreSet: startedWritable.set,
|
|
503
|
+
startedStore: { subscribe: startedWritable.subscribe },
|
|
504
|
+
before: new Set(before.map((entry) => toTaskKey(entry))),
|
|
505
|
+
after: new Set(after.map((entry) => toTaskKey(entry))),
|
|
506
|
+
invalidation: normalizeTaskInvalidation(key, taskOptions)
|
|
507
|
+
};
|
|
508
|
+
if (taskOptions.running) {
|
|
509
|
+
internalTask.running = taskOptions.running;
|
|
510
|
+
}
|
|
511
|
+
stage.tasks.set(key, internalTask);
|
|
512
|
+
markScheduleDirty();
|
|
513
|
+
internalTask.startedStoreSet(resolveEffectiveRunning(internalTask));
|
|
514
|
+
const start = () => {
|
|
515
|
+
internalTask.started = true;
|
|
516
|
+
resolveEffectiveRunning(internalTask);
|
|
517
|
+
};
|
|
518
|
+
const stop = () => {
|
|
519
|
+
internalTask.started = false;
|
|
520
|
+
resolveEffectiveRunning(internalTask);
|
|
521
|
+
};
|
|
522
|
+
return {
|
|
523
|
+
task: internalTask.task,
|
|
524
|
+
start,
|
|
525
|
+
stop,
|
|
526
|
+
started: internalTask.startedStore,
|
|
527
|
+
unsubscribe: () => {
|
|
528
|
+
if (stage.tasks.delete(key)) {
|
|
529
|
+
markScheduleDirty();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
},
|
|
534
|
+
run(state) {
|
|
535
|
+
const clampedDelta = Math.min(state.delta, maxDelta);
|
|
536
|
+
const frameState = clampedDelta === state.delta
|
|
537
|
+
? state
|
|
538
|
+
: {
|
|
539
|
+
...state,
|
|
540
|
+
delta: clampedDelta
|
|
541
|
+
};
|
|
542
|
+
syncSchedule();
|
|
543
|
+
const frameStart = profilingEnabled ? performance.now() : 0;
|
|
544
|
+
const stageTimings = {};
|
|
545
|
+
for (const stage of sortedStages) {
|
|
546
|
+
if (!stage.started) {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
const stageStart = profilingEnabled ? performance.now() : 0;
|
|
550
|
+
const taskTimings = {};
|
|
551
|
+
const taskList = sortedTasksByStage.get(stage.key) ?? [];
|
|
552
|
+
stage.callback(frameState, () => {
|
|
553
|
+
for (const task of taskList) {
|
|
554
|
+
if (!resolveEffectiveRunning(task)) {
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
const taskStart = profilingEnabled ? performance.now() : 0;
|
|
558
|
+
task.callback(frameState);
|
|
559
|
+
if (profilingEnabled) {
|
|
560
|
+
taskTimings[frameKeyToString(task.task.key)] = performance.now() - taskStart;
|
|
561
|
+
}
|
|
562
|
+
applyTaskInvalidation(task);
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
if (profilingEnabled) {
|
|
566
|
+
stageTimings[frameKeyToString(stage.key)] = {
|
|
567
|
+
duration: performance.now() - stageStart,
|
|
568
|
+
tasks: taskTimings
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (profilingEnabled) {
|
|
573
|
+
const timings = {
|
|
574
|
+
total: performance.now() - frameStart,
|
|
575
|
+
stages: stageTimings
|
|
576
|
+
};
|
|
577
|
+
lastRunTimings = timings;
|
|
578
|
+
pushProfile(timings);
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
invalidate(token) {
|
|
582
|
+
invalidateWithToken(token);
|
|
583
|
+
},
|
|
584
|
+
advance() {
|
|
585
|
+
shouldAdvance = true;
|
|
586
|
+
invalidateWithToken();
|
|
587
|
+
},
|
|
588
|
+
shouldRender() {
|
|
589
|
+
if (!autoRender) {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
if (renderMode === 'always') {
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
if (renderMode === 'on-demand') {
|
|
596
|
+
return shouldAdvance || hasPendingInvalidation();
|
|
597
|
+
}
|
|
598
|
+
return shouldAdvance;
|
|
599
|
+
},
|
|
600
|
+
endFrame() {
|
|
601
|
+
hasUntokenizedInvalidation = false;
|
|
602
|
+
invalidationTokens.clear();
|
|
603
|
+
shouldAdvance = false;
|
|
604
|
+
},
|
|
605
|
+
setRenderMode(mode) {
|
|
606
|
+
if (renderMode === mode) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
renderMode = mode;
|
|
610
|
+
shouldAdvance = false;
|
|
611
|
+
if (mode === 'on-demand') {
|
|
612
|
+
invalidateWithToken(RENDER_MODE_INVALIDATION_TOKEN);
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
setAutoRender(enabled) {
|
|
616
|
+
autoRender = enabled;
|
|
617
|
+
},
|
|
618
|
+
setMaxDelta(value) {
|
|
619
|
+
maxDelta = assertMaxDelta(value);
|
|
620
|
+
},
|
|
621
|
+
setProfilingEnabled(enabled) {
|
|
622
|
+
profilingEnabled = enabled;
|
|
623
|
+
if (!enabled) {
|
|
624
|
+
clearProfiling();
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
setProfilingWindow(window) {
|
|
628
|
+
profilingWindow = assertProfilingWindow(window);
|
|
629
|
+
while (profilingHistory.length > profilingWindow) {
|
|
630
|
+
profilingHistory.shift();
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
resetProfiling() {
|
|
634
|
+
clearProfiling();
|
|
635
|
+
},
|
|
636
|
+
setDiagnosticsEnabled(enabled) {
|
|
637
|
+
profilingEnabled = enabled;
|
|
638
|
+
if (!enabled) {
|
|
639
|
+
clearProfiling();
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
getRenderMode() {
|
|
643
|
+
return renderMode;
|
|
644
|
+
},
|
|
645
|
+
getAutoRender() {
|
|
646
|
+
return autoRender;
|
|
647
|
+
},
|
|
648
|
+
getMaxDelta() {
|
|
649
|
+
return maxDelta;
|
|
650
|
+
},
|
|
651
|
+
getProfilingEnabled() {
|
|
652
|
+
return profilingEnabled;
|
|
653
|
+
},
|
|
654
|
+
getProfilingWindow() {
|
|
655
|
+
return profilingWindow;
|
|
656
|
+
},
|
|
657
|
+
getProfilingSnapshot() {
|
|
658
|
+
return buildProfilingSnapshot();
|
|
659
|
+
},
|
|
660
|
+
getDiagnosticsEnabled() {
|
|
661
|
+
return profilingEnabled;
|
|
662
|
+
},
|
|
663
|
+
getLastRunTimings() {
|
|
664
|
+
return lastRunTimings;
|
|
665
|
+
},
|
|
666
|
+
getSchedule() {
|
|
667
|
+
syncSchedule();
|
|
668
|
+
return scheduleSnapshot;
|
|
669
|
+
},
|
|
670
|
+
createStage(key, options) {
|
|
671
|
+
const stageOptions = options
|
|
672
|
+
? {
|
|
673
|
+
...(Object.prototype.hasOwnProperty.call(options, 'before')
|
|
674
|
+
? { before: asArray(options.before) }
|
|
675
|
+
: {}),
|
|
676
|
+
...(Object.prototype.hasOwnProperty.call(options, 'after')
|
|
677
|
+
? { after: asArray(options.after) }
|
|
678
|
+
: {}),
|
|
679
|
+
...(Object.prototype.hasOwnProperty.call(options, 'callback')
|
|
680
|
+
? { callback: options.callback ?? null }
|
|
681
|
+
: {})
|
|
682
|
+
}
|
|
683
|
+
: undefined;
|
|
684
|
+
const stage = ensureStage(key, stageOptions);
|
|
685
|
+
return { key: stage.key };
|
|
686
|
+
},
|
|
687
|
+
getStage(key) {
|
|
688
|
+
const stage = stages.get(key);
|
|
689
|
+
if (!stage) {
|
|
690
|
+
return undefined;
|
|
691
|
+
}
|
|
692
|
+
return { key: stage.key };
|
|
693
|
+
},
|
|
694
|
+
clear() {
|
|
695
|
+
for (const stage of stages.values()) {
|
|
696
|
+
stage.tasks.clear();
|
|
697
|
+
}
|
|
698
|
+
markScheduleDirty();
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Provides a frame registry through Svelte context.
|
|
704
|
+
*
|
|
705
|
+
* @param registry - Registry to provide.
|
|
706
|
+
*/
|
|
707
|
+
export function provideFrameRegistry(registry) {
|
|
708
|
+
setContext(FRAME_CONTEXT_KEY, registry);
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Registers a callback in the active frame registry and auto-unsubscribes on destroy.
|
|
712
|
+
*
|
|
713
|
+
* @returns Frame task handle for start/stop control.
|
|
714
|
+
* @throws {Error} When used outside `<FragCanvas>`.
|
|
715
|
+
*/
|
|
716
|
+
export function useFrame(keyOrCallback, callbackOrOptions, maybeOptions) {
|
|
717
|
+
const registry = getContext(FRAME_CONTEXT_KEY);
|
|
718
|
+
if (!registry) {
|
|
719
|
+
throw new Error('useFrame must be used inside <FragCanvas>');
|
|
720
|
+
}
|
|
721
|
+
const registration = typeof keyOrCallback === 'function'
|
|
722
|
+
? registry.register(keyOrCallback, callbackOrOptions)
|
|
723
|
+
: registry.register(keyOrCallback, callbackOrOptions, maybeOptions);
|
|
724
|
+
onDestroy(registration.unsubscribe);
|
|
725
|
+
return {
|
|
726
|
+
task: registration.task,
|
|
727
|
+
start: registration.start,
|
|
728
|
+
stop: registration.stop,
|
|
729
|
+
started: registration.started
|
|
730
|
+
};
|
|
731
|
+
}
|