@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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +325 -0
  3. package/dist/FragCanvas.svelte +511 -0
  4. package/dist/FragCanvas.svelte.d.ts +26 -0
  5. package/dist/MotionGPUErrorOverlay.svelte +394 -0
  6. package/dist/MotionGPUErrorOverlay.svelte.d.ts +7 -0
  7. package/dist/Portal.svelte +46 -0
  8. package/dist/Portal.svelte.d.ts +8 -0
  9. package/dist/advanced-scheduler.d.ts +44 -0
  10. package/dist/advanced-scheduler.js +58 -0
  11. package/dist/advanced.d.ts +14 -0
  12. package/dist/advanced.js +9 -0
  13. package/dist/core/error-diagnostics.d.ts +40 -0
  14. package/dist/core/error-diagnostics.js +111 -0
  15. package/dist/core/error-report.d.ts +67 -0
  16. package/dist/core/error-report.js +190 -0
  17. package/dist/core/material-preprocess.d.ts +63 -0
  18. package/dist/core/material-preprocess.js +166 -0
  19. package/dist/core/material.d.ts +157 -0
  20. package/dist/core/material.js +358 -0
  21. package/dist/core/recompile-policy.d.ts +27 -0
  22. package/dist/core/recompile-policy.js +15 -0
  23. package/dist/core/render-graph.d.ts +55 -0
  24. package/dist/core/render-graph.js +73 -0
  25. package/dist/core/render-targets.d.ts +39 -0
  26. package/dist/core/render-targets.js +63 -0
  27. package/dist/core/renderer.d.ts +9 -0
  28. package/dist/core/renderer.js +1097 -0
  29. package/dist/core/shader.d.ts +42 -0
  30. package/dist/core/shader.js +196 -0
  31. package/dist/core/texture-loader.d.ts +129 -0
  32. package/dist/core/texture-loader.js +295 -0
  33. package/dist/core/textures.d.ts +114 -0
  34. package/dist/core/textures.js +136 -0
  35. package/dist/core/types.d.ts +523 -0
  36. package/dist/core/types.js +4 -0
  37. package/dist/core/uniforms.d.ts +48 -0
  38. package/dist/core/uniforms.js +222 -0
  39. package/dist/current-writable.d.ts +31 -0
  40. package/dist/current-writable.js +27 -0
  41. package/dist/frame-context.d.ts +287 -0
  42. package/dist/frame-context.js +731 -0
  43. package/dist/index.d.ts +17 -0
  44. package/dist/index.js +11 -0
  45. package/dist/motiongpu-context.d.ts +77 -0
  46. package/dist/motiongpu-context.js +26 -0
  47. package/dist/passes/BlitPass.d.ts +32 -0
  48. package/dist/passes/BlitPass.js +158 -0
  49. package/dist/passes/CopyPass.d.ts +25 -0
  50. package/dist/passes/CopyPass.js +53 -0
  51. package/dist/passes/ShaderPass.d.ts +40 -0
  52. package/dist/passes/ShaderPass.js +182 -0
  53. package/dist/passes/index.d.ts +3 -0
  54. package/dist/passes/index.js +3 -0
  55. package/dist/use-motiongpu-user-context.d.ts +35 -0
  56. package/dist/use-motiongpu-user-context.js +74 -0
  57. package/dist/use-texture.d.ts +35 -0
  58. package/dist/use-texture.js +147 -0
  59. 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
+ }