@motion-core/motion-gpu 0.4.0 → 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 +2 -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 +2 -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 +2 -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 +2 -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 +2 -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 +2 -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 +2 -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 +384 -48
- 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 +2 -0
- package/dist/svelte/FragCanvas.svelte.d.ts.map +1 -0
- package/dist/svelte/MotionGPUErrorOverlay.svelte +17 -20
- 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 +15 -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/MotionGPUErrorOverlay.tsx +392 -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
- package/dist/react/MotionGPUErrorOverlay.tsx +0 -129
- /package/{dist → src/lib}/react/FragCanvas.tsx +0 -0
- /package/{dist → src/lib}/react/Portal.tsx +0 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
UniformLayout,
|
|
3
|
+
UniformLayoutEntry,
|
|
4
|
+
UniformMap,
|
|
5
|
+
UniformMat4Value,
|
|
6
|
+
UniformType,
|
|
7
|
+
UniformValue
|
|
8
|
+
} from './types.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Internal representation of explicitly typed uniform input.
|
|
12
|
+
*/
|
|
13
|
+
type UniformTypedInput = Extract<UniformValue, { type: UniformType; value: unknown }>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Valid WGSL identifier pattern used for uniform and texture keys.
|
|
17
|
+
*/
|
|
18
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Rounds a value up to the nearest multiple of `alignment`.
|
|
22
|
+
*/
|
|
23
|
+
function roundUp(value: number, alignment: number): number {
|
|
24
|
+
return Math.ceil(value / alignment) * alignment;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns WGSL std140-like alignment and size metadata for a uniform type.
|
|
29
|
+
*/
|
|
30
|
+
function getTypeLayout(type: UniformType): { alignment: number; size: number } {
|
|
31
|
+
switch (type) {
|
|
32
|
+
case 'f32':
|
|
33
|
+
return { alignment: 4, size: 4 };
|
|
34
|
+
case 'vec2f':
|
|
35
|
+
return { alignment: 8, size: 8 };
|
|
36
|
+
case 'vec3f':
|
|
37
|
+
return { alignment: 16, size: 12 };
|
|
38
|
+
case 'vec4f':
|
|
39
|
+
return { alignment: 16, size: 16 };
|
|
40
|
+
case 'mat4x4f':
|
|
41
|
+
return { alignment: 16, size: 64 };
|
|
42
|
+
default:
|
|
43
|
+
throw new Error(`Unsupported uniform type: ${type satisfies never}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Type guard for explicitly typed uniform objects.
|
|
49
|
+
*/
|
|
50
|
+
function isTypedUniformValue(value: UniformValue): value is UniformTypedInput {
|
|
51
|
+
return typeof value === 'object' && value !== null && 'type' in value && 'value' in value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validates numeric tuple input with a fixed length.
|
|
56
|
+
*/
|
|
57
|
+
function isTuple(value: unknown, size: number): value is number[] {
|
|
58
|
+
return (
|
|
59
|
+
Array.isArray(value) &&
|
|
60
|
+
value.length === size &&
|
|
61
|
+
value.every((entry) => typeof entry === 'number' && Number.isFinite(entry))
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Type guard for accepted 4x4 matrix uniform values.
|
|
67
|
+
*/
|
|
68
|
+
function isMat4Value(value: unknown): value is UniformMat4Value {
|
|
69
|
+
if (value instanceof Float32Array) {
|
|
70
|
+
return value.length === 16;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
Array.isArray(value) &&
|
|
75
|
+
value.length === 16 &&
|
|
76
|
+
value.every((entry) => typeof entry === 'number' && Number.isFinite(entry))
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Asserts that a name can be safely used as a WGSL identifier.
|
|
82
|
+
*
|
|
83
|
+
* @param name - Candidate uniform/texture name.
|
|
84
|
+
* @throws {Error} When the identifier is invalid.
|
|
85
|
+
*/
|
|
86
|
+
export function assertUniformName(name: string): void {
|
|
87
|
+
if (!IDENTIFIER_PATTERN.test(name)) {
|
|
88
|
+
throw new Error(`Invalid uniform name: ${name}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Infers the WGSL type tag from a runtime uniform value.
|
|
94
|
+
*
|
|
95
|
+
* @param value - Uniform input value.
|
|
96
|
+
* @returns Inferred uniform type.
|
|
97
|
+
* @throws {Error} When the value does not match any supported shape.
|
|
98
|
+
*/
|
|
99
|
+
export function inferUniformType(value: UniformValue): UniformType {
|
|
100
|
+
if (isTypedUniformValue(value)) {
|
|
101
|
+
return value.type;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof value === 'number') {
|
|
105
|
+
return 'f32';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (Array.isArray(value)) {
|
|
109
|
+
if (value.length === 2) {
|
|
110
|
+
return 'vec2f';
|
|
111
|
+
}
|
|
112
|
+
if (value.length === 3) {
|
|
113
|
+
return 'vec3f';
|
|
114
|
+
}
|
|
115
|
+
if (value.length === 4) {
|
|
116
|
+
return 'vec4f';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
throw new Error('Uniform value must resolve to f32, vec2f, vec3f, vec4f or mat4x4f');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Validates that a uniform value matches an explicit uniform type declaration.
|
|
125
|
+
*
|
|
126
|
+
* @param type - Declared WGSL type.
|
|
127
|
+
* @param value - Runtime value to validate.
|
|
128
|
+
* @throws {Error} When the value shape is incompatible with the declared type.
|
|
129
|
+
*/
|
|
130
|
+
export function assertUniformValueForType(type: UniformType, value: UniformValue): void {
|
|
131
|
+
const input = isTypedUniformValue(value) ? value.value : value;
|
|
132
|
+
|
|
133
|
+
if (type === 'f32') {
|
|
134
|
+
if (typeof input !== 'number' || !Number.isFinite(input)) {
|
|
135
|
+
throw new Error('Uniform f32 value must be a finite number');
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (type === 'vec2f') {
|
|
141
|
+
if (!isTuple(input, 2)) {
|
|
142
|
+
throw new Error('Uniform vec2f value must be a tuple with 2 numbers');
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (type === 'vec3f') {
|
|
148
|
+
if (!isTuple(input, 3)) {
|
|
149
|
+
throw new Error('Uniform vec3f value must be a tuple with 3 numbers');
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (type === 'vec4f') {
|
|
155
|
+
if (!isTuple(input, 4)) {
|
|
156
|
+
throw new Error('Uniform vec4f value must be a tuple with 4 numbers');
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!isMat4Value(input)) {
|
|
162
|
+
throw new Error('Uniform mat4x4f value must contain 16 numbers');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Resolves a deterministic packed uniform buffer layout from a uniform map.
|
|
168
|
+
*
|
|
169
|
+
* @param uniforms - Input uniform definitions.
|
|
170
|
+
* @returns Sorted layout with byte offsets and final buffer byte length.
|
|
171
|
+
*/
|
|
172
|
+
export function resolveUniformLayout(uniforms: UniformMap): UniformLayout {
|
|
173
|
+
const names = Object.keys(uniforms).sort();
|
|
174
|
+
let offset = 0;
|
|
175
|
+
const entries: UniformLayoutEntry[] = [];
|
|
176
|
+
const byName: Record<string, UniformLayoutEntry> = {};
|
|
177
|
+
|
|
178
|
+
for (const name of names) {
|
|
179
|
+
assertUniformName(name);
|
|
180
|
+
const type = inferUniformType(uniforms[name] as UniformValue);
|
|
181
|
+
const { alignment, size } = getTypeLayout(type);
|
|
182
|
+
offset = roundUp(offset, alignment);
|
|
183
|
+
|
|
184
|
+
const entry: UniformLayoutEntry = {
|
|
185
|
+
name,
|
|
186
|
+
type,
|
|
187
|
+
offset,
|
|
188
|
+
size
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
entries.push(entry);
|
|
192
|
+
byName[name] = entry;
|
|
193
|
+
offset += size;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const byteLength = Math.max(16, roundUp(offset, 16));
|
|
197
|
+
return { entries, byName, byteLength };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Writes one validated uniform value directly into the output float buffer.
|
|
202
|
+
*/
|
|
203
|
+
function writeUniformValue(
|
|
204
|
+
type: UniformType,
|
|
205
|
+
value: UniformValue,
|
|
206
|
+
data: Float32Array,
|
|
207
|
+
base: number
|
|
208
|
+
): void {
|
|
209
|
+
const input = isTypedUniformValue(value) ? value.value : value;
|
|
210
|
+
assertUniformValueForType(type, value);
|
|
211
|
+
|
|
212
|
+
if (type === 'f32') {
|
|
213
|
+
data[base] = input as number;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (type === 'mat4x4f') {
|
|
218
|
+
const matrix = input as UniformMat4Value;
|
|
219
|
+
if (matrix instanceof Float32Array) {
|
|
220
|
+
for (let index = 0; index < 16; index += 1) {
|
|
221
|
+
data[base + index] = matrix[index] ?? 0;
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (let index = 0; index < 16; index += 1) {
|
|
227
|
+
data[base + index] = matrix[index] ?? 0;
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const tuple = input as number[];
|
|
233
|
+
const length = type === 'vec2f' ? 2 : type === 'vec3f' ? 3 : 4;
|
|
234
|
+
for (let index = 0; index < length; index += 1) {
|
|
235
|
+
data[base + index] = tuple[index] ?? 0;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Packs uniforms into a newly allocated `Float32Array`.
|
|
241
|
+
*
|
|
242
|
+
* @param uniforms - Uniform values to pack.
|
|
243
|
+
* @param layout - Target layout definition.
|
|
244
|
+
* @returns Packed float buffer sized to `layout.byteLength`.
|
|
245
|
+
*/
|
|
246
|
+
export function packUniforms(uniforms: UniformMap, layout: UniformLayout): Float32Array {
|
|
247
|
+
const data = new Float32Array(layout.byteLength / 4);
|
|
248
|
+
packUniformsInto(uniforms, layout, data);
|
|
249
|
+
return data;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Packs uniforms into an existing output buffer and zeroes missing values.
|
|
254
|
+
*
|
|
255
|
+
* @param uniforms - Uniform values to pack.
|
|
256
|
+
* @param layout - Target layout metadata.
|
|
257
|
+
* @param data - Destination float buffer.
|
|
258
|
+
* @throws {Error} When `data` size does not match the required layout size.
|
|
259
|
+
*/
|
|
260
|
+
export function packUniformsInto(
|
|
261
|
+
uniforms: UniformMap,
|
|
262
|
+
layout: UniformLayout,
|
|
263
|
+
data: Float32Array
|
|
264
|
+
): void {
|
|
265
|
+
const requiredLength = layout.byteLength / 4;
|
|
266
|
+
if (data.length !== requiredLength) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`Uniform output buffer size mismatch. Expected ${requiredLength}, got ${data.length}`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
data.fill(0);
|
|
273
|
+
for (const entry of layout.entries) {
|
|
274
|
+
const raw = uniforms[entry.name];
|
|
275
|
+
if (raw === undefined) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const base = entry.offset / 4;
|
|
280
|
+
writeUniformValue(entry.type, raw, data, base);
|
|
281
|
+
}
|
|
282
|
+
}
|
package/src/lib/index.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { FullscreenPass, type FullscreenPassOptions } from './FullscreenPass.js';
|
|
2
|
+
|
|
3
|
+
const FULLSCREEN_BLIT_SHADER = `
|
|
4
|
+
struct MotionGPUVertexOut {
|
|
5
|
+
@builtin(position) position: vec4f,
|
|
6
|
+
@location(0) uv: vec2f,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
@group(0) @binding(0) var motiongpuBlitSampler: sampler;
|
|
10
|
+
@group(0) @binding(1) var motiongpuBlitTexture: texture_2d<f32>;
|
|
11
|
+
|
|
12
|
+
@vertex
|
|
13
|
+
fn motiongpuBlitVertex(@builtin(vertex_index) index: u32) -> MotionGPUVertexOut {
|
|
14
|
+
var positions = array<vec2f, 3>(
|
|
15
|
+
vec2f(-1.0, -3.0),
|
|
16
|
+
vec2f(-1.0, 1.0),
|
|
17
|
+
vec2f(3.0, 1.0)
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
let position = positions[index];
|
|
21
|
+
var out: MotionGPUVertexOut;
|
|
22
|
+
out.position = vec4f(position, 0.0, 1.0);
|
|
23
|
+
out.uv = (position + vec2f(1.0, 1.0)) * 0.5;
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@fragment
|
|
28
|
+
fn motiongpuBlitFragment(in: MotionGPUVertexOut) -> @location(0) vec4f {
|
|
29
|
+
return textureSample(motiongpuBlitTexture, motiongpuBlitSampler, in.uv);
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
export type BlitPassOptions = FullscreenPassOptions;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Fullscreen texture blit pass.
|
|
37
|
+
*/
|
|
38
|
+
export class BlitPass extends FullscreenPass {
|
|
39
|
+
protected getProgram(): string {
|
|
40
|
+
return FULLSCREEN_BLIT_SHADER;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
constructor(options: BlitPassOptions = {}) {
|
|
44
|
+
super(options);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected getVertexEntryPoint(): string {
|
|
48
|
+
return 'motiongpuBlitVertex';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
protected getFragmentEntryPoint(): string {
|
|
52
|
+
return 'motiongpuBlitFragment';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RenderPass,
|
|
3
|
+
RenderPassContext,
|
|
4
|
+
RenderPassFlags,
|
|
5
|
+
RenderPassInputSlot,
|
|
6
|
+
RenderPassOutputSlot
|
|
7
|
+
} from '../core/types.js';
|
|
8
|
+
import { BlitPass } from './BlitPass.js';
|
|
9
|
+
|
|
10
|
+
export interface CopyPassOptions extends RenderPassFlags {
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
needsSwap?: boolean;
|
|
13
|
+
input?: RenderPassInputSlot;
|
|
14
|
+
output?: RenderPassOutputSlot;
|
|
15
|
+
filter?: GPUFilterMode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Texture copy pass with fullscreen-blit fallback.
|
|
20
|
+
*/
|
|
21
|
+
export class CopyPass implements RenderPass {
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
needsSwap: boolean;
|
|
24
|
+
input: RenderPassInputSlot;
|
|
25
|
+
output: RenderPassOutputSlot;
|
|
26
|
+
clear: boolean;
|
|
27
|
+
clearColor: [number, number, number, number];
|
|
28
|
+
preserve: boolean;
|
|
29
|
+
private readonly fallbackBlit: BlitPass;
|
|
30
|
+
|
|
31
|
+
constructor(options: CopyPassOptions = {}) {
|
|
32
|
+
this.enabled = options.enabled ?? true;
|
|
33
|
+
this.needsSwap = options.needsSwap ?? true;
|
|
34
|
+
this.input = options.input ?? 'source';
|
|
35
|
+
this.output = options.output ?? (this.needsSwap ? 'target' : 'source');
|
|
36
|
+
this.clear = options.clear ?? false;
|
|
37
|
+
this.clearColor = options.clearColor ?? [0, 0, 0, 1];
|
|
38
|
+
this.preserve = options.preserve ?? true;
|
|
39
|
+
this.fallbackBlit = new BlitPass({
|
|
40
|
+
enabled: true,
|
|
41
|
+
needsSwap: false,
|
|
42
|
+
input: this.input,
|
|
43
|
+
output: this.output,
|
|
44
|
+
...(options.filter !== undefined ? { filter: options.filter } : {})
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
setSize(width: number, height: number): void {
|
|
49
|
+
this.fallbackBlit.setSize(width, height);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
render(context: RenderPassContext): void {
|
|
53
|
+
const source = context.input;
|
|
54
|
+
const target = context.output;
|
|
55
|
+
const canDirectCopy =
|
|
56
|
+
context.clear === false &&
|
|
57
|
+
context.preserve === true &&
|
|
58
|
+
source.texture !== target.texture &&
|
|
59
|
+
source.texture !== context.canvas.texture &&
|
|
60
|
+
target.texture !== context.canvas.texture &&
|
|
61
|
+
source.width === target.width &&
|
|
62
|
+
source.height === target.height &&
|
|
63
|
+
source.format === target.format;
|
|
64
|
+
|
|
65
|
+
if (canDirectCopy) {
|
|
66
|
+
context.commandEncoder.copyTextureToTexture(
|
|
67
|
+
{ texture: source.texture },
|
|
68
|
+
{ texture: target.texture },
|
|
69
|
+
{ width: source.width, height: source.height, depthOrArrayLayers: 1 }
|
|
70
|
+
);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.fallbackBlit.render(context);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
dispose(): void {
|
|
78
|
+
this.fallbackBlit.dispose();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RenderPass,
|
|
3
|
+
RenderPassContext,
|
|
4
|
+
RenderPassFlags,
|
|
5
|
+
RenderPassInputSlot,
|
|
6
|
+
RenderPassOutputSlot
|
|
7
|
+
} from '../core/types.js';
|
|
8
|
+
|
|
9
|
+
export interface FullscreenPassOptions extends RenderPassFlags {
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
needsSwap?: boolean;
|
|
12
|
+
input?: RenderPassInputSlot;
|
|
13
|
+
output?: RenderPassOutputSlot;
|
|
14
|
+
filter?: GPUFilterMode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Shared base for fullscreen texture sampling passes.
|
|
19
|
+
*/
|
|
20
|
+
export abstract class FullscreenPass implements RenderPass {
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
needsSwap: boolean;
|
|
23
|
+
input: RenderPassInputSlot;
|
|
24
|
+
output: RenderPassOutputSlot;
|
|
25
|
+
clear: boolean;
|
|
26
|
+
clearColor: [number, number, number, number];
|
|
27
|
+
preserve: boolean;
|
|
28
|
+
private readonly filter: GPUFilterMode;
|
|
29
|
+
private device: GPUDevice | null = null;
|
|
30
|
+
private sampler: GPUSampler | null = null;
|
|
31
|
+
private bindGroupLayout: GPUBindGroupLayout | null = null;
|
|
32
|
+
private shaderModule: GPUShaderModule | null = null;
|
|
33
|
+
private readonly pipelineByFormat = new Map<GPUTextureFormat, GPURenderPipeline>();
|
|
34
|
+
private bindGroupByView = new WeakMap<GPUTextureView, GPUBindGroup>();
|
|
35
|
+
|
|
36
|
+
protected constructor(options: FullscreenPassOptions = {}) {
|
|
37
|
+
this.enabled = options.enabled ?? true;
|
|
38
|
+
this.needsSwap = options.needsSwap ?? true;
|
|
39
|
+
this.input = options.input ?? 'source';
|
|
40
|
+
this.output = options.output ?? (this.needsSwap ? 'target' : 'source');
|
|
41
|
+
this.clear = options.clear ?? false;
|
|
42
|
+
this.clearColor = options.clearColor ?? [0, 0, 0, 1];
|
|
43
|
+
this.preserve = options.preserve ?? true;
|
|
44
|
+
this.filter = options.filter ?? 'linear';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected abstract getProgram(): string;
|
|
48
|
+
protected abstract getVertexEntryPoint(): string;
|
|
49
|
+
protected abstract getFragmentEntryPoint(): string;
|
|
50
|
+
|
|
51
|
+
protected invalidateFullscreenCache(): void {
|
|
52
|
+
this.shaderModule = null;
|
|
53
|
+
this.pipelineByFormat.clear();
|
|
54
|
+
this.bindGroupByView = new WeakMap();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private ensureResources(
|
|
58
|
+
device: GPUDevice,
|
|
59
|
+
format: GPUTextureFormat
|
|
60
|
+
): {
|
|
61
|
+
sampler: GPUSampler;
|
|
62
|
+
bindGroupLayout: GPUBindGroupLayout;
|
|
63
|
+
pipeline: GPURenderPipeline;
|
|
64
|
+
} {
|
|
65
|
+
if (this.device !== device) {
|
|
66
|
+
this.device = device;
|
|
67
|
+
this.sampler = null;
|
|
68
|
+
this.bindGroupLayout = null;
|
|
69
|
+
this.invalidateFullscreenCache();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!this.sampler) {
|
|
73
|
+
this.sampler = device.createSampler({
|
|
74
|
+
magFilter: this.filter,
|
|
75
|
+
minFilter: this.filter,
|
|
76
|
+
addressModeU: 'clamp-to-edge',
|
|
77
|
+
addressModeV: 'clamp-to-edge'
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!this.bindGroupLayout) {
|
|
82
|
+
this.bindGroupLayout = device.createBindGroupLayout({
|
|
83
|
+
entries: [
|
|
84
|
+
{
|
|
85
|
+
binding: 0,
|
|
86
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
87
|
+
sampler: { type: 'filtering' }
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
binding: 1,
|
|
91
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
92
|
+
texture: {
|
|
93
|
+
sampleType: 'float',
|
|
94
|
+
viewDimension: '2d',
|
|
95
|
+
multisampled: false
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!this.shaderModule) {
|
|
103
|
+
this.shaderModule = device.createShaderModule({ code: this.getProgram() });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let pipeline = this.pipelineByFormat.get(format);
|
|
107
|
+
if (!pipeline) {
|
|
108
|
+
const pipelineLayout = device.createPipelineLayout({
|
|
109
|
+
bindGroupLayouts: [this.bindGroupLayout]
|
|
110
|
+
});
|
|
111
|
+
pipeline = device.createRenderPipeline({
|
|
112
|
+
layout: pipelineLayout,
|
|
113
|
+
vertex: {
|
|
114
|
+
module: this.shaderModule,
|
|
115
|
+
entryPoint: this.getVertexEntryPoint()
|
|
116
|
+
},
|
|
117
|
+
fragment: {
|
|
118
|
+
module: this.shaderModule,
|
|
119
|
+
entryPoint: this.getFragmentEntryPoint(),
|
|
120
|
+
targets: [{ format }]
|
|
121
|
+
},
|
|
122
|
+
primitive: { topology: 'triangle-list' }
|
|
123
|
+
});
|
|
124
|
+
this.pipelineByFormat.set(format, pipeline);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
sampler: this.sampler,
|
|
129
|
+
bindGroupLayout: this.bindGroupLayout,
|
|
130
|
+
pipeline
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
setSize(width: number, height: number): void {
|
|
135
|
+
void width;
|
|
136
|
+
void height;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
protected renderFullscreen(context: RenderPassContext): void {
|
|
140
|
+
const { sampler, bindGroupLayout, pipeline } = this.ensureResources(
|
|
141
|
+
context.device,
|
|
142
|
+
context.output.format
|
|
143
|
+
);
|
|
144
|
+
const inputView = context.input.view;
|
|
145
|
+
let bindGroup = this.bindGroupByView.get(inputView);
|
|
146
|
+
if (!bindGroup) {
|
|
147
|
+
bindGroup = context.device.createBindGroup({
|
|
148
|
+
layout: bindGroupLayout,
|
|
149
|
+
entries: [
|
|
150
|
+
{ binding: 0, resource: sampler },
|
|
151
|
+
{ binding: 1, resource: inputView }
|
|
152
|
+
]
|
|
153
|
+
});
|
|
154
|
+
this.bindGroupByView.set(inputView, bindGroup);
|
|
155
|
+
}
|
|
156
|
+
const pass = context.beginRenderPass();
|
|
157
|
+
pass.setPipeline(pipeline);
|
|
158
|
+
pass.setBindGroup(0, bindGroup);
|
|
159
|
+
pass.draw(3);
|
|
160
|
+
pass.end();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
render(context: RenderPassContext): void {
|
|
164
|
+
this.renderFullscreen(context);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
dispose(): void {
|
|
168
|
+
this.device = null;
|
|
169
|
+
this.sampler = null;
|
|
170
|
+
this.bindGroupLayout = null;
|
|
171
|
+
this.invalidateFullscreenCache();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { FullscreenPass, type FullscreenPassOptions } from './FullscreenPass.js';
|
|
2
|
+
|
|
3
|
+
const SHADER_PASS_CONTRACT =
|
|
4
|
+
/\bfn\s+shade\s*\(\s*inputColor\s*:\s*vec4f\s*,\s*uv\s*:\s*vec2f\s*\)\s*->\s*vec4f/;
|
|
5
|
+
|
|
6
|
+
export interface ShaderPassOptions extends FullscreenPassOptions {
|
|
7
|
+
fragment: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function buildShaderPassProgram(fragment: string): string {
|
|
11
|
+
if (!SHADER_PASS_CONTRACT.test(fragment)) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
'ShaderPass fragment must declare `fn shade(inputColor: vec4f, uv: vec2f) -> vec4f`.'
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return `
|
|
18
|
+
struct MotionGPUVertexOut {
|
|
19
|
+
@builtin(position) position: vec4f,
|
|
20
|
+
@location(0) uv: vec2f,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
@group(0) @binding(0) var motiongpuShaderPassSampler: sampler;
|
|
24
|
+
@group(0) @binding(1) var motiongpuShaderPassTexture: texture_2d<f32>;
|
|
25
|
+
|
|
26
|
+
@vertex
|
|
27
|
+
fn motiongpuShaderPassVertex(@builtin(vertex_index) index: u32) -> MotionGPUVertexOut {
|
|
28
|
+
var positions = array<vec2f, 3>(
|
|
29
|
+
vec2f(-1.0, -3.0),
|
|
30
|
+
vec2f(-1.0, 1.0),
|
|
31
|
+
vec2f(3.0, 1.0)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
let position = positions[index];
|
|
35
|
+
var out: MotionGPUVertexOut;
|
|
36
|
+
out.position = vec4f(position, 0.0, 1.0);
|
|
37
|
+
out.uv = (position + vec2f(1.0, 1.0)) * 0.5;
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
${fragment}
|
|
42
|
+
|
|
43
|
+
@fragment
|
|
44
|
+
fn motiongpuShaderPassFragment(in: MotionGPUVertexOut) -> @location(0) vec4f {
|
|
45
|
+
let inputColor = textureSample(motiongpuShaderPassTexture, motiongpuShaderPassSampler, in.uv);
|
|
46
|
+
return shade(inputColor, in.uv);
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Fullscreen programmable shader pass.
|
|
53
|
+
*/
|
|
54
|
+
export class ShaderPass extends FullscreenPass {
|
|
55
|
+
private fragment: string;
|
|
56
|
+
private program: string;
|
|
57
|
+
|
|
58
|
+
constructor(options: ShaderPassOptions) {
|
|
59
|
+
super(options);
|
|
60
|
+
this.fragment = options.fragment;
|
|
61
|
+
this.program = buildShaderPassProgram(options.fragment);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Replaces current shader fragment and invalidates pipeline cache.
|
|
66
|
+
*/
|
|
67
|
+
setFragment(fragment: string): void {
|
|
68
|
+
this.fragment = fragment;
|
|
69
|
+
this.program = buildShaderPassProgram(fragment);
|
|
70
|
+
this.invalidateFullscreenCache();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getFragment(): string {
|
|
74
|
+
return this.fragment;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
protected getProgram(): string {
|
|
78
|
+
return this.program;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
protected getVertexEntryPoint(): string {
|
|
82
|
+
return 'motiongpuShaderPassVertex';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
protected getFragmentEntryPoint(): string {
|
|
86
|
+
return 'motiongpuShaderPassFragment';
|
|
87
|
+
}
|
|
88
|
+
}
|