@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.
Files changed (207) hide show
  1. package/dist/advanced.d.ts +1 -0
  2. package/dist/advanced.d.ts.map +1 -0
  3. package/dist/advanced.js +12 -6
  4. package/dist/core/advanced.d.ts +1 -0
  5. package/dist/core/advanced.d.ts.map +1 -0
  6. package/dist/core/advanced.js +12 -5
  7. package/dist/core/current-value.d.ts +1 -0
  8. package/dist/core/current-value.d.ts.map +1 -0
  9. package/dist/core/current-value.js +35 -34
  10. package/dist/core/current-value.js.map +1 -0
  11. package/dist/core/error-diagnostics.d.ts +1 -0
  12. package/dist/core/error-diagnostics.d.ts.map +1 -0
  13. package/dist/core/error-diagnostics.js +70 -137
  14. package/dist/core/error-diagnostics.js.map +1 -0
  15. package/dist/core/error-report.d.ts +1 -0
  16. package/dist/core/error-report.d.ts.map +1 -0
  17. package/dist/core/error-report.js +184 -233
  18. package/dist/core/error-report.js.map +1 -0
  19. package/dist/core/frame-registry.d.ts +1 -0
  20. package/dist/core/frame-registry.d.ts.map +1 -0
  21. package/dist/core/frame-registry.js +546 -662
  22. package/dist/core/frame-registry.js.map +1 -0
  23. package/dist/core/index.d.ts +1 -0
  24. package/dist/core/index.d.ts.map +1 -0
  25. package/dist/core/index.js +11 -12
  26. package/dist/core/material-preprocess.d.ts +1 -0
  27. package/dist/core/material-preprocess.d.ts.map +1 -0
  28. package/dist/core/material-preprocess.js +128 -151
  29. package/dist/core/material-preprocess.js.map +1 -0
  30. package/dist/core/material.d.ts +1 -0
  31. package/dist/core/material.d.ts.map +1 -0
  32. package/dist/core/material.js +263 -317
  33. package/dist/core/material.js.map +1 -0
  34. package/dist/core/recompile-policy.d.ts +1 -0
  35. package/dist/core/recompile-policy.d.ts.map +1 -0
  36. package/dist/core/recompile-policy.js +18 -13
  37. package/dist/core/recompile-policy.js.map +1 -0
  38. package/dist/core/render-graph.d.ts +1 -0
  39. package/dist/core/render-graph.d.ts.map +1 -0
  40. package/dist/core/render-graph.js +61 -68
  41. package/dist/core/render-graph.js.map +1 -0
  42. package/dist/core/render-targets.d.ts +2 -0
  43. package/dist/core/render-targets.d.ts.map +1 -0
  44. package/dist/core/render-targets.js +52 -53
  45. package/dist/core/render-targets.js.map +1 -0
  46. package/dist/core/renderer.d.ts +1 -0
  47. package/dist/core/renderer.d.ts.map +1 -0
  48. package/dist/core/renderer.js +942 -1081
  49. package/dist/core/renderer.js.map +1 -0
  50. package/dist/core/runtime-loop.d.ts +2 -0
  51. package/dist/core/runtime-loop.d.ts.map +1 -0
  52. package/dist/core/runtime-loop.js +305 -362
  53. package/dist/core/runtime-loop.js.map +1 -0
  54. package/dist/core/scheduler-helpers.d.ts +1 -0
  55. package/dist/core/scheduler-helpers.d.ts.map +1 -0
  56. package/dist/core/scheduler-helpers.js +52 -51
  57. package/dist/core/scheduler-helpers.js.map +1 -0
  58. package/dist/core/shader.d.ts +1 -0
  59. package/dist/core/shader.d.ts.map +1 -0
  60. package/dist/core/shader.js +92 -117
  61. package/dist/core/shader.js.map +1 -0
  62. package/dist/core/texture-loader.d.ts +1 -0
  63. package/dist/core/texture-loader.d.ts.map +1 -0
  64. package/dist/core/texture-loader.js +205 -273
  65. package/dist/core/texture-loader.js.map +1 -0
  66. package/dist/core/textures.d.ts +2 -0
  67. package/dist/core/textures.d.ts.map +1 -0
  68. package/dist/core/textures.js +106 -116
  69. package/dist/core/textures.js.map +1 -0
  70. package/dist/core/types.d.ts +2 -0
  71. package/dist/core/types.d.ts.map +1 -0
  72. package/dist/core/types.js +0 -4
  73. package/dist/core/uniforms.d.ts +1 -0
  74. package/dist/core/uniforms.d.ts.map +1 -0
  75. package/dist/core/uniforms.js +170 -191
  76. package/dist/core/uniforms.js.map +1 -0
  77. package/dist/index.d.ts +1 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +11 -6
  80. package/dist/passes/BlitPass.d.ts +1 -0
  81. package/dist/passes/BlitPass.d.ts.map +1 -0
  82. package/dist/passes/BlitPass.js +23 -18
  83. package/dist/passes/BlitPass.js.map +1 -0
  84. package/dist/passes/CopyPass.d.ts +2 -0
  85. package/dist/passes/CopyPass.d.ts.map +1 -0
  86. package/dist/passes/CopyPass.js +58 -52
  87. package/dist/passes/CopyPass.js.map +1 -0
  88. package/dist/passes/FullscreenPass.d.ts +2 -0
  89. package/dist/passes/FullscreenPass.d.ts.map +1 -0
  90. package/dist/passes/FullscreenPass.js +127 -130
  91. package/dist/passes/FullscreenPass.js.map +1 -0
  92. package/dist/passes/ShaderPass.d.ts +1 -0
  93. package/dist/passes/ShaderPass.d.ts.map +1 -0
  94. package/dist/passes/ShaderPass.js +40 -37
  95. package/dist/passes/ShaderPass.js.map +1 -0
  96. package/dist/passes/index.d.ts +1 -0
  97. package/dist/passes/index.d.ts.map +1 -0
  98. package/dist/passes/index.js +4 -3
  99. package/dist/react/FragCanvas.d.ts +2 -0
  100. package/dist/react/FragCanvas.d.ts.map +1 -0
  101. package/dist/react/FragCanvas.js +234 -211
  102. package/dist/react/FragCanvas.js.map +1 -0
  103. package/dist/react/MotionGPUErrorOverlay.d.ts +1 -0
  104. package/dist/react/MotionGPUErrorOverlay.d.ts.map +1 -0
  105. package/dist/react/MotionGPUErrorOverlay.js +384 -48
  106. package/dist/react/MotionGPUErrorOverlay.js.map +1 -0
  107. package/dist/react/Portal.d.ts +1 -0
  108. package/dist/react/Portal.d.ts.map +1 -0
  109. package/dist/react/Portal.js +18 -21
  110. package/dist/react/Portal.js.map +1 -0
  111. package/dist/react/advanced.d.ts +1 -0
  112. package/dist/react/advanced.d.ts.map +1 -0
  113. package/dist/react/advanced.js +12 -6
  114. package/dist/react/frame-context.d.ts +1 -0
  115. package/dist/react/frame-context.d.ts.map +1 -0
  116. package/dist/react/frame-context.js +88 -94
  117. package/dist/react/frame-context.js.map +1 -0
  118. package/dist/react/index.d.ts +1 -0
  119. package/dist/react/index.d.ts.map +1 -0
  120. package/dist/react/index.js +10 -9
  121. package/dist/react/motiongpu-context.d.ts +1 -0
  122. package/dist/react/motiongpu-context.d.ts.map +1 -0
  123. package/dist/react/motiongpu-context.js +18 -15
  124. package/dist/react/motiongpu-context.js.map +1 -0
  125. package/dist/react/use-motiongpu-user-context.d.ts +1 -0
  126. package/dist/react/use-motiongpu-user-context.d.ts.map +1 -0
  127. package/dist/react/use-motiongpu-user-context.js +83 -82
  128. package/dist/react/use-motiongpu-user-context.js.map +1 -0
  129. package/dist/react/use-texture.d.ts +1 -0
  130. package/dist/react/use-texture.d.ts.map +1 -0
  131. package/dist/react/use-texture.js +132 -152
  132. package/dist/react/use-texture.js.map +1 -0
  133. package/dist/svelte/FragCanvas.svelte.d.ts +2 -0
  134. package/dist/svelte/FragCanvas.svelte.d.ts.map +1 -0
  135. package/dist/svelte/MotionGPUErrorOverlay.svelte +17 -20
  136. package/dist/svelte/MotionGPUErrorOverlay.svelte.d.ts +1 -0
  137. package/dist/svelte/MotionGPUErrorOverlay.svelte.d.ts.map +1 -0
  138. package/dist/svelte/Portal.svelte.d.ts +1 -0
  139. package/dist/svelte/Portal.svelte.d.ts.map +1 -0
  140. package/dist/svelte/advanced.d.ts +1 -0
  141. package/dist/svelte/advanced.d.ts.map +1 -0
  142. package/dist/svelte/advanced.js +11 -6
  143. package/dist/svelte/frame-context.d.ts +1 -0
  144. package/dist/svelte/frame-context.d.ts.map +1 -0
  145. package/dist/svelte/frame-context.js +27 -27
  146. package/dist/svelte/frame-context.js.map +1 -0
  147. package/dist/svelte/index.d.ts +1 -0
  148. package/dist/svelte/index.d.ts.map +1 -0
  149. package/dist/svelte/index.js +10 -9
  150. package/dist/svelte/motiongpu-context.d.ts +1 -0
  151. package/dist/svelte/motiongpu-context.d.ts.map +1 -0
  152. package/dist/svelte/motiongpu-context.js +24 -21
  153. package/dist/svelte/motiongpu-context.js.map +1 -0
  154. package/dist/svelte/use-motiongpu-user-context.d.ts +1 -0
  155. package/dist/svelte/use-motiongpu-user-context.d.ts.map +1 -0
  156. package/dist/svelte/use-motiongpu-user-context.js +69 -70
  157. package/dist/svelte/use-motiongpu-user-context.js.map +1 -0
  158. package/dist/svelte/use-texture.d.ts +1 -0
  159. package/dist/svelte/use-texture.d.ts.map +1 -0
  160. package/dist/svelte/use-texture.js +125 -147
  161. package/dist/svelte/use-texture.js.map +1 -0
  162. package/package.json +15 -7
  163. package/src/lib/advanced.ts +6 -0
  164. package/src/lib/core/advanced.ts +12 -0
  165. package/src/lib/core/current-value.ts +64 -0
  166. package/src/lib/core/error-diagnostics.ts +236 -0
  167. package/src/lib/core/error-report.ts +406 -0
  168. package/src/lib/core/frame-registry.ts +1189 -0
  169. package/src/lib/core/index.ts +77 -0
  170. package/src/lib/core/material-preprocess.ts +284 -0
  171. package/src/lib/core/material.ts +667 -0
  172. package/src/lib/core/recompile-policy.ts +31 -0
  173. package/src/lib/core/render-graph.ts +143 -0
  174. package/src/lib/core/render-targets.ts +107 -0
  175. package/src/lib/core/renderer.ts +1547 -0
  176. package/src/lib/core/runtime-loop.ts +458 -0
  177. package/src/lib/core/scheduler-helpers.ts +136 -0
  178. package/src/lib/core/shader.ts +258 -0
  179. package/src/lib/core/texture-loader.ts +476 -0
  180. package/src/lib/core/textures.ts +235 -0
  181. package/src/lib/core/types.ts +582 -0
  182. package/src/lib/core/uniforms.ts +282 -0
  183. package/src/lib/index.ts +6 -0
  184. package/src/lib/passes/BlitPass.ts +54 -0
  185. package/src/lib/passes/CopyPass.ts +80 -0
  186. package/src/lib/passes/FullscreenPass.ts +173 -0
  187. package/src/lib/passes/ShaderPass.ts +88 -0
  188. package/src/lib/passes/index.ts +3 -0
  189. package/src/lib/react/MotionGPUErrorOverlay.tsx +392 -0
  190. package/src/lib/react/advanced.ts +36 -0
  191. package/src/lib/react/frame-context.ts +169 -0
  192. package/src/lib/react/index.ts +51 -0
  193. package/src/lib/react/motiongpu-context.ts +88 -0
  194. package/src/lib/react/use-motiongpu-user-context.ts +186 -0
  195. package/src/lib/react/use-texture.ts +233 -0
  196. package/src/lib/svelte/FragCanvas.svelte +249 -0
  197. package/src/lib/svelte/MotionGPUErrorOverlay.svelte +382 -0
  198. package/src/lib/svelte/Portal.svelte +31 -0
  199. package/src/lib/svelte/advanced.ts +32 -0
  200. package/src/lib/svelte/frame-context.ts +87 -0
  201. package/src/lib/svelte/index.ts +51 -0
  202. package/src/lib/svelte/motiongpu-context.ts +97 -0
  203. package/src/lib/svelte/use-motiongpu-user-context.ts +145 -0
  204. package/src/lib/svelte/use-texture.ts +232 -0
  205. package/dist/react/MotionGPUErrorOverlay.tsx +0 -129
  206. /package/{dist → src/lib}/react/FragCanvas.tsx +0 -0
  207. /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
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Root package entrypoint.
3
+ *
4
+ * Framework-agnostic core entrypoint.
5
+ */
6
+ export * from './core/index.js';
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export { BlitPass, type BlitPassOptions } from './BlitPass.js';
2
+ export { CopyPass, type CopyPassOptions } from './CopyPass.js';
3
+ export { ShaderPass, type ShaderPassOptions } from './ShaderPass.js';