@motion-core/motion-gpu 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +99 -0
  2. package/dist/advanced.js +3 -1
  3. package/dist/core/advanced.js +3 -1
  4. package/dist/core/compute-shader.d.ts +87 -0
  5. package/dist/core/compute-shader.d.ts.map +1 -0
  6. package/dist/core/compute-shader.js +205 -0
  7. package/dist/core/compute-shader.js.map +1 -0
  8. package/dist/core/error-report.d.ts +1 -1
  9. package/dist/core/error-report.d.ts.map +1 -1
  10. package/dist/core/error-report.js +63 -0
  11. package/dist/core/error-report.js.map +1 -1
  12. package/dist/core/frame-registry.d.ts.map +1 -1
  13. package/dist/core/frame-registry.js +1 -1
  14. package/dist/core/frame-registry.js.map +1 -1
  15. package/dist/core/index.d.ts +5 -2
  16. package/dist/core/index.d.ts.map +1 -1
  17. package/dist/core/index.js +3 -1
  18. package/dist/core/material-preprocess.d.ts.map +1 -1
  19. package/dist/core/material-preprocess.js +5 -3
  20. package/dist/core/material-preprocess.js.map +1 -1
  21. package/dist/core/material.d.ts +22 -6
  22. package/dist/core/material.d.ts.map +1 -1
  23. package/dist/core/material.js +30 -3
  24. package/dist/core/material.js.map +1 -1
  25. package/dist/core/render-graph.d.ts +7 -3
  26. package/dist/core/render-graph.d.ts.map +1 -1
  27. package/dist/core/render-graph.js +22 -6
  28. package/dist/core/render-graph.js.map +1 -1
  29. package/dist/core/renderer.d.ts.map +1 -1
  30. package/dist/core/renderer.js +418 -23
  31. package/dist/core/renderer.js.map +1 -1
  32. package/dist/core/runtime-loop.d.ts +2 -2
  33. package/dist/core/runtime-loop.d.ts.map +1 -1
  34. package/dist/core/runtime-loop.js +49 -1
  35. package/dist/core/runtime-loop.js.map +1 -1
  36. package/dist/core/shader.d.ts +9 -1
  37. package/dist/core/shader.d.ts.map +1 -1
  38. package/dist/core/shader.js +21 -2
  39. package/dist/core/shader.js.map +1 -1
  40. package/dist/core/storage-buffers.d.ts +37 -0
  41. package/dist/core/storage-buffers.d.ts.map +1 -0
  42. package/dist/core/storage-buffers.js +95 -0
  43. package/dist/core/storage-buffers.js.map +1 -0
  44. package/dist/core/texture-loader.d.ts.map +1 -1
  45. package/dist/core/texture-loader.js +4 -0
  46. package/dist/core/texture-loader.js.map +1 -1
  47. package/dist/core/textures.d.ts +12 -0
  48. package/dist/core/textures.d.ts.map +1 -1
  49. package/dist/core/textures.js +7 -2
  50. package/dist/core/textures.js.map +1 -1
  51. package/dist/core/types.d.ts +146 -4
  52. package/dist/core/types.d.ts.map +1 -1
  53. package/dist/index.js +3 -1
  54. package/dist/passes/ComputePass.d.ts +83 -0
  55. package/dist/passes/ComputePass.d.ts.map +1 -0
  56. package/dist/passes/ComputePass.js +92 -0
  57. package/dist/passes/ComputePass.js.map +1 -0
  58. package/dist/passes/PingPongComputePass.d.ts +104 -0
  59. package/dist/passes/PingPongComputePass.d.ts.map +1 -0
  60. package/dist/passes/PingPongComputePass.js +132 -0
  61. package/dist/passes/PingPongComputePass.js.map +1 -0
  62. package/dist/passes/ShaderPass.d.ts.map +1 -1
  63. package/dist/passes/ShaderPass.js +2 -1
  64. package/dist/passes/ShaderPass.js.map +1 -1
  65. package/dist/passes/index.d.ts +2 -0
  66. package/dist/passes/index.d.ts.map +1 -1
  67. package/dist/passes/index.js +3 -1
  68. package/dist/react/FragCanvas.d.ts +2 -2
  69. package/dist/react/FragCanvas.d.ts.map +1 -1
  70. package/dist/react/FragCanvas.js.map +1 -1
  71. package/dist/react/MotionGPUErrorOverlay.d.ts.map +1 -1
  72. package/dist/react/MotionGPUErrorOverlay.js +123 -20
  73. package/dist/react/MotionGPUErrorOverlay.js.map +1 -1
  74. package/dist/react/advanced.js +3 -1
  75. package/dist/react/index.d.ts +5 -2
  76. package/dist/react/index.d.ts.map +1 -1
  77. package/dist/react/index.js +3 -1
  78. package/dist/svelte/FragCanvas.svelte +2 -2
  79. package/dist/svelte/FragCanvas.svelte.d.ts +2 -2
  80. package/dist/svelte/FragCanvas.svelte.d.ts.map +1 -1
  81. package/dist/svelte/MotionGPUErrorOverlay.svelte +137 -7
  82. package/dist/svelte/MotionGPUErrorOverlay.svelte.d.ts.map +1 -1
  83. package/dist/svelte/advanced.js +3 -1
  84. package/dist/svelte/index.d.ts +5 -2
  85. package/dist/svelte/index.d.ts.map +1 -1
  86. package/dist/svelte/index.js +3 -1
  87. package/package.json +1 -1
  88. package/src/lib/core/compute-shader.ts +326 -0
  89. package/src/lib/core/error-report.ts +129 -0
  90. package/src/lib/core/frame-registry.ts +2 -1
  91. package/src/lib/core/index.ts +18 -1
  92. package/src/lib/core/material-preprocess.ts +17 -6
  93. package/src/lib/core/material.ts +101 -20
  94. package/src/lib/core/render-graph.ts +39 -9
  95. package/src/lib/core/renderer.ts +655 -41
  96. package/src/lib/core/runtime-loop.ts +82 -3
  97. package/src/lib/core/shader.ts +45 -2
  98. package/src/lib/core/storage-buffers.ts +142 -0
  99. package/src/lib/core/texture-loader.ts +6 -0
  100. package/src/lib/core/textures.ts +24 -2
  101. package/src/lib/core/types.ts +165 -4
  102. package/src/lib/passes/ComputePass.ts +136 -0
  103. package/src/lib/passes/PingPongComputePass.ts +180 -0
  104. package/src/lib/passes/ShaderPass.ts +2 -1
  105. package/src/lib/passes/index.ts +6 -0
  106. package/src/lib/react/FragCanvas.tsx +3 -3
  107. package/src/lib/react/MotionGPUErrorOverlay.tsx +137 -5
  108. package/src/lib/react/index.ts +18 -1
  109. package/src/lib/svelte/FragCanvas.svelte +2 -2
  110. package/src/lib/svelte/MotionGPUErrorOverlay.svelte +137 -7
  111. package/src/lib/svelte/index.ts +18 -1
@@ -0,0 +1,136 @@
1
+ import { assertComputeContract, extractWorkgroupSize } from '../core/compute-shader.js';
2
+
3
+ /**
4
+ * Dispatch context provided to dynamic dispatch callbacks.
5
+ */
6
+ export interface ComputeDispatchContext {
7
+ width: number;
8
+ height: number;
9
+ time: number;
10
+ delta: number;
11
+ workgroupSize: [number, number, number];
12
+ }
13
+
14
+ /**
15
+ * Options for constructing a `ComputePass`.
16
+ */
17
+ export interface ComputePassOptions {
18
+ /**
19
+ * Compute shader WGSL source code.
20
+ * Must declare `@compute @workgroup_size(...) fn compute(@builtin(global_invocation_id) ...)`.
21
+ */
22
+ compute: string;
23
+ /**
24
+ * Dispatch workgroup counts.
25
+ * - Static tuple: `[x]`, `[x, y]`, or `[x, y, z]`
26
+ * - `'auto'`: derived from canvas size / workgroup size
27
+ * - Function: dynamic dispatch based on frame context
28
+ */
29
+ dispatch?:
30
+ | [number, number?, number?]
31
+ | 'auto'
32
+ | ((ctx: ComputeDispatchContext) => [number, number, number]);
33
+ /**
34
+ * Enables/disables this compute pass.
35
+ */
36
+ enabled?: boolean;
37
+ }
38
+
39
+ /**
40
+ * Compute pass class used within the render graph.
41
+ *
42
+ * Validates compute shader contract, parses workgroup size,
43
+ * and resolves dispatch dimensions. Does **not** manage GPU resources
44
+ * (that responsibility belongs to the renderer).
45
+ */
46
+ export class ComputePass {
47
+ /**
48
+ * Enables/disables this pass without removing it from graph.
49
+ */
50
+ enabled: boolean;
51
+
52
+ /**
53
+ * Discriminant flag for render graph to identify compute passes.
54
+ */
55
+ readonly isCompute = true as const;
56
+
57
+ private compute: string;
58
+ private workgroupSize: [number, number, number];
59
+ private dispatch: ComputePassOptions['dispatch'];
60
+
61
+ constructor(options: ComputePassOptions) {
62
+ assertComputeContract(options.compute);
63
+ const workgroupSize = extractWorkgroupSize(options.compute);
64
+ this.compute = options.compute;
65
+ this.workgroupSize = workgroupSize;
66
+ this.dispatch = options.dispatch ?? 'auto';
67
+ this.enabled = options.enabled ?? true;
68
+ }
69
+
70
+ /**
71
+ * Replaces current compute shader and updates workgroup size.
72
+ *
73
+ * @param compute - New compute shader WGSL source.
74
+ * @throws {Error} When shader does not match the compute contract.
75
+ */
76
+ setCompute(compute: string): void {
77
+ assertComputeContract(compute);
78
+ const workgroupSize = extractWorkgroupSize(compute);
79
+ this.compute = compute;
80
+ this.workgroupSize = workgroupSize;
81
+ }
82
+
83
+ /**
84
+ * Updates dispatch strategy.
85
+ */
86
+ setDispatch(dispatch: ComputePassOptions['dispatch']): void {
87
+ this.dispatch = dispatch ?? 'auto';
88
+ }
89
+
90
+ /**
91
+ * Returns current compute shader source.
92
+ */
93
+ getCompute(): string {
94
+ return this.compute;
95
+ }
96
+
97
+ /**
98
+ * Returns parsed workgroup size from current compute shader.
99
+ */
100
+ getWorkgroupSize(): [number, number, number] {
101
+ return [...this.workgroupSize];
102
+ }
103
+
104
+ /**
105
+ * Resolves dispatch workgroup counts for current frame.
106
+ *
107
+ * @param ctx - Dispatch context with canvas dimensions and timing.
108
+ * @returns Tuple [x, y, z] workgroup counts.
109
+ */
110
+ resolveDispatch(ctx: ComputeDispatchContext): [number, number, number] {
111
+ if (this.dispatch === 'auto') {
112
+ return [
113
+ Math.ceil(ctx.width / this.workgroupSize[0]),
114
+ Math.ceil(ctx.height / this.workgroupSize[1]),
115
+ Math.ceil(1 / this.workgroupSize[2])
116
+ ];
117
+ }
118
+
119
+ if (typeof this.dispatch === 'function') {
120
+ return this.dispatch(ctx);
121
+ }
122
+
123
+ if (Array.isArray(this.dispatch)) {
124
+ return [this.dispatch[0], this.dispatch[1] ?? 1, this.dispatch[2] ?? 1];
125
+ }
126
+
127
+ return [1, 1, 1];
128
+ }
129
+
130
+ /**
131
+ * Releases resources (no-op since GPU resource lifecycle is renderer-managed).
132
+ */
133
+ dispose(): void {
134
+ // No-op — GPU resources are managed by the renderer.
135
+ }
136
+ }
@@ -0,0 +1,180 @@
1
+ import { assertComputeContract, extractWorkgroupSize } from '../core/compute-shader.js';
2
+ import type { ComputePassOptions, ComputeDispatchContext } from './ComputePass.js';
3
+
4
+ /**
5
+ * Options for constructing a `PingPongComputePass`.
6
+ */
7
+ export interface PingPongComputePassOptions {
8
+ /**
9
+ * Compute shader WGSL source code.
10
+ */
11
+ compute: string;
12
+ /**
13
+ * Target texture key from `material.textures`.
14
+ * The engine will auto-generate `{target}A` and `{target}B` bindings.
15
+ */
16
+ target: string;
17
+ /**
18
+ * Number of compute iterations per frame. Default: 1.
19
+ */
20
+ iterations?: number;
21
+ /**
22
+ * Dispatch workgroup counts (same as ComputePass).
23
+ */
24
+ dispatch?: ComputePassOptions['dispatch'];
25
+ /**
26
+ * Enables/disables this pass.
27
+ */
28
+ enabled?: boolean;
29
+ }
30
+
31
+ /**
32
+ * Ping-pong compute pass for iterative GPU simulations.
33
+ *
34
+ * Manages two texture buffers (A/B) and alternates between them each iteration,
35
+ * enabling read-from-previous-write patterns commonly used in fluid simulations,
36
+ * reaction-diffusion, and particle systems.
37
+ */
38
+ export class PingPongComputePass {
39
+ /**
40
+ * Enables/disables this pass without removing it from graph.
41
+ */
42
+ enabled: boolean;
43
+
44
+ /**
45
+ * Discriminant flag for render graph to identify compute passes.
46
+ */
47
+ readonly isCompute = true as const;
48
+
49
+ /**
50
+ * Discriminant flag to identify ping-pong compute passes.
51
+ */
52
+ readonly isPingPong = true as const;
53
+
54
+ private compute: string;
55
+ private target: string;
56
+ private iterations: number;
57
+ private dispatch: ComputePassOptions['dispatch'];
58
+ private workgroupSize: [number, number, number];
59
+ private frameCount: number = 0;
60
+
61
+ constructor(options: PingPongComputePassOptions) {
62
+ assertComputeContract(options.compute);
63
+ const workgroupSize = extractWorkgroupSize(options.compute);
64
+ this.compute = options.compute;
65
+ this.target = options.target;
66
+ this.iterations = PingPongComputePass.assertIterations(options.iterations ?? 1);
67
+ this.dispatch = options.dispatch ?? 'auto';
68
+ this.enabled = options.enabled ?? true;
69
+ this.workgroupSize = workgroupSize;
70
+ }
71
+
72
+ private static assertIterations(count: number): number {
73
+ if (!Number.isFinite(count) || count < 1 || !Number.isInteger(count)) {
74
+ throw new Error(
75
+ `PingPongComputePass iterations must be a positive integer >= 1, got ${count}`
76
+ );
77
+ }
78
+ return count;
79
+ }
80
+
81
+ /**
82
+ * Returns the texture key holding the latest result.
83
+ * Alternates between `{target}A` and `{target}B` based on total iteration parity.
84
+ */
85
+ getCurrentOutput(): string {
86
+ const totalIterations = this.frameCount * this.iterations;
87
+ return totalIterations % 2 === 0 ? `${this.target}A` : `${this.target}B`;
88
+ }
89
+
90
+ /**
91
+ * Advances the internal frame counter (called by renderer after each frame's iterations).
92
+ */
93
+ advanceFrame(): void {
94
+ this.frameCount += 1;
95
+ }
96
+
97
+ /**
98
+ * Replaces compute shader and updates workgroup size.
99
+ */
100
+ setCompute(compute: string): void {
101
+ assertComputeContract(compute);
102
+ const workgroupSize = extractWorkgroupSize(compute);
103
+ this.compute = compute;
104
+ this.workgroupSize = workgroupSize;
105
+ }
106
+
107
+ /**
108
+ * Updates iteration count.
109
+ *
110
+ * @param count - Must be >= 1.
111
+ */
112
+ setIterations(count: number): void {
113
+ this.iterations = PingPongComputePass.assertIterations(count);
114
+ }
115
+
116
+ /**
117
+ * Updates dispatch strategy.
118
+ */
119
+ setDispatch(dispatch: ComputePassOptions['dispatch']): void {
120
+ this.dispatch = dispatch ?? 'auto';
121
+ }
122
+
123
+ /**
124
+ * Returns the target texture key.
125
+ */
126
+ getTarget(): string {
127
+ return this.target;
128
+ }
129
+
130
+ /**
131
+ * Returns the current iteration count.
132
+ */
133
+ getIterations(): number {
134
+ return this.iterations;
135
+ }
136
+
137
+ /**
138
+ * Returns current compute shader source.
139
+ */
140
+ getCompute(): string {
141
+ return this.compute;
142
+ }
143
+
144
+ /**
145
+ * Returns parsed workgroup size.
146
+ */
147
+ getWorkgroupSize(): [number, number, number] {
148
+ return [...this.workgroupSize];
149
+ }
150
+
151
+ /**
152
+ * Resolves dispatch workgroup counts for current frame.
153
+ */
154
+ resolveDispatch(ctx: ComputeDispatchContext): [number, number, number] {
155
+ if (this.dispatch === 'auto') {
156
+ return [
157
+ Math.ceil(ctx.width / this.workgroupSize[0]),
158
+ Math.ceil(ctx.height / this.workgroupSize[1]),
159
+ Math.ceil(1 / this.workgroupSize[2])
160
+ ];
161
+ }
162
+
163
+ if (typeof this.dispatch === 'function') {
164
+ return this.dispatch(ctx);
165
+ }
166
+
167
+ if (Array.isArray(this.dispatch)) {
168
+ return [this.dispatch[0], this.dispatch[1] ?? 1, this.dispatch[2] ?? 1];
169
+ }
170
+
171
+ return [1, 1, 1];
172
+ }
173
+
174
+ /**
175
+ * Releases resources (no-op, GPU lifecycle is renderer-managed).
176
+ */
177
+ dispose(): void {
178
+ // No-op
179
+ }
180
+ }
@@ -65,8 +65,9 @@ export class ShaderPass extends FullscreenPass {
65
65
  * Replaces current shader fragment and invalidates pipeline cache.
66
66
  */
67
67
  setFragment(fragment: string): void {
68
+ const nextProgram = buildShaderPassProgram(fragment);
68
69
  this.fragment = fragment;
69
- this.program = buildShaderPassProgram(fragment);
70
+ this.program = nextProgram;
70
71
  this.invalidateFullscreenCache();
71
72
  }
72
73
 
@@ -1,3 +1,9 @@
1
1
  export { BlitPass, type BlitPassOptions } from './BlitPass.js';
2
2
  export { CopyPass, type CopyPassOptions } from './CopyPass.js';
3
3
  export { ShaderPass, type ShaderPassOptions } from './ShaderPass.js';
4
+ export {
5
+ ComputePass,
6
+ type ComputePassOptions,
7
+ type ComputeDispatchContext
8
+ } from './ComputePass.js';
9
+ export { PingPongComputePass, type PingPongComputePassOptions } from './PingPongComputePass.js';
@@ -4,8 +4,8 @@ import type { FragMaterial } from '../core/material.js';
4
4
  import { createFrameRegistry } from '../core/frame-registry.js';
5
5
  import { createMotionGPURuntimeLoop } from '../core/runtime-loop.js';
6
6
  import type {
7
+ AnyPass,
7
8
  OutputColorSpace,
8
- RenderPass,
9
9
  RenderMode,
10
10
  RenderTargetDefinitionMap
11
11
  } from '../core/types.js';
@@ -17,7 +17,7 @@ import { MotionGPUReactContext, type MotionGPUContext } from './motiongpu-contex
17
17
  export interface FragCanvasProps {
18
18
  material: FragMaterial;
19
19
  renderTargets?: RenderTargetDefinitionMap;
20
- passes?: RenderPass[];
20
+ passes?: AnyPass[];
21
21
  clearColor?: [number, number, number, number];
22
22
  outputColorSpace?: OutputColorSpace;
23
23
  renderMode?: RenderMode;
@@ -39,7 +39,7 @@ export interface FragCanvasProps {
39
39
  interface RuntimePropsSnapshot {
40
40
  material: FragMaterial;
41
41
  renderTargets: RenderTargetDefinitionMap;
42
- passes: RenderPass[];
42
+ passes: AnyPass[];
43
43
  clearColor: [number, number, number, number];
44
44
  outputColorSpace: OutputColorSpace;
45
45
  adapterOptions: GPURequestAdapterOptions | undefined;
@@ -75,6 +75,19 @@ const MOTIONGPU_ERROR_OVERLAY_STYLES = `
75
75
  border-bottom: 1px solid var(--motiongpu-color-border);
76
76
  }
77
77
 
78
+ .motiongpu-error-header-top {
79
+ display: flex;
80
+ align-items: flex-start;
81
+ gap: 0.75rem;
82
+ }
83
+
84
+ .motiongpu-error-badges {
85
+ display: inline-flex;
86
+ align-items: center;
87
+ gap: 0.4rem;
88
+ flex-wrap: wrap;
89
+ }
90
+
78
91
  .motiongpu-error-badge-wrap {
79
92
  display: inline-flex;
80
93
  align-items: center;
@@ -86,7 +99,7 @@ const MOTIONGPU_ERROR_OVERLAY_STYLES = `
86
99
  background: var(--motiongpu-color-background-muted);
87
100
  }
88
101
 
89
- .motiongpu-error-phase {
102
+ .motiongpu-error-badge {
90
103
  display: inline-flex;
91
104
  align-items: center;
92
105
  margin: 0;
@@ -106,6 +119,20 @@ const MOTIONGPU_ERROR_OVERLAY_STYLES = `
106
119
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.24);
107
120
  }
108
121
 
122
+ .motiongpu-error-recoverable {
123
+ margin: 0;
124
+ font-size: 0.67rem;
125
+ line-height: 1.2;
126
+ letter-spacing: 0.06em;
127
+ text-transform: uppercase;
128
+ color: var(--motiongpu-color-foreground-muted);
129
+ }
130
+
131
+ .motiongpu-error-recoverable span {
132
+ font-family: var(--motiongpu-font-mono);
133
+ color: var(--motiongpu-color-foreground);
134
+ }
135
+
109
136
  .motiongpu-error-title {
110
137
  margin: 0;
111
138
  font-size: clamp(1.02rem, 1vw + 0.72rem, 1.32rem);
@@ -285,6 +312,11 @@ const MOTIONGPU_ERROR_OVERLAY_STYLES = `
285
312
  .motiongpu-error-title {
286
313
  font-size: 1.02rem;
287
314
  }
315
+
316
+ .motiongpu-error-header-top {
317
+ flex-direction: column;
318
+ align-items: flex-start;
319
+ }
288
320
  }
289
321
 
290
322
  @media (prefers-reduced-motion: reduce) {
@@ -302,7 +334,87 @@ function normalizeErrorText(value: string): string {
302
334
  }
303
335
 
304
336
  function shouldShowErrorMessage(value: MotionGPUErrorReport): boolean {
305
- return normalizeErrorText(value.message) !== normalizeErrorText(value.title);
337
+ return resolveDisplayMessage(value).length > 0;
338
+ }
339
+
340
+ function resolveDisplayMessage(value: MotionGPUErrorReport): string {
341
+ const rawMessage = value.message.trim();
342
+ if (rawMessage.length === 0) {
343
+ return '';
344
+ }
345
+
346
+ const normalizedMessage = normalizeErrorText(rawMessage);
347
+ const normalizedTitle = normalizeErrorText(value.title);
348
+ if (normalizedMessage === normalizedTitle) {
349
+ return '';
350
+ }
351
+
352
+ const escapedTitle = value.title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
353
+ const prefixPattern = new RegExp(`^${escapedTitle}\\s*[:\\-|]\\s*`, 'i');
354
+ const stripped = rawMessage.replace(prefixPattern, '').trim();
355
+ return stripped.length > 0 ? stripped : rawMessage;
356
+ }
357
+
358
+ function formatRuntimeContext(context: MotionGPUErrorReport['context']): string {
359
+ if (!context) {
360
+ return '';
361
+ }
362
+
363
+ const indentBlock = (value: string, spaces = 2): string => {
364
+ const prefix = ' '.repeat(spaces);
365
+ return value
366
+ .split('\n')
367
+ .map((line) => `${prefix}${line}`)
368
+ .join('\n');
369
+ };
370
+
371
+ const formatMaterialSignature = (value: string): string => {
372
+ const trimmed = value.trim();
373
+ if (trimmed.length === 0) {
374
+ return '<empty>';
375
+ }
376
+ try {
377
+ return JSON.stringify(JSON.parse(trimmed), null, 2);
378
+ } catch {
379
+ return trimmed;
380
+ }
381
+ };
382
+
383
+ const lines: string[] = [];
384
+ if (context.materialSignature) {
385
+ lines.push('materialSignature:');
386
+ lines.push(indentBlock(formatMaterialSignature(context.materialSignature)));
387
+ }
388
+ if (context.passGraph) {
389
+ lines.push('passGraph:');
390
+ lines.push(` passCount: ${context.passGraph.passCount}`);
391
+ lines.push(` enabledPassCount: ${context.passGraph.enabledPassCount}`);
392
+ lines.push(' inputs:');
393
+ if (context.passGraph.inputs.length === 0) {
394
+ lines.push(' - <none>');
395
+ } else {
396
+ for (const input of context.passGraph.inputs) {
397
+ lines.push(` - ${input}`);
398
+ }
399
+ }
400
+ lines.push(' outputs:');
401
+ if (context.passGraph.outputs.length === 0) {
402
+ lines.push(' - <none>');
403
+ } else {
404
+ for (const output of context.passGraph.outputs) {
405
+ lines.push(` - ${output}`);
406
+ }
407
+ }
408
+ }
409
+ lines.push('activeRenderTargets:');
410
+ if (context.activeRenderTargets.length === 0) {
411
+ lines.push(' - <none>');
412
+ } else {
413
+ for (const target of context.activeRenderTargets) {
414
+ lines.push(` - ${target}`);
415
+ }
416
+ }
417
+ return lines.join('\n');
306
418
  }
307
419
 
308
420
  export function MotionGPUErrorOverlay({ report }: MotionGPUErrorOverlayProps) {
@@ -320,14 +432,28 @@ export function MotionGPUErrorOverlay({ report }: MotionGPUErrorOverlayProps) {
320
432
  data-testid="motiongpu-error"
321
433
  >
322
434
  <header className="motiongpu-error-header">
323
- <div className="motiongpu-error-badge-wrap">
324
- <p className="motiongpu-error-phase">{report.phase}</p>
435
+ <div className="motiongpu-error-header-top">
436
+ <div className="motiongpu-error-badges">
437
+ <div className="motiongpu-error-badge-wrap">
438
+ <p className="motiongpu-error-badge motiongpu-error-badge-phase">
439
+ {report.phase}
440
+ </p>
441
+ </div>
442
+ <div className="motiongpu-error-badge-wrap">
443
+ <p className="motiongpu-error-badge motiongpu-error-badge-severity">
444
+ {report.severity}
445
+ </p>
446
+ </div>
447
+ </div>
325
448
  </div>
326
449
  <h2 className="motiongpu-error-title">{report.title}</h2>
450
+ <p className="motiongpu-error-recoverable">
451
+ Recoverable: <span>{report.recoverable ? 'yes' : 'no'}</span>
452
+ </p>
327
453
  </header>
328
454
  <div className="motiongpu-error-body">
329
455
  {shouldShowErrorMessage(report) ? (
330
- <p className="motiongpu-error-message">{report.message}</p>
456
+ <p className="motiongpu-error-message">{resolveDisplayMessage(report)}</p>
331
457
  ) : null}
332
458
  <p className="motiongpu-error-hint">{report.hint}</p>
333
459
  </div>
@@ -384,6 +510,12 @@ export function MotionGPUErrorOverlay({ report }: MotionGPUErrorOverlayProps) {
384
510
  <pre>{report.stack.join('\n')}</pre>
385
511
  </details>
386
512
  ) : null}
513
+ {report.context ? (
514
+ <details className="motiongpu-error-details">
515
+ <summary>Runtime context</summary>
516
+ <pre>{formatRuntimeContext(report.context)}</pre>
517
+ </details>
518
+ ) : null}
387
519
  </div>
388
520
  </section>
389
521
  </div>
@@ -3,7 +3,13 @@
3
3
  */
4
4
  export { FragCanvas } from './FragCanvas.js';
5
5
  export { defineMaterial } from '../core/material.js';
6
- export { BlitPass, CopyPass, ShaderPass } from '../passes/index.js';
6
+ export {
7
+ BlitPass,
8
+ CopyPass,
9
+ ShaderPass,
10
+ ComputePass,
11
+ PingPongComputePass
12
+ } from '../passes/index.js';
7
13
  export { useMotionGPU } from './motiongpu-context.js';
8
14
  export { useFrame } from './frame-context.js';
9
15
  export { useTexture } from './use-texture.js';
@@ -11,6 +17,8 @@ export type {
11
17
  FrameInvalidationToken,
12
18
  FrameState,
13
19
  OutputColorSpace,
20
+ AnyPass,
21
+ ComputePassLike,
14
22
  RenderPass,
15
23
  RenderPassContext,
16
24
  RenderPassFlags,
@@ -49,3 +57,12 @@ export type {
49
57
  export type { MotionGPUContext } from './motiongpu-context.js';
50
58
  export type { UseFrameOptions, UseFrameResult } from './frame-context.js';
51
59
  export type { TextureUrlInput, UseTextureResult } from './use-texture.js';
60
+ export type {
61
+ StorageBufferAccess,
62
+ StorageBufferDefinition,
63
+ StorageBufferDefinitionMap,
64
+ StorageBufferType,
65
+ ComputePassContext
66
+ } from '../core/types.js';
67
+ export type { ComputePassOptions, ComputeDispatchContext } from '../passes/ComputePass.js';
68
+ export type { PingPongComputePassOptions } from '../passes/PingPongComputePass.js';
@@ -7,9 +7,9 @@
7
7
  import MotionGPUErrorOverlay from './MotionGPUErrorOverlay.svelte';
8
8
  import { createMotionGPURuntimeLoop } from '../core/runtime-loop';
9
9
  import type {
10
+ AnyPass,
10
11
  FrameInvalidationToken,
11
12
  OutputColorSpace,
12
- RenderPass,
13
13
  RenderMode,
14
14
  RenderTargetDefinitionMap
15
15
  } from '../core/types';
@@ -19,7 +19,7 @@
19
19
  interface Props {
20
20
  material: FragMaterial;
21
21
  renderTargets?: RenderTargetDefinitionMap;
22
- passes?: RenderPass[];
22
+ passes?: AnyPass[];
23
23
  clearColor?: [number, number, number, number];
24
24
  outputColorSpace?: OutputColorSpace;
25
25
  renderMode?: RenderMode;