@motion-core/motion-gpu 0.4.1 → 0.4.2
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/dist/advanced.d.ts +1 -0
- package/dist/advanced.d.ts.map +1 -0
- package/dist/advanced.js +12 -6
- package/dist/core/advanced.d.ts +1 -0
- package/dist/core/advanced.d.ts.map +1 -0
- package/dist/core/advanced.js +12 -5
- package/dist/core/current-value.d.ts +1 -0
- package/dist/core/current-value.d.ts.map +1 -0
- package/dist/core/current-value.js +35 -34
- package/dist/core/current-value.js.map +1 -0
- package/dist/core/error-diagnostics.d.ts +1 -0
- package/dist/core/error-diagnostics.d.ts.map +1 -0
- package/dist/core/error-diagnostics.js +70 -137
- package/dist/core/error-diagnostics.js.map +1 -0
- package/dist/core/error-report.d.ts +1 -0
- package/dist/core/error-report.d.ts.map +1 -0
- package/dist/core/error-report.js +184 -233
- package/dist/core/error-report.js.map +1 -0
- package/dist/core/frame-registry.d.ts +1 -0
- package/dist/core/frame-registry.d.ts.map +1 -0
- package/dist/core/frame-registry.js +546 -662
- package/dist/core/frame-registry.js.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +11 -12
- package/dist/core/material-preprocess.d.ts +1 -0
- package/dist/core/material-preprocess.d.ts.map +1 -0
- package/dist/core/material-preprocess.js +128 -151
- package/dist/core/material-preprocess.js.map +1 -0
- package/dist/core/material.d.ts +1 -0
- package/dist/core/material.d.ts.map +1 -0
- package/dist/core/material.js +263 -317
- package/dist/core/material.js.map +1 -0
- package/dist/core/recompile-policy.d.ts +1 -0
- package/dist/core/recompile-policy.d.ts.map +1 -0
- package/dist/core/recompile-policy.js +18 -13
- package/dist/core/recompile-policy.js.map +1 -0
- package/dist/core/render-graph.d.ts +1 -0
- package/dist/core/render-graph.d.ts.map +1 -0
- package/dist/core/render-graph.js +61 -68
- package/dist/core/render-graph.js.map +1 -0
- package/dist/core/render-targets.d.ts +1 -0
- package/dist/core/render-targets.d.ts.map +1 -0
- package/dist/core/render-targets.js +52 -53
- package/dist/core/render-targets.js.map +1 -0
- package/dist/core/renderer.d.ts +1 -0
- package/dist/core/renderer.d.ts.map +1 -0
- package/dist/core/renderer.js +942 -1081
- package/dist/core/renderer.js.map +1 -0
- package/dist/core/runtime-loop.d.ts +1 -0
- package/dist/core/runtime-loop.d.ts.map +1 -0
- package/dist/core/runtime-loop.js +305 -362
- package/dist/core/runtime-loop.js.map +1 -0
- package/dist/core/scheduler-helpers.d.ts +1 -0
- package/dist/core/scheduler-helpers.d.ts.map +1 -0
- package/dist/core/scheduler-helpers.js +52 -51
- package/dist/core/scheduler-helpers.js.map +1 -0
- package/dist/core/shader.d.ts +1 -0
- package/dist/core/shader.d.ts.map +1 -0
- package/dist/core/shader.js +92 -117
- package/dist/core/shader.js.map +1 -0
- package/dist/core/texture-loader.d.ts +1 -0
- package/dist/core/texture-loader.d.ts.map +1 -0
- package/dist/core/texture-loader.js +205 -273
- package/dist/core/texture-loader.js.map +1 -0
- package/dist/core/textures.d.ts +1 -0
- package/dist/core/textures.d.ts.map +1 -0
- package/dist/core/textures.js +106 -116
- package/dist/core/textures.js.map +1 -0
- package/dist/core/types.d.ts +1 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +0 -4
- package/dist/core/uniforms.d.ts +1 -0
- package/dist/core/uniforms.d.ts.map +1 -0
- package/dist/core/uniforms.js +170 -191
- package/dist/core/uniforms.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -6
- package/dist/passes/BlitPass.d.ts +1 -0
- package/dist/passes/BlitPass.d.ts.map +1 -0
- package/dist/passes/BlitPass.js +23 -18
- package/dist/passes/BlitPass.js.map +1 -0
- package/dist/passes/CopyPass.d.ts +1 -0
- package/dist/passes/CopyPass.d.ts.map +1 -0
- package/dist/passes/CopyPass.js +58 -52
- package/dist/passes/CopyPass.js.map +1 -0
- package/dist/passes/FullscreenPass.d.ts +1 -0
- package/dist/passes/FullscreenPass.d.ts.map +1 -0
- package/dist/passes/FullscreenPass.js +127 -130
- package/dist/passes/FullscreenPass.js.map +1 -0
- package/dist/passes/ShaderPass.d.ts +1 -0
- package/dist/passes/ShaderPass.d.ts.map +1 -0
- package/dist/passes/ShaderPass.js +40 -37
- package/dist/passes/ShaderPass.js.map +1 -0
- package/dist/passes/index.d.ts +1 -0
- package/dist/passes/index.d.ts.map +1 -0
- package/dist/passes/index.js +4 -3
- package/dist/react/FragCanvas.d.ts +1 -0
- package/dist/react/FragCanvas.d.ts.map +1 -0
- package/dist/react/FragCanvas.js +234 -211
- package/dist/react/FragCanvas.js.map +1 -0
- package/dist/react/MotionGPUErrorOverlay.d.ts +1 -0
- package/dist/react/MotionGPUErrorOverlay.d.ts.map +1 -0
- package/dist/react/MotionGPUErrorOverlay.js +96 -13
- package/dist/react/MotionGPUErrorOverlay.js.map +1 -0
- package/dist/react/Portal.d.ts +1 -0
- package/dist/react/Portal.d.ts.map +1 -0
- package/dist/react/Portal.js +18 -21
- package/dist/react/Portal.js.map +1 -0
- package/dist/react/advanced.d.ts +1 -0
- package/dist/react/advanced.d.ts.map +1 -0
- package/dist/react/advanced.js +12 -6
- package/dist/react/frame-context.d.ts +1 -0
- package/dist/react/frame-context.d.ts.map +1 -0
- package/dist/react/frame-context.js +88 -94
- package/dist/react/frame-context.js.map +1 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +10 -9
- package/dist/react/motiongpu-context.d.ts +1 -0
- package/dist/react/motiongpu-context.d.ts.map +1 -0
- package/dist/react/motiongpu-context.js +18 -15
- package/dist/react/motiongpu-context.js.map +1 -0
- package/dist/react/use-motiongpu-user-context.d.ts +1 -0
- package/dist/react/use-motiongpu-user-context.d.ts.map +1 -0
- package/dist/react/use-motiongpu-user-context.js +83 -82
- package/dist/react/use-motiongpu-user-context.js.map +1 -0
- package/dist/react/use-texture.d.ts +1 -0
- package/dist/react/use-texture.d.ts.map +1 -0
- package/dist/react/use-texture.js +132 -152
- package/dist/react/use-texture.js.map +1 -0
- package/dist/svelte/FragCanvas.svelte.d.ts +1 -0
- package/dist/svelte/FragCanvas.svelte.d.ts.map +1 -0
- package/dist/svelte/MotionGPUErrorOverlay.svelte.d.ts +1 -0
- package/dist/svelte/MotionGPUErrorOverlay.svelte.d.ts.map +1 -0
- package/dist/svelte/Portal.svelte.d.ts +1 -0
- package/dist/svelte/Portal.svelte.d.ts.map +1 -0
- package/dist/svelte/advanced.d.ts +1 -0
- package/dist/svelte/advanced.d.ts.map +1 -0
- package/dist/svelte/advanced.js +11 -6
- package/dist/svelte/frame-context.d.ts +1 -0
- package/dist/svelte/frame-context.d.ts.map +1 -0
- package/dist/svelte/frame-context.js +27 -27
- package/dist/svelte/frame-context.js.map +1 -0
- package/dist/svelte/index.d.ts +1 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +10 -9
- package/dist/svelte/motiongpu-context.d.ts +1 -0
- package/dist/svelte/motiongpu-context.d.ts.map +1 -0
- package/dist/svelte/motiongpu-context.js +24 -21
- package/dist/svelte/motiongpu-context.js.map +1 -0
- package/dist/svelte/use-motiongpu-user-context.d.ts +1 -0
- package/dist/svelte/use-motiongpu-user-context.d.ts.map +1 -0
- package/dist/svelte/use-motiongpu-user-context.js +69 -70
- package/dist/svelte/use-motiongpu-user-context.js.map +1 -0
- package/dist/svelte/use-texture.d.ts +1 -0
- package/dist/svelte/use-texture.d.ts.map +1 -0
- package/dist/svelte/use-texture.js +125 -147
- package/dist/svelte/use-texture.js.map +1 -0
- package/package.json +12 -7
- package/src/lib/advanced.ts +6 -0
- package/src/lib/core/advanced.ts +12 -0
- package/src/lib/core/current-value.ts +64 -0
- package/src/lib/core/error-diagnostics.ts +236 -0
- package/src/lib/core/error-report.ts +406 -0
- package/src/lib/core/frame-registry.ts +1189 -0
- package/src/lib/core/index.ts +77 -0
- package/src/lib/core/material-preprocess.ts +284 -0
- package/src/lib/core/material.ts +667 -0
- package/src/lib/core/recompile-policy.ts +31 -0
- package/src/lib/core/render-graph.ts +143 -0
- package/src/lib/core/render-targets.ts +107 -0
- package/src/lib/core/renderer.ts +1547 -0
- package/src/lib/core/runtime-loop.ts +458 -0
- package/src/lib/core/scheduler-helpers.ts +136 -0
- package/src/lib/core/shader.ts +258 -0
- package/src/lib/core/texture-loader.ts +476 -0
- package/src/lib/core/textures.ts +235 -0
- package/src/lib/core/types.ts +582 -0
- package/src/lib/core/uniforms.ts +282 -0
- package/src/lib/index.ts +6 -0
- package/src/lib/passes/BlitPass.ts +54 -0
- package/src/lib/passes/CopyPass.ts +80 -0
- package/src/lib/passes/FullscreenPass.ts +173 -0
- package/src/lib/passes/ShaderPass.ts +88 -0
- package/src/lib/passes/index.ts +3 -0
- package/src/lib/react/FragCanvas.tsx +345 -0
- package/src/lib/react/MotionGPUErrorOverlay.tsx +392 -0
- package/src/lib/react/Portal.tsx +34 -0
- package/src/lib/react/advanced.ts +36 -0
- package/src/lib/react/frame-context.ts +169 -0
- package/src/lib/react/index.ts +51 -0
- package/src/lib/react/motiongpu-context.ts +88 -0
- package/src/lib/react/use-motiongpu-user-context.ts +186 -0
- package/src/lib/react/use-texture.ts +233 -0
- package/src/lib/svelte/FragCanvas.svelte +249 -0
- package/src/lib/svelte/MotionGPUErrorOverlay.svelte +382 -0
- package/src/lib/svelte/Portal.svelte +31 -0
- package/src/lib/svelte/advanced.ts +32 -0
- package/src/lib/svelte/frame-context.ts +87 -0
- package/src/lib/svelte/index.ts +51 -0
- package/src/lib/svelte/motiongpu-context.ts +97 -0
- package/src/lib/svelte/use-motiongpu-user-context.ts +145 -0
- package/src/lib/svelte/use-texture.ts +232 -0
|
@@ -0,0 +1,1189 @@
|
|
|
1
|
+
import { createCurrentWritable, type CurrentWritable, type Subscribable } from './current-value.js';
|
|
2
|
+
import type { FrameInvalidationToken, FrameState, RenderMode } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Per-frame callback executed by the frame scheduler.
|
|
6
|
+
*/
|
|
7
|
+
export type FrameCallback = (state: FrameState) => void;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Stable key type used to identify frame tasks and stages.
|
|
11
|
+
*/
|
|
12
|
+
export type FrameKey = string | symbol;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Public metadata describing a registered frame task.
|
|
16
|
+
*/
|
|
17
|
+
export interface FrameTask {
|
|
18
|
+
key: FrameKey;
|
|
19
|
+
stage: FrameKey;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Public metadata describing a frame stage.
|
|
24
|
+
*/
|
|
25
|
+
export interface FrameStage {
|
|
26
|
+
key: FrameKey;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Stage callback allowing custom orchestration around task execution.
|
|
31
|
+
*/
|
|
32
|
+
export type FrameStageCallback = (state: FrameState, runTasks: () => void) => void;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Options controlling task registration and scheduling behavior.
|
|
36
|
+
*/
|
|
37
|
+
export interface UseFrameOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Whether task starts in active state.
|
|
40
|
+
*
|
|
41
|
+
* @default true
|
|
42
|
+
*/
|
|
43
|
+
autoStart?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Whether task execution invalidates frame automatically.
|
|
46
|
+
*
|
|
47
|
+
* @default true
|
|
48
|
+
*/
|
|
49
|
+
autoInvalidate?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Explicit task invalidation policy.
|
|
52
|
+
*/
|
|
53
|
+
invalidation?: FrameTaskInvalidation;
|
|
54
|
+
/**
|
|
55
|
+
* Stage to register task in.
|
|
56
|
+
*
|
|
57
|
+
* If omitted, main stage is used unless inferred from task dependencies.
|
|
58
|
+
*/
|
|
59
|
+
stage?: FrameKey | FrameStage;
|
|
60
|
+
/**
|
|
61
|
+
* Task dependencies that should run after this task.
|
|
62
|
+
*/
|
|
63
|
+
before?: (FrameKey | FrameTask) | (FrameKey | FrameTask)[];
|
|
64
|
+
/**
|
|
65
|
+
* Task dependencies that should run before this task.
|
|
66
|
+
*/
|
|
67
|
+
after?: (FrameKey | FrameTask) | (FrameKey | FrameTask)[];
|
|
68
|
+
/**
|
|
69
|
+
* Dynamic predicate controlling whether the task is currently active.
|
|
70
|
+
*/
|
|
71
|
+
running?: () => boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Invalidation token value or resolver.
|
|
76
|
+
*/
|
|
77
|
+
export type FrameTaskInvalidationToken =
|
|
78
|
+
| FrameInvalidationToken
|
|
79
|
+
| (() => FrameInvalidationToken | null | undefined);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Explicit task invalidation policy.
|
|
83
|
+
*/
|
|
84
|
+
export type FrameTaskInvalidation =
|
|
85
|
+
| 'never'
|
|
86
|
+
| 'always'
|
|
87
|
+
| {
|
|
88
|
+
mode?: 'never' | 'always';
|
|
89
|
+
token?: FrameTaskInvalidationToken;
|
|
90
|
+
}
|
|
91
|
+
| {
|
|
92
|
+
mode: 'on-change';
|
|
93
|
+
token: FrameTaskInvalidationToken;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Handle returned by `useFrame` registration.
|
|
98
|
+
*/
|
|
99
|
+
export interface UseFrameResult {
|
|
100
|
+
/**
|
|
101
|
+
* Registered task metadata.
|
|
102
|
+
*/
|
|
103
|
+
task: FrameTask;
|
|
104
|
+
/**
|
|
105
|
+
* Starts task execution.
|
|
106
|
+
*/
|
|
107
|
+
start: () => void;
|
|
108
|
+
/**
|
|
109
|
+
* Stops task execution.
|
|
110
|
+
*/
|
|
111
|
+
stop: () => void;
|
|
112
|
+
/**
|
|
113
|
+
* Readable flag representing effective running state.
|
|
114
|
+
*/
|
|
115
|
+
started: Subscribable<boolean>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Snapshot of the resolved stage/task execution order.
|
|
120
|
+
*/
|
|
121
|
+
export interface FrameScheduleSnapshot {
|
|
122
|
+
stages: Array<{
|
|
123
|
+
key: string;
|
|
124
|
+
tasks: string[];
|
|
125
|
+
}>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Optional scheduler diagnostics payload captured for the last run.
|
|
130
|
+
*/
|
|
131
|
+
export interface FrameRunTimings {
|
|
132
|
+
total: number;
|
|
133
|
+
stages: Record<
|
|
134
|
+
string,
|
|
135
|
+
{
|
|
136
|
+
duration: number;
|
|
137
|
+
tasks: Record<string, number>;
|
|
138
|
+
}
|
|
139
|
+
>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Aggregated timing statistics for stage/task profiling.
|
|
144
|
+
*/
|
|
145
|
+
export interface FrameTimingStats {
|
|
146
|
+
last: number;
|
|
147
|
+
avg: number;
|
|
148
|
+
min: number;
|
|
149
|
+
max: number;
|
|
150
|
+
count: number;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Profiling snapshot aggregated from the configured history window.
|
|
155
|
+
*/
|
|
156
|
+
export interface FrameProfilingSnapshot {
|
|
157
|
+
window: number;
|
|
158
|
+
frameCount: number;
|
|
159
|
+
lastFrame: FrameRunTimings | null;
|
|
160
|
+
total: FrameTimingStats;
|
|
161
|
+
stages: Record<
|
|
162
|
+
string,
|
|
163
|
+
{
|
|
164
|
+
timings: FrameTimingStats;
|
|
165
|
+
tasks: Record<string, FrameTimingStats>;
|
|
166
|
+
}
|
|
167
|
+
>;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Internal registration payload including unsubscribe callback.
|
|
172
|
+
*/
|
|
173
|
+
interface RegisteredFrameTask extends UseFrameResult {
|
|
174
|
+
unsubscribe: () => void;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Internal mutable task descriptor used by scheduler runtime.
|
|
179
|
+
*/
|
|
180
|
+
interface InternalTask {
|
|
181
|
+
task: FrameTask;
|
|
182
|
+
callback: FrameCallback;
|
|
183
|
+
order: number;
|
|
184
|
+
started: boolean;
|
|
185
|
+
lastRunning: boolean;
|
|
186
|
+
startedStoreSet: (value: boolean) => void;
|
|
187
|
+
startedStore: Subscribable<boolean>;
|
|
188
|
+
before: Set<FrameKey>;
|
|
189
|
+
after: Set<FrameKey>;
|
|
190
|
+
invalidation: {
|
|
191
|
+
mode: 'never' | 'always' | 'on-change';
|
|
192
|
+
token?: FrameTaskInvalidationToken;
|
|
193
|
+
lastToken: FrameInvalidationToken | null;
|
|
194
|
+
hasToken: boolean;
|
|
195
|
+
};
|
|
196
|
+
running?: () => boolean;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Internal mutable stage descriptor used by scheduler runtime.
|
|
201
|
+
*/
|
|
202
|
+
interface InternalStage {
|
|
203
|
+
key: FrameKey;
|
|
204
|
+
order: number;
|
|
205
|
+
started: boolean;
|
|
206
|
+
before: Set<FrameKey>;
|
|
207
|
+
after: Set<FrameKey>;
|
|
208
|
+
callback: FrameStageCallback;
|
|
209
|
+
tasks: Map<FrameKey, InternalTask>;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Default stage key used when task stage is not explicitly specified.
|
|
214
|
+
*/
|
|
215
|
+
const MAIN_STAGE_KEY = Symbol('motiongpu-main-stage');
|
|
216
|
+
const RENDER_MODE_INVALIDATION_TOKEN = Symbol('motiongpu-render-mode-change');
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Default stage callback that runs tasks immediately.
|
|
220
|
+
*/
|
|
221
|
+
const DEFAULT_STAGE_CALLBACK: FrameStageCallback = (_state, runTasks) => runTasks();
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Normalizes scalar-or-array options to array form.
|
|
225
|
+
*/
|
|
226
|
+
function asArray<T>(value: T | T[] | undefined): T[] {
|
|
227
|
+
if (!value) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return Array.isArray(value) ? value : [value];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Normalizes frame keys to readable string labels.
|
|
236
|
+
*/
|
|
237
|
+
function frameKeyToString(key: FrameKey): string {
|
|
238
|
+
return typeof key === 'symbol' ? key.toString() : key;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Extracts task key from either direct key or task reference.
|
|
243
|
+
*/
|
|
244
|
+
function toTaskKey(reference: FrameKey | FrameTask): FrameKey {
|
|
245
|
+
if (typeof reference === 'string' || typeof reference === 'symbol') {
|
|
246
|
+
return reference;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return reference.key;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Extracts stage key from either direct key or stage reference.
|
|
254
|
+
*/
|
|
255
|
+
function toStageKey(reference: FrameKey | FrameStage): FrameKey {
|
|
256
|
+
if (typeof reference === 'string' || typeof reference === 'symbol') {
|
|
257
|
+
return reference;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return reference.key;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Resolves invalidation token from static value or resolver callback.
|
|
265
|
+
*/
|
|
266
|
+
function resolveInvalidationToken(
|
|
267
|
+
token: FrameTaskInvalidationToken | undefined
|
|
268
|
+
): FrameInvalidationToken | null {
|
|
269
|
+
if (token === undefined) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const resolved = typeof token === 'function' ? token() : token;
|
|
274
|
+
if (resolved === null || resolved === undefined) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return resolved;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Normalizes task invalidation options to runtime representation.
|
|
283
|
+
*/
|
|
284
|
+
function normalizeTaskInvalidation(
|
|
285
|
+
key: FrameKey,
|
|
286
|
+
options: UseFrameOptions
|
|
287
|
+
): InternalTask['invalidation'] {
|
|
288
|
+
const explicit = options.invalidation;
|
|
289
|
+
if (explicit === undefined) {
|
|
290
|
+
if (options.autoInvalidate === false) {
|
|
291
|
+
return {
|
|
292
|
+
mode: 'never',
|
|
293
|
+
lastToken: null,
|
|
294
|
+
hasToken: false
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
mode: 'always',
|
|
300
|
+
token: key,
|
|
301
|
+
lastToken: null,
|
|
302
|
+
hasToken: false
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (explicit === 'never' || explicit === 'always') {
|
|
307
|
+
if (explicit === 'never') {
|
|
308
|
+
return {
|
|
309
|
+
mode: explicit,
|
|
310
|
+
lastToken: null,
|
|
311
|
+
hasToken: false
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
mode: explicit,
|
|
317
|
+
token: key,
|
|
318
|
+
lastToken: null,
|
|
319
|
+
hasToken: false
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const mode = explicit.mode ?? 'always';
|
|
324
|
+
const token = explicit.token;
|
|
325
|
+
if (mode === 'on-change' && token === undefined) {
|
|
326
|
+
throw new Error('Task invalidation mode "on-change" requires a token');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (mode === 'never') {
|
|
330
|
+
return {
|
|
331
|
+
mode,
|
|
332
|
+
lastToken: null,
|
|
333
|
+
hasToken: false
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (mode === 'on-change') {
|
|
338
|
+
return {
|
|
339
|
+
mode,
|
|
340
|
+
token: token as FrameTaskInvalidationToken,
|
|
341
|
+
lastToken: null,
|
|
342
|
+
hasToken: false
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
mode,
|
|
348
|
+
token: token ?? key,
|
|
349
|
+
lastToken: null,
|
|
350
|
+
hasToken: false
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Computes aggregate timing stats from sampled durations.
|
|
356
|
+
*/
|
|
357
|
+
function buildTimingStats(samples: number[], last: number): FrameTimingStats {
|
|
358
|
+
if (samples.length === 0) {
|
|
359
|
+
return {
|
|
360
|
+
last,
|
|
361
|
+
avg: 0,
|
|
362
|
+
min: 0,
|
|
363
|
+
max: 0,
|
|
364
|
+
count: 0
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
let sum = 0;
|
|
369
|
+
let min = Number.POSITIVE_INFINITY;
|
|
370
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
371
|
+
|
|
372
|
+
for (const value of samples) {
|
|
373
|
+
sum += value;
|
|
374
|
+
if (value < min) {
|
|
375
|
+
min = value;
|
|
376
|
+
}
|
|
377
|
+
if (value > max) {
|
|
378
|
+
max = value;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
last,
|
|
384
|
+
avg: sum / samples.length,
|
|
385
|
+
min,
|
|
386
|
+
max,
|
|
387
|
+
count: samples.length
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Dependency graph sorting options used for diagnostics labels.
|
|
393
|
+
*/
|
|
394
|
+
interface SortDependenciesOptions<T extends { key: FrameKey; order: number }> {
|
|
395
|
+
graphName: string;
|
|
396
|
+
getItemLabel: (item: T) => string;
|
|
397
|
+
isKnownExternalDependency?: (key: FrameKey) => boolean;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Deterministically sorts dependency keys for stable traversal and diagnostics.
|
|
402
|
+
*/
|
|
403
|
+
function sortDependencyKeys(keys: Iterable<FrameKey>): FrameKey[] {
|
|
404
|
+
return Array.from(keys).sort((a, b) => frameKeyToString(a).localeCompare(frameKeyToString(b)));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Finds one deterministic cycle path in the directed dependency graph.
|
|
409
|
+
*/
|
|
410
|
+
function findDependencyCycle<T extends { key: FrameKey; order: number }>(
|
|
411
|
+
items: T[],
|
|
412
|
+
edges: ReadonlyMap<FrameKey, ReadonlySet<FrameKey>>
|
|
413
|
+
): FrameKey[] | null {
|
|
414
|
+
const visitState = new Map<FrameKey, 0 | 1 | 2>();
|
|
415
|
+
const stack: FrameKey[] = [];
|
|
416
|
+
let cycle: FrameKey[] | null = null;
|
|
417
|
+
const sortedItems = [...items].sort((a, b) => a.order - b.order);
|
|
418
|
+
|
|
419
|
+
const visit = (key: FrameKey): boolean => {
|
|
420
|
+
visitState.set(key, 1);
|
|
421
|
+
stack.push(key);
|
|
422
|
+
|
|
423
|
+
for (const childKey of sortDependencyKeys(edges.get(key) ?? [])) {
|
|
424
|
+
const state = visitState.get(childKey) ?? 0;
|
|
425
|
+
if (state === 0) {
|
|
426
|
+
if (visit(childKey)) {
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (state === 1) {
|
|
433
|
+
const cycleStartIndex = stack.findIndex((entry) => entry === childKey);
|
|
434
|
+
const cyclePath = cycleStartIndex === -1 ? [childKey] : stack.slice(cycleStartIndex);
|
|
435
|
+
cycle = [...cyclePath, childKey];
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
stack.pop();
|
|
441
|
+
visitState.set(key, 2);
|
|
442
|
+
return false;
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
for (const item of sortedItems) {
|
|
446
|
+
if ((visitState.get(item.key) ?? 0) !== 0) {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (visit(item.key)) {
|
|
451
|
+
return cycle;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Topologically sorts items by `before`/`after` dependencies.
|
|
460
|
+
*
|
|
461
|
+
* Throws deterministic errors when dependencies are missing or cyclic.
|
|
462
|
+
*/
|
|
463
|
+
function sortByDependencies<T extends { key: FrameKey; order: number }>(
|
|
464
|
+
items: T[],
|
|
465
|
+
getBefore: (item: T) => Iterable<FrameKey>,
|
|
466
|
+
getAfter: (item: T) => Iterable<FrameKey>,
|
|
467
|
+
options: SortDependenciesOptions<T>
|
|
468
|
+
): T[] {
|
|
469
|
+
const itemsByKey = new Map<FrameKey, T>();
|
|
470
|
+
for (const item of items) {
|
|
471
|
+
itemsByKey.set(item.key, item);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const indegree = new Map<FrameKey, number>();
|
|
475
|
+
const edges = new Map<FrameKey, Set<FrameKey>>();
|
|
476
|
+
|
|
477
|
+
for (const item of items) {
|
|
478
|
+
indegree.set(item.key, 0);
|
|
479
|
+
edges.set(item.key, new Set());
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
for (const item of items) {
|
|
483
|
+
for (const dependencyKey of getAfter(item)) {
|
|
484
|
+
if (!itemsByKey.has(dependencyKey)) {
|
|
485
|
+
if (options.isKnownExternalDependency?.(dependencyKey)) {
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
throw new Error(
|
|
489
|
+
`${options.graphName} dependency error: ${options.getItemLabel(item)} references missing dependency "${frameKeyToString(dependencyKey)}" in "after".`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
edges.get(dependencyKey)?.add(item.key);
|
|
494
|
+
indegree.set(item.key, (indegree.get(item.key) ?? 0) + 1);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
for (const dependencyKey of getBefore(item)) {
|
|
498
|
+
if (!itemsByKey.has(dependencyKey)) {
|
|
499
|
+
if (options.isKnownExternalDependency?.(dependencyKey)) {
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
throw new Error(
|
|
503
|
+
`${options.graphName} dependency error: ${options.getItemLabel(item)} references missing dependency "${frameKeyToString(dependencyKey)}" in "before".`
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
edges.get(item.key)?.add(dependencyKey);
|
|
508
|
+
indegree.set(dependencyKey, (indegree.get(dependencyKey) ?? 0) + 1);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const queue = items.filter((item) => (indegree.get(item.key) ?? 0) === 0);
|
|
513
|
+
queue.sort((a, b) => a.order - b.order);
|
|
514
|
+
|
|
515
|
+
const ordered: T[] = [];
|
|
516
|
+
while (queue.length > 0) {
|
|
517
|
+
const current = queue.shift();
|
|
518
|
+
if (!current) {
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
ordered.push(current);
|
|
523
|
+
|
|
524
|
+
for (const childKey of edges.get(current.key) ?? []) {
|
|
525
|
+
const nextDegree = (indegree.get(childKey) ?? 0) - 1;
|
|
526
|
+
indegree.set(childKey, nextDegree);
|
|
527
|
+
if (nextDegree === 0) {
|
|
528
|
+
const child = itemsByKey.get(childKey);
|
|
529
|
+
if (child) {
|
|
530
|
+
queue.push(child);
|
|
531
|
+
queue.sort((a, b) => a.order - b.order);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (ordered.length !== items.length) {
|
|
538
|
+
const cycle = findDependencyCycle(items, edges);
|
|
539
|
+
if (cycle) {
|
|
540
|
+
throw new Error(
|
|
541
|
+
`${options.graphName} dependency cycle detected: ${cycle.map((key) => frameKeyToString(key)).join(' -> ')}`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
throw new Error(`${options.graphName} dependency resolution failed.`);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return ordered;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Runtime registry that stores frame tasks/stages and drives render scheduling.
|
|
553
|
+
*/
|
|
554
|
+
export interface FrameRegistry {
|
|
555
|
+
/**
|
|
556
|
+
* Registers a frame callback in the scheduler.
|
|
557
|
+
*/
|
|
558
|
+
register: (
|
|
559
|
+
keyOrCallback: FrameKey | FrameCallback,
|
|
560
|
+
callbackOrOptions?: FrameCallback | UseFrameOptions,
|
|
561
|
+
maybeOptions?: UseFrameOptions
|
|
562
|
+
) => RegisteredFrameTask;
|
|
563
|
+
/**
|
|
564
|
+
* Executes one scheduler run.
|
|
565
|
+
*/
|
|
566
|
+
run: (state: FrameState) => void;
|
|
567
|
+
/**
|
|
568
|
+
* Marks frame as invalidated for `on-demand` mode.
|
|
569
|
+
*/
|
|
570
|
+
invalidate: (token?: FrameInvalidationToken) => void;
|
|
571
|
+
/**
|
|
572
|
+
* Requests a single render in `manual` mode.
|
|
573
|
+
*/
|
|
574
|
+
advance: () => void;
|
|
575
|
+
/**
|
|
576
|
+
* Returns whether renderer should submit a frame now.
|
|
577
|
+
*/
|
|
578
|
+
shouldRender: () => boolean;
|
|
579
|
+
/**
|
|
580
|
+
* Resets one-frame invalidation/advance flags.
|
|
581
|
+
*/
|
|
582
|
+
endFrame: () => void;
|
|
583
|
+
/**
|
|
584
|
+
* Sets render scheduling mode.
|
|
585
|
+
*/
|
|
586
|
+
setRenderMode: (mode: RenderMode) => void;
|
|
587
|
+
/**
|
|
588
|
+
* Enables or disables automatic rendering entirely.
|
|
589
|
+
*/
|
|
590
|
+
setAutoRender: (enabled: boolean) => void;
|
|
591
|
+
/**
|
|
592
|
+
* Sets maximum allowed delta passed to frame tasks.
|
|
593
|
+
*/
|
|
594
|
+
setMaxDelta: (value: number) => void;
|
|
595
|
+
/**
|
|
596
|
+
* Enables/disables frame profiling.
|
|
597
|
+
*/
|
|
598
|
+
setProfilingEnabled: (enabled: boolean) => void;
|
|
599
|
+
/**
|
|
600
|
+
* Sets profiling history window (in frames).
|
|
601
|
+
*/
|
|
602
|
+
setProfilingWindow: (window: number) => void;
|
|
603
|
+
/**
|
|
604
|
+
* Clears collected profiling samples.
|
|
605
|
+
*/
|
|
606
|
+
resetProfiling: () => void;
|
|
607
|
+
/**
|
|
608
|
+
* Enables/disables diagnostics capture.
|
|
609
|
+
*/
|
|
610
|
+
setDiagnosticsEnabled: (enabled: boolean) => void;
|
|
611
|
+
/**
|
|
612
|
+
* Returns current render mode.
|
|
613
|
+
*/
|
|
614
|
+
getRenderMode: () => RenderMode;
|
|
615
|
+
/**
|
|
616
|
+
* Returns whether automatic rendering is enabled.
|
|
617
|
+
*/
|
|
618
|
+
getAutoRender: () => boolean;
|
|
619
|
+
/**
|
|
620
|
+
* Returns current max delta clamp.
|
|
621
|
+
*/
|
|
622
|
+
getMaxDelta: () => number;
|
|
623
|
+
/**
|
|
624
|
+
* Returns profiling toggle state.
|
|
625
|
+
*/
|
|
626
|
+
getProfilingEnabled: () => boolean;
|
|
627
|
+
/**
|
|
628
|
+
* Returns active profiling history window (in frames).
|
|
629
|
+
*/
|
|
630
|
+
getProfilingWindow: () => number;
|
|
631
|
+
/**
|
|
632
|
+
* Returns aggregated profiling snapshot.
|
|
633
|
+
*/
|
|
634
|
+
getProfilingSnapshot: () => FrameProfilingSnapshot | null;
|
|
635
|
+
/**
|
|
636
|
+
* Returns diagnostics toggle state.
|
|
637
|
+
*/
|
|
638
|
+
getDiagnosticsEnabled: () => boolean;
|
|
639
|
+
/**
|
|
640
|
+
* Returns last run timings snapshot when diagnostics are enabled.
|
|
641
|
+
*/
|
|
642
|
+
getLastRunTimings: () => FrameRunTimings | null;
|
|
643
|
+
/**
|
|
644
|
+
* Returns dependency-sorted schedule snapshot.
|
|
645
|
+
*/
|
|
646
|
+
getSchedule: () => FrameScheduleSnapshot;
|
|
647
|
+
/**
|
|
648
|
+
* Creates or updates a stage.
|
|
649
|
+
*/
|
|
650
|
+
createStage: (
|
|
651
|
+
key: FrameKey,
|
|
652
|
+
options?: {
|
|
653
|
+
before?: (FrameKey | FrameStage) | (FrameKey | FrameStage)[];
|
|
654
|
+
after?: (FrameKey | FrameStage) | (FrameKey | FrameStage)[];
|
|
655
|
+
callback?: FrameStageCallback | null;
|
|
656
|
+
}
|
|
657
|
+
) => FrameStage;
|
|
658
|
+
/**
|
|
659
|
+
* Reads stage metadata by key.
|
|
660
|
+
*/
|
|
661
|
+
getStage: (key: FrameKey) => FrameStage | undefined;
|
|
662
|
+
/**
|
|
663
|
+
* Removes all tasks from all stages.
|
|
664
|
+
*/
|
|
665
|
+
clear: () => void;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Creates a frame registry used by `FragCanvas` and `useFrame`.
|
|
670
|
+
*
|
|
671
|
+
* @param options - Initial scheduler options.
|
|
672
|
+
* @returns Mutable frame registry instance.
|
|
673
|
+
*/
|
|
674
|
+
export function createFrameRegistry(options?: {
|
|
675
|
+
renderMode?: RenderMode;
|
|
676
|
+
autoRender?: boolean;
|
|
677
|
+
maxDelta?: number;
|
|
678
|
+
profilingEnabled?: boolean;
|
|
679
|
+
profilingWindow?: number;
|
|
680
|
+
diagnosticsEnabled?: boolean;
|
|
681
|
+
}): FrameRegistry {
|
|
682
|
+
let renderMode: RenderMode = options?.renderMode ?? 'always';
|
|
683
|
+
let autoRender = options?.autoRender ?? true;
|
|
684
|
+
let maxDelta = options?.maxDelta ?? 0.1;
|
|
685
|
+
let profilingEnabled = options?.profilingEnabled ?? options?.diagnosticsEnabled ?? false;
|
|
686
|
+
let profilingWindow = options?.profilingWindow ?? 120;
|
|
687
|
+
let lastRunTimings: FrameRunTimings | null = null;
|
|
688
|
+
const profilingHistory: FrameRunTimings[] = [];
|
|
689
|
+
let hasUntokenizedInvalidation = true;
|
|
690
|
+
const invalidationTokens = new Set<FrameInvalidationToken>();
|
|
691
|
+
let shouldAdvance = false;
|
|
692
|
+
let orderCounter = 0;
|
|
693
|
+
|
|
694
|
+
const assertMaxDelta = (value: number): number => {
|
|
695
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
696
|
+
throw new Error('maxDelta must be a finite number greater than 0');
|
|
697
|
+
}
|
|
698
|
+
return value;
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
const assertProfilingWindow = (value: number): number => {
|
|
702
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
703
|
+
throw new Error('profilingWindow must be a finite number greater than 0');
|
|
704
|
+
}
|
|
705
|
+
return Math.floor(value);
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
maxDelta = assertMaxDelta(maxDelta);
|
|
709
|
+
profilingWindow = assertProfilingWindow(profilingWindow);
|
|
710
|
+
|
|
711
|
+
const stages = new Map<FrameKey, InternalStage>();
|
|
712
|
+
let scheduleDirty = true;
|
|
713
|
+
let sortedStages: InternalStage[] = [];
|
|
714
|
+
const sortedTasksByStage = new Map<FrameKey, InternalTask[]>();
|
|
715
|
+
let scheduleSnapshot: FrameScheduleSnapshot = { stages: [] };
|
|
716
|
+
|
|
717
|
+
const markScheduleDirty = (): void => {
|
|
718
|
+
scheduleDirty = true;
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
const syncSchedule = (): void => {
|
|
722
|
+
if (!scheduleDirty) {
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const stageList = sortByDependencies(
|
|
727
|
+
Array.from(stages.values()),
|
|
728
|
+
(stage) => stage.before,
|
|
729
|
+
(stage) => stage.after,
|
|
730
|
+
{
|
|
731
|
+
graphName: 'Frame stage graph',
|
|
732
|
+
getItemLabel: (stage) => `stage "${frameKeyToString(stage.key)}"`
|
|
733
|
+
}
|
|
734
|
+
);
|
|
735
|
+
const nextTasksByStage = new Map<FrameKey, InternalTask[]>();
|
|
736
|
+
const globalTaskKeys = new Set<FrameKey>();
|
|
737
|
+
for (const stage of stageList) {
|
|
738
|
+
for (const task of stage.tasks.values()) {
|
|
739
|
+
globalTaskKeys.add(task.task.key);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
for (const stage of stageList) {
|
|
744
|
+
const taskList = sortByDependencies(
|
|
745
|
+
Array.from(stage.tasks.values()).map((task) => ({
|
|
746
|
+
key: task.task.key,
|
|
747
|
+
order: task.order,
|
|
748
|
+
task
|
|
749
|
+
})),
|
|
750
|
+
(task) => task.task.before,
|
|
751
|
+
(task) => task.task.after,
|
|
752
|
+
{
|
|
753
|
+
graphName: `Frame task graph for stage "${frameKeyToString(stage.key)}"`,
|
|
754
|
+
getItemLabel: (task) => `task "${frameKeyToString(task.key)}"`,
|
|
755
|
+
isKnownExternalDependency: (key) => globalTaskKeys.has(key)
|
|
756
|
+
}
|
|
757
|
+
).map((task) => task.task);
|
|
758
|
+
nextTasksByStage.set(stage.key, taskList);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
sortedStages = stageList;
|
|
762
|
+
sortedTasksByStage.clear();
|
|
763
|
+
for (const [stageKey, taskList] of nextTasksByStage) {
|
|
764
|
+
sortedTasksByStage.set(stageKey, taskList);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
scheduleSnapshot = {
|
|
768
|
+
stages: sortedStages.map((stage) => ({
|
|
769
|
+
key: frameKeyToString(stage.key),
|
|
770
|
+
tasks: (sortedTasksByStage.get(stage.key) ?? []).map((task) =>
|
|
771
|
+
frameKeyToString(task.task.key)
|
|
772
|
+
)
|
|
773
|
+
}))
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
scheduleDirty = false;
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
const pushProfile = (timings: FrameRunTimings): void => {
|
|
780
|
+
profilingHistory.push(timings);
|
|
781
|
+
while (profilingHistory.length > profilingWindow) {
|
|
782
|
+
profilingHistory.shift();
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
const clearProfiling = (): void => {
|
|
787
|
+
profilingHistory.length = 0;
|
|
788
|
+
lastRunTimings = null;
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
const buildProfilingSnapshot = (): FrameProfilingSnapshot | null => {
|
|
792
|
+
if (!profilingEnabled) {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const stageBuckets = new Map<
|
|
797
|
+
string,
|
|
798
|
+
{
|
|
799
|
+
durations: number[];
|
|
800
|
+
taskDurations: Map<string, number[]>;
|
|
801
|
+
}
|
|
802
|
+
>();
|
|
803
|
+
const totalDurations: number[] = [];
|
|
804
|
+
|
|
805
|
+
for (const frame of profilingHistory) {
|
|
806
|
+
totalDurations.push(frame.total);
|
|
807
|
+
for (const [stageKey, stageTiming] of Object.entries(frame.stages)) {
|
|
808
|
+
const stageBucket = stageBuckets.get(stageKey) ?? {
|
|
809
|
+
durations: [],
|
|
810
|
+
taskDurations: new Map<string, number[]>()
|
|
811
|
+
};
|
|
812
|
+
stageBucket.durations.push(stageTiming.duration);
|
|
813
|
+
|
|
814
|
+
for (const [taskKey, taskDuration] of Object.entries(stageTiming.tasks)) {
|
|
815
|
+
const bucket = stageBucket.taskDurations.get(taskKey) ?? [];
|
|
816
|
+
bucket.push(taskDuration);
|
|
817
|
+
stageBucket.taskDurations.set(taskKey, bucket);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
stageBuckets.set(stageKey, stageBucket);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const stagesSnapshot: FrameProfilingSnapshot['stages'] = {};
|
|
825
|
+
for (const [stageKey, stageBucket] of stageBuckets) {
|
|
826
|
+
const lastStageDuration = lastRunTimings?.stages[stageKey]?.duration ?? 0;
|
|
827
|
+
const taskSnapshot: Record<string, FrameTimingStats> = {};
|
|
828
|
+
for (const [taskKey, taskDurations] of stageBucket.taskDurations) {
|
|
829
|
+
const lastTaskDuration = lastRunTimings?.stages[stageKey]?.tasks[taskKey] ?? 0;
|
|
830
|
+
taskSnapshot[taskKey] = buildTimingStats(taskDurations, lastTaskDuration);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
stagesSnapshot[stageKey] = {
|
|
834
|
+
timings: buildTimingStats(stageBucket.durations, lastStageDuration),
|
|
835
|
+
tasks: taskSnapshot
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return {
|
|
840
|
+
window: profilingWindow,
|
|
841
|
+
frameCount: profilingHistory.length,
|
|
842
|
+
lastFrame: lastRunTimings,
|
|
843
|
+
total: buildTimingStats(totalDurations, lastRunTimings?.total ?? 0),
|
|
844
|
+
stages: stagesSnapshot
|
|
845
|
+
};
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
const ensureStage = (
|
|
849
|
+
stageReference: FrameKey | FrameStage,
|
|
850
|
+
stageOptions?: {
|
|
851
|
+
before?: (FrameKey | FrameStage)[];
|
|
852
|
+
after?: (FrameKey | FrameStage)[];
|
|
853
|
+
callback?: FrameStageCallback | null;
|
|
854
|
+
}
|
|
855
|
+
): InternalStage => {
|
|
856
|
+
const stageKey = toStageKey(stageReference);
|
|
857
|
+
const existing = stages.get(stageKey);
|
|
858
|
+
if (existing) {
|
|
859
|
+
if (stageOptions?.before !== undefined) {
|
|
860
|
+
existing.before = new Set(stageOptions.before.map((entry) => toStageKey(entry)));
|
|
861
|
+
markScheduleDirty();
|
|
862
|
+
}
|
|
863
|
+
if (stageOptions?.after !== undefined) {
|
|
864
|
+
existing.after = new Set(stageOptions.after.map((entry) => toStageKey(entry)));
|
|
865
|
+
markScheduleDirty();
|
|
866
|
+
}
|
|
867
|
+
if (stageOptions && Object.prototype.hasOwnProperty.call(stageOptions, 'callback')) {
|
|
868
|
+
existing.callback = stageOptions.callback ?? DEFAULT_STAGE_CALLBACK;
|
|
869
|
+
}
|
|
870
|
+
return existing;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const stage: InternalStage = {
|
|
874
|
+
key: stageKey,
|
|
875
|
+
order: orderCounter++,
|
|
876
|
+
started: true,
|
|
877
|
+
before: new Set((stageOptions?.before ?? []).map((entry) => toStageKey(entry))),
|
|
878
|
+
after: new Set((stageOptions?.after ?? []).map((entry) => toStageKey(entry))),
|
|
879
|
+
callback: stageOptions?.callback ?? DEFAULT_STAGE_CALLBACK,
|
|
880
|
+
tasks: new Map()
|
|
881
|
+
};
|
|
882
|
+
stages.set(stageKey, stage);
|
|
883
|
+
markScheduleDirty();
|
|
884
|
+
return stage;
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
ensureStage(MAIN_STAGE_KEY);
|
|
888
|
+
|
|
889
|
+
const resolveEffectiveRunning = (task: InternalTask): boolean => {
|
|
890
|
+
const running = task.started && (task.running?.() ?? true);
|
|
891
|
+
if (task.lastRunning !== running) {
|
|
892
|
+
task.lastRunning = running;
|
|
893
|
+
task.startedStoreSet(running);
|
|
894
|
+
}
|
|
895
|
+
return running;
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
const hasPendingInvalidation = (): boolean => {
|
|
899
|
+
return hasUntokenizedInvalidation || invalidationTokens.size > 0;
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
const invalidateWithToken = (token?: FrameInvalidationToken): void => {
|
|
903
|
+
if (token === undefined) {
|
|
904
|
+
hasUntokenizedInvalidation = true;
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
invalidationTokens.add(token);
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
const applyTaskInvalidation = (task: InternalTask): void => {
|
|
912
|
+
const config = task.invalidation;
|
|
913
|
+
if (config.mode === 'never') {
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (config.mode === 'always') {
|
|
918
|
+
const token = resolveInvalidationToken(config.token);
|
|
919
|
+
invalidateWithToken(token ?? task.task.key);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const token = resolveInvalidationToken(config.token);
|
|
924
|
+
if (token === null) {
|
|
925
|
+
config.hasToken = false;
|
|
926
|
+
config.lastToken = null;
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const changed = !config.hasToken || config.lastToken !== token;
|
|
931
|
+
config.hasToken = true;
|
|
932
|
+
config.lastToken = token;
|
|
933
|
+
if (changed) {
|
|
934
|
+
invalidateWithToken(token);
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
return {
|
|
939
|
+
register(keyOrCallback, callbackOrOptions, maybeOptions) {
|
|
940
|
+
const key =
|
|
941
|
+
typeof keyOrCallback === 'function'
|
|
942
|
+
? (Symbol('motiongpu-task') as FrameKey)
|
|
943
|
+
: (keyOrCallback as FrameKey);
|
|
944
|
+
const callback =
|
|
945
|
+
typeof keyOrCallback === 'function' ? keyOrCallback : (callbackOrOptions as FrameCallback);
|
|
946
|
+
const taskOptions =
|
|
947
|
+
typeof keyOrCallback === 'function'
|
|
948
|
+
? ((callbackOrOptions as UseFrameOptions | undefined) ?? {})
|
|
949
|
+
: (maybeOptions ?? {});
|
|
950
|
+
|
|
951
|
+
if (typeof callback !== 'function') {
|
|
952
|
+
throw new Error('useFrame requires a callback');
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const before = asArray(taskOptions.before);
|
|
956
|
+
const after = asArray(taskOptions.after);
|
|
957
|
+
const inferredStage = [...before, ...after].find(
|
|
958
|
+
(entry) => typeof entry === 'object' && entry !== null && 'stage' in entry
|
|
959
|
+
) as FrameTask | undefined;
|
|
960
|
+
const stageKey = taskOptions.stage
|
|
961
|
+
? toStageKey(taskOptions.stage)
|
|
962
|
+
: (inferredStage?.stage ?? MAIN_STAGE_KEY);
|
|
963
|
+
|
|
964
|
+
const stage = ensureStage(stageKey);
|
|
965
|
+
const startedWritable: CurrentWritable<boolean> = createCurrentWritable(
|
|
966
|
+
taskOptions.autoStart ?? true
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
const internalTask: InternalTask = {
|
|
970
|
+
task: { key, stage: stage.key },
|
|
971
|
+
callback,
|
|
972
|
+
order: orderCounter++,
|
|
973
|
+
started: taskOptions.autoStart ?? true,
|
|
974
|
+
lastRunning: taskOptions.autoStart ?? true,
|
|
975
|
+
startedStoreSet: startedWritable.set,
|
|
976
|
+
startedStore: { subscribe: startedWritable.subscribe },
|
|
977
|
+
before: new Set(before.map((entry) => toTaskKey(entry))),
|
|
978
|
+
after: new Set(after.map((entry) => toTaskKey(entry))),
|
|
979
|
+
invalidation: normalizeTaskInvalidation(key, taskOptions)
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
if (taskOptions.running) {
|
|
983
|
+
internalTask.running = taskOptions.running;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
stage.tasks.set(key, internalTask);
|
|
987
|
+
markScheduleDirty();
|
|
988
|
+
internalTask.startedStoreSet(resolveEffectiveRunning(internalTask));
|
|
989
|
+
|
|
990
|
+
const start = () => {
|
|
991
|
+
internalTask.started = true;
|
|
992
|
+
resolveEffectiveRunning(internalTask);
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
const stop = () => {
|
|
996
|
+
internalTask.started = false;
|
|
997
|
+
resolveEffectiveRunning(internalTask);
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
return {
|
|
1001
|
+
task: internalTask.task,
|
|
1002
|
+
start,
|
|
1003
|
+
stop,
|
|
1004
|
+
started: internalTask.startedStore,
|
|
1005
|
+
unsubscribe: () => {
|
|
1006
|
+
if (stage.tasks.delete(key)) {
|
|
1007
|
+
markScheduleDirty();
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
},
|
|
1012
|
+
run(state) {
|
|
1013
|
+
const clampedDelta = Math.min(state.delta, maxDelta);
|
|
1014
|
+
const frameState =
|
|
1015
|
+
clampedDelta === state.delta
|
|
1016
|
+
? state
|
|
1017
|
+
: {
|
|
1018
|
+
...state,
|
|
1019
|
+
delta: clampedDelta
|
|
1020
|
+
};
|
|
1021
|
+
syncSchedule();
|
|
1022
|
+
const frameStart = profilingEnabled ? performance.now() : 0;
|
|
1023
|
+
const stageTimings: FrameRunTimings['stages'] = {};
|
|
1024
|
+
|
|
1025
|
+
for (const stage of sortedStages) {
|
|
1026
|
+
if (!stage.started) {
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
const stageStart = profilingEnabled ? performance.now() : 0;
|
|
1030
|
+
const taskTimings: Record<string, number> = {};
|
|
1031
|
+
const taskList = sortedTasksByStage.get(stage.key) ?? [];
|
|
1032
|
+
|
|
1033
|
+
stage.callback(frameState, () => {
|
|
1034
|
+
for (const task of taskList) {
|
|
1035
|
+
if (!resolveEffectiveRunning(task)) {
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
const taskStart = profilingEnabled ? performance.now() : 0;
|
|
1039
|
+
|
|
1040
|
+
task.callback(frameState);
|
|
1041
|
+
if (profilingEnabled) {
|
|
1042
|
+
taskTimings[frameKeyToString(task.task.key)] = performance.now() - taskStart;
|
|
1043
|
+
}
|
|
1044
|
+
applyTaskInvalidation(task);
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
if (profilingEnabled) {
|
|
1049
|
+
stageTimings[frameKeyToString(stage.key)] = {
|
|
1050
|
+
duration: performance.now() - stageStart,
|
|
1051
|
+
tasks: taskTimings
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (profilingEnabled) {
|
|
1057
|
+
const timings = {
|
|
1058
|
+
total: performance.now() - frameStart,
|
|
1059
|
+
stages: stageTimings
|
|
1060
|
+
};
|
|
1061
|
+
lastRunTimings = timings;
|
|
1062
|
+
pushProfile(timings);
|
|
1063
|
+
}
|
|
1064
|
+
},
|
|
1065
|
+
invalidate(token) {
|
|
1066
|
+
invalidateWithToken(token);
|
|
1067
|
+
},
|
|
1068
|
+
advance() {
|
|
1069
|
+
shouldAdvance = true;
|
|
1070
|
+
invalidateWithToken();
|
|
1071
|
+
},
|
|
1072
|
+
shouldRender() {
|
|
1073
|
+
if (!autoRender) {
|
|
1074
|
+
return false;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (renderMode === 'always') {
|
|
1078
|
+
return true;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (renderMode === 'on-demand') {
|
|
1082
|
+
return shouldAdvance || hasPendingInvalidation();
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
return shouldAdvance;
|
|
1086
|
+
},
|
|
1087
|
+
endFrame() {
|
|
1088
|
+
hasUntokenizedInvalidation = false;
|
|
1089
|
+
invalidationTokens.clear();
|
|
1090
|
+
shouldAdvance = false;
|
|
1091
|
+
},
|
|
1092
|
+
setRenderMode(mode) {
|
|
1093
|
+
if (renderMode === mode) {
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
renderMode = mode;
|
|
1098
|
+
shouldAdvance = false;
|
|
1099
|
+
if (mode === 'on-demand') {
|
|
1100
|
+
invalidateWithToken(RENDER_MODE_INVALIDATION_TOKEN);
|
|
1101
|
+
}
|
|
1102
|
+
},
|
|
1103
|
+
setAutoRender(enabled) {
|
|
1104
|
+
autoRender = enabled;
|
|
1105
|
+
},
|
|
1106
|
+
setMaxDelta(value) {
|
|
1107
|
+
maxDelta = assertMaxDelta(value);
|
|
1108
|
+
},
|
|
1109
|
+
setProfilingEnabled(enabled) {
|
|
1110
|
+
profilingEnabled = enabled;
|
|
1111
|
+
if (!enabled) {
|
|
1112
|
+
clearProfiling();
|
|
1113
|
+
}
|
|
1114
|
+
},
|
|
1115
|
+
setProfilingWindow(window) {
|
|
1116
|
+
profilingWindow = assertProfilingWindow(window);
|
|
1117
|
+
while (profilingHistory.length > profilingWindow) {
|
|
1118
|
+
profilingHistory.shift();
|
|
1119
|
+
}
|
|
1120
|
+
},
|
|
1121
|
+
resetProfiling() {
|
|
1122
|
+
clearProfiling();
|
|
1123
|
+
},
|
|
1124
|
+
setDiagnosticsEnabled(enabled) {
|
|
1125
|
+
profilingEnabled = enabled;
|
|
1126
|
+
if (!enabled) {
|
|
1127
|
+
clearProfiling();
|
|
1128
|
+
}
|
|
1129
|
+
},
|
|
1130
|
+
getRenderMode() {
|
|
1131
|
+
return renderMode;
|
|
1132
|
+
},
|
|
1133
|
+
getAutoRender() {
|
|
1134
|
+
return autoRender;
|
|
1135
|
+
},
|
|
1136
|
+
getMaxDelta() {
|
|
1137
|
+
return maxDelta;
|
|
1138
|
+
},
|
|
1139
|
+
getProfilingEnabled() {
|
|
1140
|
+
return profilingEnabled;
|
|
1141
|
+
},
|
|
1142
|
+
getProfilingWindow() {
|
|
1143
|
+
return profilingWindow;
|
|
1144
|
+
},
|
|
1145
|
+
getProfilingSnapshot() {
|
|
1146
|
+
return buildProfilingSnapshot();
|
|
1147
|
+
},
|
|
1148
|
+
getDiagnosticsEnabled() {
|
|
1149
|
+
return profilingEnabled;
|
|
1150
|
+
},
|
|
1151
|
+
getLastRunTimings() {
|
|
1152
|
+
return lastRunTimings;
|
|
1153
|
+
},
|
|
1154
|
+
getSchedule() {
|
|
1155
|
+
syncSchedule();
|
|
1156
|
+
return scheduleSnapshot;
|
|
1157
|
+
},
|
|
1158
|
+
createStage(key, options) {
|
|
1159
|
+
const stageOptions: Parameters<typeof ensureStage>[1] | undefined = options
|
|
1160
|
+
? {
|
|
1161
|
+
...(Object.prototype.hasOwnProperty.call(options, 'before')
|
|
1162
|
+
? { before: asArray(options.before) }
|
|
1163
|
+
: {}),
|
|
1164
|
+
...(Object.prototype.hasOwnProperty.call(options, 'after')
|
|
1165
|
+
? { after: asArray(options.after) }
|
|
1166
|
+
: {}),
|
|
1167
|
+
...(Object.prototype.hasOwnProperty.call(options, 'callback')
|
|
1168
|
+
? { callback: options.callback ?? null }
|
|
1169
|
+
: {})
|
|
1170
|
+
}
|
|
1171
|
+
: undefined;
|
|
1172
|
+
const stage = ensureStage(key, stageOptions);
|
|
1173
|
+
return { key: stage.key };
|
|
1174
|
+
},
|
|
1175
|
+
getStage(key) {
|
|
1176
|
+
const stage = stages.get(key);
|
|
1177
|
+
if (!stage) {
|
|
1178
|
+
return undefined;
|
|
1179
|
+
}
|
|
1180
|
+
return { key: stage.key };
|
|
1181
|
+
},
|
|
1182
|
+
clear() {
|
|
1183
|
+
for (const stage of stages.values()) {
|
|
1184
|
+
stage.tasks.clear();
|
|
1185
|
+
}
|
|
1186
|
+
markScheduleDirty();
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
}
|