@motion-core/motion-gpu 0.2.0 → 0.3.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/README.md +6 -0
- package/dist/core/current-value.js +3 -0
- package/dist/core/error-diagnostics.d.ts +14 -0
- package/dist/core/error-diagnostics.js +41 -1
- package/dist/core/error-report.d.ts +37 -0
- package/dist/core/error-report.js +60 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/renderer.d.ts +10 -0
- package/dist/core/renderer.js +66 -4
- package/dist/core/runtime-loop.d.ts +3 -0
- package/dist/core/runtime-loop.js +72 -1
- package/dist/core/types.d.ts +4 -0
- package/dist/passes/BlitPass.d.ts +6 -27
- package/dist/passes/BlitPass.js +10 -121
- package/dist/passes/FullscreenPass.d.ts +37 -0
- package/dist/passes/FullscreenPass.js +131 -0
- package/dist/passes/ShaderPass.d.ts +6 -26
- package/dist/passes/ShaderPass.js +10 -121
- package/dist/svelte/FragCanvas.svelte +43 -0
- package/dist/svelte/FragCanvas.svelte.d.ts +2 -0
- package/dist/svelte/MotionGPUErrorOverlay.svelte +10 -19
- package/dist/svelte/use-texture.d.ts +5 -0
- package/dist/svelte/use-texture.js +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -157,6 +157,12 @@ npm i @motion-core/motion-gpu
|
|
|
157
157
|
|
|
158
158
|
---
|
|
159
159
|
|
|
160
|
+
# AI Documentation
|
|
161
|
+
|
|
162
|
+
MotionGPU documentation is also available for AI tools via [Context7](https://context7.com/motion-core/motion-gpu).
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
160
166
|
# Quick Start
|
|
161
167
|
|
|
162
168
|
## 1. Create a material and render it
|
|
@@ -19,6 +19,19 @@ export interface ShaderCompilationDiagnostic {
|
|
|
19
19
|
lineLength?: number;
|
|
20
20
|
sourceLocation: MaterialSourceLocation | null;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Runtime context snapshot captured for shader compilation diagnostics.
|
|
24
|
+
*/
|
|
25
|
+
export interface ShaderCompilationRuntimeContext {
|
|
26
|
+
materialSignature?: string;
|
|
27
|
+
passGraph?: {
|
|
28
|
+
passCount: number;
|
|
29
|
+
enabledPassCount: number;
|
|
30
|
+
inputs: string[];
|
|
31
|
+
outputs: string[];
|
|
32
|
+
};
|
|
33
|
+
activeRenderTargets: string[];
|
|
34
|
+
}
|
|
22
35
|
/**
|
|
23
36
|
* Structured payload attached to WGSL compilation errors.
|
|
24
37
|
*/
|
|
@@ -29,6 +42,7 @@ export interface ShaderCompilationDiagnosticsPayload {
|
|
|
29
42
|
includeSources: Record<string, string>;
|
|
30
43
|
defineBlockSource?: string;
|
|
31
44
|
materialSource: MaterialSourceMetadata | null;
|
|
45
|
+
runtimeContext?: ShaderCompilationRuntimeContext;
|
|
32
46
|
}
|
|
33
47
|
/**
|
|
34
48
|
* Attaches structured diagnostics payload to an Error.
|
|
@@ -56,6 +56,39 @@ function isShaderCompilationDiagnostic(value) {
|
|
|
56
56
|
}
|
|
57
57
|
return true;
|
|
58
58
|
}
|
|
59
|
+
function isStringArray(value) {
|
|
60
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
|
|
61
|
+
}
|
|
62
|
+
function isShaderCompilationRuntimeContext(value) {
|
|
63
|
+
if (value === null || typeof value !== 'object') {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const record = value;
|
|
67
|
+
if (record.materialSignature !== undefined && typeof record.materialSignature !== 'string') {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (!isStringArray(record.activeRenderTargets)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const passGraph = record.passGraph;
|
|
74
|
+
if (passGraph === undefined) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
if (passGraph === null || typeof passGraph !== 'object') {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const passGraphRecord = passGraph;
|
|
81
|
+
if (typeof passGraphRecord.passCount !== 'number') {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (typeof passGraphRecord.enabledPassCount !== 'number') {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (!isStringArray(passGraphRecord.inputs) || !isStringArray(passGraphRecord.outputs)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
59
92
|
/**
|
|
60
93
|
* Attaches structured diagnostics payload to an Error.
|
|
61
94
|
*/
|
|
@@ -98,6 +131,10 @@ export function getShaderCompilationDiagnostics(error) {
|
|
|
98
131
|
if (record.materialSource !== null && !isMaterialSourceMetadata(record.materialSource)) {
|
|
99
132
|
return null;
|
|
100
133
|
}
|
|
134
|
+
if (record.runtimeContext !== undefined &&
|
|
135
|
+
!isShaderCompilationRuntimeContext(record.runtimeContext)) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
101
138
|
return {
|
|
102
139
|
kind: 'shader-compilation',
|
|
103
140
|
diagnostics: record.diagnostics,
|
|
@@ -106,6 +143,9 @@ export function getShaderCompilationDiagnostics(error) {
|
|
|
106
143
|
...(record.defineBlockSource !== undefined
|
|
107
144
|
? { defineBlockSource: record.defineBlockSource }
|
|
108
145
|
: {}),
|
|
109
|
-
materialSource: (record.materialSource ?? null)
|
|
146
|
+
materialSource: (record.materialSource ?? null),
|
|
147
|
+
...(record.runtimeContext !== undefined
|
|
148
|
+
? { runtimeContext: record.runtimeContext }
|
|
149
|
+
: {})
|
|
110
150
|
};
|
|
111
151
|
}
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
* Runtime phase in which an error occurred.
|
|
3
3
|
*/
|
|
4
4
|
export type MotionGPUErrorPhase = 'initialization' | 'render';
|
|
5
|
+
/**
|
|
6
|
+
* Stable machine-readable error category code.
|
|
7
|
+
*/
|
|
8
|
+
export type MotionGPUErrorCode = 'WEBGPU_UNAVAILABLE' | 'WEBGPU_ADAPTER_UNAVAILABLE' | 'WEBGPU_CONTEXT_UNAVAILABLE' | 'WGSL_COMPILATION_FAILED' | 'WEBGPU_DEVICE_LOST' | 'WEBGPU_UNCAPTURED_ERROR' | 'BIND_GROUP_MISMATCH' | 'TEXTURE_USAGE_INVALID' | 'TEXTURE_REQUEST_FAILED' | 'TEXTURE_DECODE_UNAVAILABLE' | 'TEXTURE_REQUEST_ABORTED' | 'MOTIONGPU_RUNTIME_ERROR';
|
|
9
|
+
/**
|
|
10
|
+
* Severity level for user-facing diagnostics.
|
|
11
|
+
*/
|
|
12
|
+
export type MotionGPUErrorSeverity = 'error' | 'fatal';
|
|
5
13
|
/**
|
|
6
14
|
* One source-code line displayed in diagnostics snippet.
|
|
7
15
|
*/
|
|
@@ -20,10 +28,35 @@ export interface MotionGPUErrorSource {
|
|
|
20
28
|
column?: number;
|
|
21
29
|
snippet: MotionGPUErrorSourceLine[];
|
|
22
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Optional runtime context captured with diagnostics payload.
|
|
33
|
+
*/
|
|
34
|
+
export interface MotionGPUErrorContext {
|
|
35
|
+
materialSignature?: string;
|
|
36
|
+
passGraph?: {
|
|
37
|
+
passCount: number;
|
|
38
|
+
enabledPassCount: number;
|
|
39
|
+
inputs: string[];
|
|
40
|
+
outputs: string[];
|
|
41
|
+
};
|
|
42
|
+
activeRenderTargets: string[];
|
|
43
|
+
}
|
|
23
44
|
/**
|
|
24
45
|
* Structured error payload used by UI diagnostics.
|
|
25
46
|
*/
|
|
26
47
|
export interface MotionGPUErrorReport {
|
|
48
|
+
/**
|
|
49
|
+
* Stable machine-readable category code.
|
|
50
|
+
*/
|
|
51
|
+
code: MotionGPUErrorCode;
|
|
52
|
+
/**
|
|
53
|
+
* Severity level used by diagnostics UIs and telemetry.
|
|
54
|
+
*/
|
|
55
|
+
severity: MotionGPUErrorSeverity;
|
|
56
|
+
/**
|
|
57
|
+
* Whether runtime may recover without full renderer re-creation.
|
|
58
|
+
*/
|
|
59
|
+
recoverable: boolean;
|
|
27
60
|
/**
|
|
28
61
|
* Short category title.
|
|
29
62
|
*/
|
|
@@ -56,6 +89,10 @@ export interface MotionGPUErrorReport {
|
|
|
56
89
|
* Optional source context for shader-related diagnostics.
|
|
57
90
|
*/
|
|
58
91
|
source: MotionGPUErrorSource | null;
|
|
92
|
+
/**
|
|
93
|
+
* Optional runtime context snapshot (material/pass graph/render targets).
|
|
94
|
+
*/
|
|
95
|
+
context: MotionGPUErrorContext | null;
|
|
59
96
|
}
|
|
60
97
|
/**
|
|
61
98
|
* Converts unknown errors to a consistent, display-ready error report.
|
|
@@ -98,53 +98,107 @@ function formatDiagnosticMessage(entry) {
|
|
|
98
98
|
function classifyErrorMessage(message) {
|
|
99
99
|
if (message.includes('WebGPU is not available in this browser')) {
|
|
100
100
|
return {
|
|
101
|
+
code: 'WEBGPU_UNAVAILABLE',
|
|
102
|
+
severity: 'fatal',
|
|
103
|
+
recoverable: false,
|
|
101
104
|
title: 'WebGPU unavailable',
|
|
102
105
|
hint: 'Use a browser with WebGPU enabled (latest Chrome/Edge/Safari TP) and secure context.'
|
|
103
106
|
};
|
|
104
107
|
}
|
|
105
108
|
if (message.includes('Unable to acquire WebGPU adapter')) {
|
|
106
109
|
return {
|
|
110
|
+
code: 'WEBGPU_ADAPTER_UNAVAILABLE',
|
|
111
|
+
severity: 'fatal',
|
|
112
|
+
recoverable: false,
|
|
107
113
|
title: 'WebGPU adapter unavailable',
|
|
108
114
|
hint: 'GPU adapter request failed. Check browser permissions, flags and device support.'
|
|
109
115
|
};
|
|
110
116
|
}
|
|
111
117
|
if (message.includes('Canvas does not support webgpu context')) {
|
|
112
118
|
return {
|
|
119
|
+
code: 'WEBGPU_CONTEXT_UNAVAILABLE',
|
|
120
|
+
severity: 'error',
|
|
121
|
+
recoverable: true,
|
|
113
122
|
title: 'Canvas cannot create WebGPU context',
|
|
114
123
|
hint: 'Make sure this canvas is attached to DOM and not using an unsupported context option.'
|
|
115
124
|
};
|
|
116
125
|
}
|
|
117
126
|
if (message.includes('WGSL compilation failed')) {
|
|
118
127
|
return {
|
|
128
|
+
code: 'WGSL_COMPILATION_FAILED',
|
|
129
|
+
severity: 'error',
|
|
130
|
+
recoverable: true,
|
|
119
131
|
title: 'WGSL compilation failed',
|
|
120
132
|
hint: 'Check WGSL line numbers below and verify struct/binding/function signatures.'
|
|
121
133
|
};
|
|
122
134
|
}
|
|
123
135
|
if (message.includes('WebGPU device lost') || message.includes('Device Lost')) {
|
|
124
136
|
return {
|
|
137
|
+
code: 'WEBGPU_DEVICE_LOST',
|
|
138
|
+
severity: 'fatal',
|
|
139
|
+
recoverable: false,
|
|
125
140
|
title: 'WebGPU device lost',
|
|
126
141
|
hint: 'GPU device/context was lost. Recreate the renderer and check OS/GPU stability.'
|
|
127
142
|
};
|
|
128
143
|
}
|
|
129
144
|
if (message.includes('WebGPU uncaptured error')) {
|
|
130
145
|
return {
|
|
146
|
+
code: 'WEBGPU_UNCAPTURED_ERROR',
|
|
147
|
+
severity: 'error',
|
|
148
|
+
recoverable: true,
|
|
131
149
|
title: 'WebGPU uncaptured error',
|
|
132
150
|
hint: 'A GPU command failed asynchronously. Review details and validate resource/state usage.'
|
|
133
151
|
};
|
|
134
152
|
}
|
|
135
153
|
if (message.includes('CreateBindGroup') || message.includes('bind group layout')) {
|
|
136
154
|
return {
|
|
155
|
+
code: 'BIND_GROUP_MISMATCH',
|
|
156
|
+
severity: 'error',
|
|
157
|
+
recoverable: true,
|
|
137
158
|
title: 'Bind group mismatch',
|
|
138
159
|
hint: 'Bindings in shader and runtime resources are out of sync. Verify uniforms/textures layout.'
|
|
139
160
|
};
|
|
140
161
|
}
|
|
141
162
|
if (message.includes('Destination texture needs to have CopyDst')) {
|
|
142
163
|
return {
|
|
164
|
+
code: 'TEXTURE_USAGE_INVALID',
|
|
165
|
+
severity: 'error',
|
|
166
|
+
recoverable: true,
|
|
143
167
|
title: 'Invalid texture usage flags',
|
|
144
168
|
hint: 'Texture used as upload destination must include CopyDst (and often RenderAttachment).'
|
|
145
169
|
};
|
|
146
170
|
}
|
|
171
|
+
if (message.includes('Texture request failed')) {
|
|
172
|
+
return {
|
|
173
|
+
code: 'TEXTURE_REQUEST_FAILED',
|
|
174
|
+
severity: 'error',
|
|
175
|
+
recoverable: true,
|
|
176
|
+
title: 'Texture request failed',
|
|
177
|
+
hint: 'Verify texture URL, CORS policy and response status before retrying.'
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (message.includes('createImageBitmap is not available in this runtime')) {
|
|
181
|
+
return {
|
|
182
|
+
code: 'TEXTURE_DECODE_UNAVAILABLE',
|
|
183
|
+
severity: 'fatal',
|
|
184
|
+
recoverable: false,
|
|
185
|
+
title: 'Texture decode unavailable',
|
|
186
|
+
hint: 'Runtime lacks createImageBitmap support. Use a browser/runtime with image bitmap decoding.'
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (message.toLowerCase().includes('texture request was aborted')) {
|
|
190
|
+
return {
|
|
191
|
+
code: 'TEXTURE_REQUEST_ABORTED',
|
|
192
|
+
severity: 'error',
|
|
193
|
+
recoverable: true,
|
|
194
|
+
title: 'Texture request aborted',
|
|
195
|
+
hint: 'Texture load was cancelled. Retry the request when source inputs stabilize.'
|
|
196
|
+
};
|
|
197
|
+
}
|
|
147
198
|
return {
|
|
199
|
+
code: 'MOTIONGPU_RUNTIME_ERROR',
|
|
200
|
+
severity: 'error',
|
|
201
|
+
recoverable: true,
|
|
148
202
|
title: 'MotionGPU render error',
|
|
149
203
|
hint: 'Review technical details below. If issue persists, isolate shader/uniform/texture changes.'
|
|
150
204
|
};
|
|
@@ -167,6 +221,7 @@ export function toMotionGPUErrorReport(error, phase) {
|
|
|
167
221
|
const defaultMessage = rawLines[0] ?? rawMessage;
|
|
168
222
|
const defaultDetails = rawLines.slice(1);
|
|
169
223
|
const source = buildSourceFromDiagnostics(error);
|
|
224
|
+
const context = shaderDiagnostics?.runtimeContext ?? null;
|
|
170
225
|
const message = shaderDiagnostics && shaderDiagnostics.diagnostics[0]
|
|
171
226
|
? formatDiagnosticMessage(shaderDiagnostics.diagnostics[0])
|
|
172
227
|
: defaultMessage;
|
|
@@ -178,6 +233,9 @@ export function toMotionGPUErrorReport(error, phase) {
|
|
|
178
233
|
: [];
|
|
179
234
|
const classification = classifyErrorMessage(rawMessage);
|
|
180
235
|
return {
|
|
236
|
+
code: classification.code,
|
|
237
|
+
severity: classification.severity,
|
|
238
|
+
recoverable: classification.recoverable,
|
|
181
239
|
title: classification.title,
|
|
182
240
|
message,
|
|
183
241
|
hint: classification.hint,
|
|
@@ -185,6 +243,7 @@ export function toMotionGPUErrorReport(error, phase) {
|
|
|
185
243
|
stack,
|
|
186
244
|
rawMessage,
|
|
187
245
|
phase,
|
|
188
|
-
source
|
|
246
|
+
source,
|
|
247
|
+
context
|
|
189
248
|
};
|
|
190
249
|
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { createMotionGPURuntimeLoop } from './runtime-loop.js';
|
|
|
11
11
|
export { loadTexturesFromUrls } from './texture-loader.js';
|
|
12
12
|
export { BlitPass, CopyPass, ShaderPass } from '../passes/index.js';
|
|
13
13
|
export type { CurrentReadable, CurrentWritable, Subscribable } from './current-value.js';
|
|
14
|
-
export type { MotionGPUErrorPhase, MotionGPUErrorReport, MotionGPUErrorSource, MotionGPUErrorSourceLine } from './error-report.js';
|
|
14
|
+
export type { MotionGPUErrorCode, MotionGPUErrorContext, MotionGPUErrorPhase, MotionGPUErrorReport, MotionGPUErrorSeverity, MotionGPUErrorSource, MotionGPUErrorSourceLine } from './error-report.js';
|
|
15
15
|
export type { FrameCallback, FrameKey, FrameProfilingSnapshot, FrameRegistry, FrameRunTimings, FrameScheduleSnapshot, FrameStage, FrameStageCallback, FrameTask, FrameTaskInvalidation, FrameTaskInvalidationToken, FrameTimingStats, UseFrameOptions, UseFrameResult } from './frame-registry.js';
|
|
16
16
|
export type { FragMaterial, FragMaterialInput, MaterialDefineValue, MaterialDefines, MaterialIncludes, ResolvedMaterial, TypedMaterialDefineValue } from './material.js';
|
|
17
17
|
export type { MotionGPURuntimeLoop, MotionGPURuntimeLoopOptions } from './runtime-loop.js';
|
package/dist/core/renderer.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import type { Renderer, RendererOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Computes dirty float ranges between two uniform snapshots.
|
|
4
|
+
*
|
|
5
|
+
* Adjacent dirty ranges separated by a gap smaller than or equal to
|
|
6
|
+
* {@link DIRTY_RANGE_MERGE_GAP} are merged to reduce `writeBuffer` calls.
|
|
7
|
+
*/
|
|
8
|
+
export declare function findDirtyFloatRanges(previous: Float32Array, next: Float32Array, mergeGapThreshold?: number): Array<{
|
|
9
|
+
start: number;
|
|
10
|
+
count: number;
|
|
11
|
+
}>;
|
|
2
12
|
/**
|
|
3
13
|
* Creates the WebGPU renderer used by `FragCanvas`.
|
|
4
14
|
*
|
package/dist/core/renderer.js
CHANGED
|
@@ -78,9 +78,45 @@ async function assertCompilation(module, options) {
|
|
|
78
78
|
...(options?.defineBlockSource !== undefined
|
|
79
79
|
? { defineBlockSource: options.defineBlockSource }
|
|
80
80
|
: {}),
|
|
81
|
-
materialSource: options?.materialSource ?? null
|
|
81
|
+
materialSource: options?.materialSource ?? null,
|
|
82
|
+
...(options?.runtimeContext !== undefined ? { runtimeContext: options.runtimeContext } : {})
|
|
82
83
|
});
|
|
83
84
|
}
|
|
85
|
+
function toSortedUniqueStrings(values) {
|
|
86
|
+
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
87
|
+
}
|
|
88
|
+
function buildPassGraphSnapshot(passes) {
|
|
89
|
+
const declaredPasses = passes ?? [];
|
|
90
|
+
let enabledPassCount = 0;
|
|
91
|
+
const inputs = [];
|
|
92
|
+
const outputs = [];
|
|
93
|
+
for (const pass of declaredPasses) {
|
|
94
|
+
if (pass.enabled === false) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
enabledPassCount += 1;
|
|
98
|
+
const needsSwap = pass.needsSwap ?? true;
|
|
99
|
+
const input = pass.input ?? 'source';
|
|
100
|
+
const output = pass.output ?? (needsSwap ? 'target' : 'source');
|
|
101
|
+
inputs.push(input);
|
|
102
|
+
outputs.push(output);
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
passCount: declaredPasses.length,
|
|
106
|
+
enabledPassCount,
|
|
107
|
+
inputs: toSortedUniqueStrings(inputs),
|
|
108
|
+
outputs: toSortedUniqueStrings(outputs)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function buildShaderCompilationRuntimeContext(options) {
|
|
112
|
+
const passList = options.getPasses?.() ?? options.passes;
|
|
113
|
+
const renderTargetMap = options.getRenderTargets?.() ?? options.renderTargets;
|
|
114
|
+
return {
|
|
115
|
+
...(options.materialSignature ? { materialSignature: options.materialSignature } : {}),
|
|
116
|
+
passGraph: buildPassGraphSnapshot(passList),
|
|
117
|
+
activeRenderTargets: Object.keys(renderTargetMap ?? {}).sort((a, b) => a.localeCompare(b))
|
|
118
|
+
};
|
|
119
|
+
}
|
|
84
120
|
/**
|
|
85
121
|
* Creates a 1x1 white fallback texture used before user textures become available.
|
|
86
122
|
*/
|
|
@@ -182,10 +218,19 @@ function createBindGroupLayoutEntries(textureBindings) {
|
|
|
182
218
|
}
|
|
183
219
|
return entries;
|
|
184
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Maximum gap (in floats) between two dirty ranges that triggers merge.
|
|
223
|
+
*
|
|
224
|
+
* Set to 4 (16 bytes) which covers one vec4f alignment slot.
|
|
225
|
+
*/
|
|
226
|
+
const DIRTY_RANGE_MERGE_GAP = 4;
|
|
185
227
|
/**
|
|
186
228
|
* Computes dirty float ranges between two uniform snapshots.
|
|
229
|
+
*
|
|
230
|
+
* Adjacent dirty ranges separated by a gap smaller than or equal to
|
|
231
|
+
* {@link DIRTY_RANGE_MERGE_GAP} are merged to reduce `writeBuffer` calls.
|
|
187
232
|
*/
|
|
188
|
-
function findDirtyFloatRanges(previous, next) {
|
|
233
|
+
export function findDirtyFloatRanges(previous, next, mergeGapThreshold = DIRTY_RANGE_MERGE_GAP) {
|
|
189
234
|
const ranges = [];
|
|
190
235
|
let start = -1;
|
|
191
236
|
for (let index = 0; index < next.length; index += 1) {
|
|
@@ -203,7 +248,22 @@ function findDirtyFloatRanges(previous, next) {
|
|
|
203
248
|
if (start !== -1) {
|
|
204
249
|
ranges.push({ start, count: next.length - start });
|
|
205
250
|
}
|
|
206
|
-
|
|
251
|
+
if (ranges.length <= 1) {
|
|
252
|
+
return ranges;
|
|
253
|
+
}
|
|
254
|
+
const merged = [ranges[0]];
|
|
255
|
+
for (let index = 1; index < ranges.length; index += 1) {
|
|
256
|
+
const prev = merged[merged.length - 1];
|
|
257
|
+
const curr = ranges[index];
|
|
258
|
+
const gap = curr.start - (prev.start + prev.count);
|
|
259
|
+
if (gap <= mergeGapThreshold) {
|
|
260
|
+
prev.count = curr.start + curr.count - prev.start;
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
merged.push(curr);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return merged;
|
|
207
267
|
}
|
|
208
268
|
/**
|
|
209
269
|
* Determines whether shader output should perform linear-to-sRGB conversion.
|
|
@@ -339,6 +399,7 @@ export async function createRenderer(options) {
|
|
|
339
399
|
};
|
|
340
400
|
device.addEventListener('uncapturederror', handleUncapturedError);
|
|
341
401
|
try {
|
|
402
|
+
const runtimeContext = buildShaderCompilationRuntimeContext(options);
|
|
342
403
|
const convertLinearToSrgb = shouldConvertLinearToSrgb(options.outputColorSpace, format);
|
|
343
404
|
const builtShader = buildShaderSourceWithMap(options.fragmentWgsl, options.uniformLayout, options.textureKeys, {
|
|
344
405
|
convertLinearToSrgb,
|
|
@@ -352,7 +413,8 @@ export async function createRenderer(options) {
|
|
|
352
413
|
...(options.defineBlockSource !== undefined
|
|
353
414
|
? { defineBlockSource: options.defineBlockSource }
|
|
354
415
|
: {}),
|
|
355
|
-
materialSource: options.materialSource ?? null
|
|
416
|
+
materialSource: options.materialSource ?? null,
|
|
417
|
+
runtimeContext
|
|
356
418
|
});
|
|
357
419
|
const normalizedTextureDefinitions = normalizeTextureDefinitions(options.textureDefinitions, options.textureKeys);
|
|
358
420
|
const textureBindings = options.textureKeys.map((key, index) => {
|
|
@@ -21,6 +21,9 @@ export interface MotionGPURuntimeLoopOptions {
|
|
|
21
21
|
getDeviceDescriptor: () => GPUDeviceDescriptor | undefined;
|
|
22
22
|
getOnError: () => ((report: MotionGPUErrorReport) => void) | undefined;
|
|
23
23
|
reportError: (report: MotionGPUErrorReport | null) => void;
|
|
24
|
+
getErrorHistoryLimit?: () => number | undefined;
|
|
25
|
+
getOnErrorHistory?: () => ((history: MotionGPUErrorReport[]) => void) | undefined;
|
|
26
|
+
reportErrorHistory?: (history: MotionGPUErrorReport[]) => void;
|
|
24
27
|
}
|
|
25
28
|
export interface MotionGPURuntimeLoop {
|
|
26
29
|
requestFrame: () => void;
|
|
@@ -33,12 +33,81 @@ export function createMotionGPURuntimeLoop(options) {
|
|
|
33
33
|
const renderTextures = {};
|
|
34
34
|
const canvasSize = { width: 0, height: 0 };
|
|
35
35
|
let shouldContinueAfterFrame = false;
|
|
36
|
+
let activeErrorKey = null;
|
|
37
|
+
let errorHistory = [];
|
|
38
|
+
const getHistoryLimit = () => {
|
|
39
|
+
const value = options.getErrorHistoryLimit?.() ?? 0;
|
|
40
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
return Math.floor(value);
|
|
44
|
+
};
|
|
45
|
+
const publishErrorHistory = () => {
|
|
46
|
+
options.reportErrorHistory?.(errorHistory);
|
|
47
|
+
const onErrorHistory = options.getOnErrorHistory?.();
|
|
48
|
+
if (!onErrorHistory) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
onErrorHistory(errorHistory);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// User-provided error history handlers must not break runtime error recovery.
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const syncErrorHistory = () => {
|
|
59
|
+
const limit = getHistoryLimit();
|
|
60
|
+
if (limit <= 0) {
|
|
61
|
+
if (errorHistory.length === 0) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
errorHistory = [];
|
|
65
|
+
publishErrorHistory();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (errorHistory.length <= limit) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
errorHistory = errorHistory.slice(errorHistory.length - limit);
|
|
72
|
+
publishErrorHistory();
|
|
73
|
+
};
|
|
36
74
|
const setError = (error, phase) => {
|
|
37
75
|
const report = toMotionGPUErrorReport(error, phase);
|
|
76
|
+
const reportKey = JSON.stringify({
|
|
77
|
+
phase: report.phase,
|
|
78
|
+
title: report.title,
|
|
79
|
+
message: report.message,
|
|
80
|
+
rawMessage: report.rawMessage
|
|
81
|
+
});
|
|
82
|
+
if (activeErrorKey === reportKey) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
activeErrorKey = reportKey;
|
|
86
|
+
const historyLimit = getHistoryLimit();
|
|
87
|
+
if (historyLimit > 0) {
|
|
88
|
+
errorHistory = [...errorHistory, report];
|
|
89
|
+
if (errorHistory.length > historyLimit) {
|
|
90
|
+
errorHistory = errorHistory.slice(errorHistory.length - historyLimit);
|
|
91
|
+
}
|
|
92
|
+
publishErrorHistory();
|
|
93
|
+
}
|
|
38
94
|
options.reportError(report);
|
|
39
|
-
options.getOnError()
|
|
95
|
+
const onError = options.getOnError();
|
|
96
|
+
if (!onError) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
onError(report);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// User-provided error handlers must not break runtime error recovery.
|
|
104
|
+
}
|
|
40
105
|
};
|
|
41
106
|
const clearError = () => {
|
|
107
|
+
if (activeErrorKey === null) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
activeErrorKey = null;
|
|
42
111
|
options.reportError(null);
|
|
43
112
|
};
|
|
44
113
|
const scheduleFrame = () => {
|
|
@@ -127,6 +196,7 @@ export function createMotionGPURuntimeLoop(options) {
|
|
|
127
196
|
if (isDisposed) {
|
|
128
197
|
return;
|
|
129
198
|
}
|
|
199
|
+
syncErrorHistory();
|
|
130
200
|
let materialState;
|
|
131
201
|
try {
|
|
132
202
|
materialState = resolveActiveMaterial();
|
|
@@ -165,6 +235,7 @@ export function createMotionGPURuntimeLoop(options) {
|
|
|
165
235
|
includeSources: materialState.includeSources,
|
|
166
236
|
defineBlockSource: materialState.defineBlockSource,
|
|
167
237
|
materialSource: materialState.source,
|
|
238
|
+
materialSignature: materialState.signature,
|
|
168
239
|
uniformLayout: materialState.uniformLayout,
|
|
169
240
|
textureKeys: materialState.textureKeys,
|
|
170
241
|
textureDefinitions: materialState.textures,
|
package/dist/core/types.d.ts
CHANGED
|
@@ -443,6 +443,10 @@ export interface RendererOptions {
|
|
|
443
443
|
column?: number;
|
|
444
444
|
functionName?: string;
|
|
445
445
|
} | null;
|
|
446
|
+
/**
|
|
447
|
+
* Stable material signature captured during resolution.
|
|
448
|
+
*/
|
|
449
|
+
materialSignature?: string;
|
|
446
450
|
/**
|
|
447
451
|
* Resolved uniform layout.
|
|
448
452
|
*/
|
|
@@ -1,32 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
export
|
|
3
|
-
enabled?: boolean;
|
|
4
|
-
needsSwap?: boolean;
|
|
5
|
-
input?: RenderPassInputSlot;
|
|
6
|
-
output?: RenderPassOutputSlot;
|
|
7
|
-
filter?: GPUFilterMode;
|
|
8
|
-
}
|
|
1
|
+
import { FullscreenPass, type FullscreenPassOptions } from './FullscreenPass.js';
|
|
2
|
+
export type BlitPassOptions = FullscreenPassOptions;
|
|
9
3
|
/**
|
|
10
4
|
* Fullscreen texture blit pass.
|
|
11
5
|
*/
|
|
12
|
-
export declare class BlitPass
|
|
13
|
-
|
|
14
|
-
needsSwap: boolean;
|
|
15
|
-
input: RenderPassInputSlot;
|
|
16
|
-
output: RenderPassOutputSlot;
|
|
17
|
-
clear: boolean;
|
|
18
|
-
clearColor: [number, number, number, number];
|
|
19
|
-
preserve: boolean;
|
|
20
|
-
private readonly filter;
|
|
21
|
-
private device;
|
|
22
|
-
private sampler;
|
|
23
|
-
private bindGroupLayout;
|
|
24
|
-
private shaderModule;
|
|
25
|
-
private readonly pipelineByFormat;
|
|
26
|
-
private bindGroupByView;
|
|
6
|
+
export declare class BlitPass extends FullscreenPass {
|
|
7
|
+
protected getProgram(): string;
|
|
27
8
|
constructor(options?: BlitPassOptions);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
render(context: RenderPassContext): void;
|
|
31
|
-
dispose(): void;
|
|
9
|
+
protected getVertexEntryPoint(): string;
|
|
10
|
+
protected getFragmentEntryPoint(): string;
|
|
32
11
|
}
|