@motion-core/motion-gpu 0.2.0 → 0.4.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 (52) hide show
  1. package/README.md +78 -1
  2. package/dist/core/current-value.js +3 -0
  3. package/dist/core/error-diagnostics.d.ts +14 -0
  4. package/dist/core/error-diagnostics.js +41 -1
  5. package/dist/core/error-report.d.ts +37 -0
  6. package/dist/core/error-report.js +60 -1
  7. package/dist/core/index.d.ts +1 -1
  8. package/dist/core/material-preprocess.d.ts +5 -5
  9. package/dist/core/material-preprocess.js +1 -4
  10. package/dist/core/material.d.ts +32 -23
  11. package/dist/core/material.js +14 -7
  12. package/dist/core/renderer.d.ts +10 -0
  13. package/dist/core/renderer.js +66 -4
  14. package/dist/core/runtime-loop.d.ts +3 -0
  15. package/dist/core/runtime-loop.js +72 -1
  16. package/dist/core/types.d.ts +24 -10
  17. package/dist/passes/BlitPass.d.ts +6 -27
  18. package/dist/passes/BlitPass.js +10 -121
  19. package/dist/passes/FullscreenPass.d.ts +37 -0
  20. package/dist/passes/FullscreenPass.js +131 -0
  21. package/dist/passes/ShaderPass.d.ts +6 -26
  22. package/dist/passes/ShaderPass.js +10 -121
  23. package/dist/react/FragCanvas.d.ts +26 -0
  24. package/dist/react/FragCanvas.js +218 -0
  25. package/dist/react/FragCanvas.tsx +345 -0
  26. package/dist/react/MotionGPUErrorOverlay.d.ts +6 -0
  27. package/dist/react/MotionGPUErrorOverlay.js +52 -0
  28. package/dist/react/MotionGPUErrorOverlay.tsx +129 -0
  29. package/dist/react/Portal.d.ts +6 -0
  30. package/dist/react/Portal.js +24 -0
  31. package/dist/react/Portal.tsx +34 -0
  32. package/dist/react/advanced.d.ts +11 -0
  33. package/dist/react/advanced.js +6 -0
  34. package/dist/react/frame-context.d.ts +14 -0
  35. package/dist/react/frame-context.js +98 -0
  36. package/dist/react/index.d.ts +15 -0
  37. package/dist/react/index.js +9 -0
  38. package/dist/react/motiongpu-context.d.ts +73 -0
  39. package/dist/react/motiongpu-context.js +18 -0
  40. package/dist/react/use-motiongpu-user-context.d.ts +49 -0
  41. package/dist/react/use-motiongpu-user-context.js +94 -0
  42. package/dist/react/use-texture.d.ts +40 -0
  43. package/dist/react/use-texture.js +162 -0
  44. package/dist/svelte/FragCanvas.svelte +45 -16
  45. package/dist/svelte/FragCanvas.svelte.d.ts +2 -0
  46. package/dist/svelte/MotionGPUErrorOverlay.svelte +10 -19
  47. package/dist/svelte/Portal.svelte +6 -21
  48. package/dist/svelte/use-motiongpu-user-context.d.ts +9 -1
  49. package/dist/svelte/use-motiongpu-user-context.js +4 -1
  50. package/dist/svelte/use-texture.d.ts +11 -2
  51. package/dist/svelte/use-texture.js +13 -3
  52. package/package.json +28 -3
@@ -1,40 +1,20 @@
1
- import type { RenderPass, RenderPassContext, RenderPassFlags, RenderPassInputSlot, RenderPassOutputSlot } from '../core/types.js';
2
- export interface ShaderPassOptions extends RenderPassFlags {
1
+ import { FullscreenPass, type FullscreenPassOptions } from './FullscreenPass.js';
2
+ export interface ShaderPassOptions extends FullscreenPassOptions {
3
3
  fragment: string;
4
- enabled?: boolean;
5
- needsSwap?: boolean;
6
- input?: RenderPassInputSlot;
7
- output?: RenderPassOutputSlot;
8
- filter?: GPUFilterMode;
9
4
  }
10
5
  /**
11
6
  * Fullscreen programmable shader pass.
12
7
  */
13
- export declare class ShaderPass implements RenderPass {
14
- enabled: boolean;
15
- needsSwap: boolean;
16
- input: RenderPassInputSlot;
17
- output: RenderPassOutputSlot;
18
- clear: boolean;
19
- clearColor: [number, number, number, number];
20
- preserve: boolean;
21
- private readonly filter;
8
+ export declare class ShaderPass extends FullscreenPass {
22
9
  private fragment;
23
10
  private program;
24
- private device;
25
- private sampler;
26
- private bindGroupLayout;
27
- private shaderModule;
28
- private readonly pipelineByFormat;
29
- private bindGroupByView;
30
11
  constructor(options: ShaderPassOptions);
31
12
  /**
32
13
  * Replaces current shader fragment and invalidates pipeline cache.
33
14
  */
34
15
  setFragment(fragment: string): void;
35
16
  getFragment(): string;
36
- private ensureResources;
37
- setSize(width: number, height: number): void;
38
- render(context: RenderPassContext): void;
39
- dispose(): void;
17
+ protected getProgram(): string;
18
+ protected getVertexEntryPoint(): string;
19
+ protected getFragmentEntryPoint(): string;
40
20
  }
@@ -1,3 +1,4 @@
1
+ import { FullscreenPass } from './FullscreenPass.js';
1
2
  const SHADER_PASS_CONTRACT = /\bfn\s+shade\s*\(\s*inputColor\s*:\s*vec4f\s*,\s*uv\s*:\s*vec2f\s*\)\s*->\s*vec4f/;
2
3
  function buildShaderPassProgram(fragment) {
3
4
  if (!SHADER_PASS_CONTRACT.test(fragment)) {
@@ -39,32 +40,11 @@ fn motiongpuShaderPassFragment(in: MotionGPUVertexOut) -> @location(0) vec4f {
39
40
  /**
40
41
  * Fullscreen programmable shader pass.
41
42
  */
42
- export class ShaderPass {
43
- enabled;
44
- needsSwap;
45
- input;
46
- output;
47
- clear;
48
- clearColor;
49
- preserve;
50
- filter;
43
+ export class ShaderPass extends FullscreenPass {
51
44
  fragment;
52
45
  program;
53
- device = null;
54
- sampler = null;
55
- bindGroupLayout = null;
56
- shaderModule = null;
57
- pipelineByFormat = new Map();
58
- bindGroupByView = new WeakMap();
59
46
  constructor(options) {
60
- this.enabled = options.enabled ?? true;
61
- this.needsSwap = options.needsSwap ?? true;
62
- this.input = options.input ?? 'source';
63
- this.output = options.output ?? (this.needsSwap ? 'target' : 'source');
64
- this.clear = options.clear ?? false;
65
- this.clearColor = options.clearColor ?? [0, 0, 0, 1];
66
- this.preserve = options.preserve ?? true;
67
- this.filter = options.filter ?? 'linear';
47
+ super(options);
68
48
  this.fragment = options.fragment;
69
49
  this.program = buildShaderPassProgram(options.fragment);
70
50
  }
@@ -74,109 +54,18 @@ export class ShaderPass {
74
54
  setFragment(fragment) {
75
55
  this.fragment = fragment;
76
56
  this.program = buildShaderPassProgram(fragment);
77
- this.shaderModule = null;
78
- this.pipelineByFormat.clear();
79
- this.bindGroupByView = new WeakMap();
57
+ this.invalidateFullscreenCache();
80
58
  }
81
59
  getFragment() {
82
60
  return this.fragment;
83
61
  }
84
- ensureResources(device, format) {
85
- if (this.device !== device) {
86
- this.device = device;
87
- this.sampler = null;
88
- this.bindGroupLayout = null;
89
- this.shaderModule = null;
90
- this.pipelineByFormat.clear();
91
- this.bindGroupByView = new WeakMap();
92
- }
93
- if (!this.sampler) {
94
- this.sampler = device.createSampler({
95
- magFilter: this.filter,
96
- minFilter: this.filter,
97
- addressModeU: 'clamp-to-edge',
98
- addressModeV: 'clamp-to-edge'
99
- });
100
- }
101
- if (!this.bindGroupLayout) {
102
- this.bindGroupLayout = device.createBindGroupLayout({
103
- entries: [
104
- {
105
- binding: 0,
106
- visibility: GPUShaderStage.FRAGMENT,
107
- sampler: { type: 'filtering' }
108
- },
109
- {
110
- binding: 1,
111
- visibility: GPUShaderStage.FRAGMENT,
112
- texture: {
113
- sampleType: 'float',
114
- viewDimension: '2d',
115
- multisampled: false
116
- }
117
- }
118
- ]
119
- });
120
- }
121
- if (!this.shaderModule) {
122
- this.shaderModule = device.createShaderModule({ code: this.program });
123
- }
124
- let pipeline = this.pipelineByFormat.get(format);
125
- if (!pipeline) {
126
- const pipelineLayout = device.createPipelineLayout({
127
- bindGroupLayouts: [this.bindGroupLayout]
128
- });
129
- pipeline = device.createRenderPipeline({
130
- layout: pipelineLayout,
131
- vertex: {
132
- module: this.shaderModule,
133
- entryPoint: 'motiongpuShaderPassVertex'
134
- },
135
- fragment: {
136
- module: this.shaderModule,
137
- entryPoint: 'motiongpuShaderPassFragment',
138
- targets: [{ format }]
139
- },
140
- primitive: { topology: 'triangle-list' }
141
- });
142
- this.pipelineByFormat.set(format, pipeline);
143
- }
144
- return {
145
- sampler: this.sampler,
146
- bindGroupLayout: this.bindGroupLayout,
147
- pipeline
148
- };
62
+ getProgram() {
63
+ return this.program;
149
64
  }
150
- setSize(width, height) {
151
- void width;
152
- void height;
65
+ getVertexEntryPoint() {
66
+ return 'motiongpuShaderPassVertex';
153
67
  }
154
- render(context) {
155
- const { sampler, bindGroupLayout, pipeline } = this.ensureResources(context.device, context.output.format);
156
- const inputView = context.input.view;
157
- let bindGroup = this.bindGroupByView.get(inputView);
158
- if (!bindGroup) {
159
- bindGroup = context.device.createBindGroup({
160
- layout: bindGroupLayout,
161
- entries: [
162
- { binding: 0, resource: sampler },
163
- { binding: 1, resource: inputView }
164
- ]
165
- });
166
- this.bindGroupByView.set(inputView, bindGroup);
167
- }
168
- const pass = context.beginRenderPass();
169
- pass.setPipeline(pipeline);
170
- pass.setBindGroup(0, bindGroup);
171
- pass.draw(3);
172
- pass.end();
173
- }
174
- dispose() {
175
- this.device = null;
176
- this.sampler = null;
177
- this.bindGroupLayout = null;
178
- this.shaderModule = null;
179
- this.pipelineByFormat.clear();
180
- this.bindGroupByView = new WeakMap();
68
+ getFragmentEntryPoint() {
69
+ return 'motiongpuShaderPassFragment';
181
70
  }
182
71
  }
@@ -0,0 +1,26 @@
1
+ import { type MotionGPUErrorReport } from '../core/error-report.js';
2
+ import type { FragMaterial } from '../core/material.js';
3
+ import type { OutputColorSpace, RenderPass, RenderMode, RenderTargetDefinitionMap } from '../core/types.js';
4
+ import { type CSSProperties, type ReactNode } from 'react';
5
+ export interface FragCanvasProps {
6
+ material: FragMaterial;
7
+ renderTargets?: RenderTargetDefinitionMap;
8
+ passes?: RenderPass[];
9
+ clearColor?: [number, number, number, number];
10
+ outputColorSpace?: OutputColorSpace;
11
+ renderMode?: RenderMode;
12
+ autoRender?: boolean;
13
+ maxDelta?: number;
14
+ adapterOptions?: GPURequestAdapterOptions;
15
+ deviceDescriptor?: GPUDeviceDescriptor;
16
+ dpr?: number;
17
+ showErrorOverlay?: boolean;
18
+ errorRenderer?: (report: MotionGPUErrorReport) => ReactNode;
19
+ onError?: (report: MotionGPUErrorReport) => void;
20
+ errorHistoryLimit?: number;
21
+ onErrorHistory?: (history: MotionGPUErrorReport[]) => void;
22
+ className?: string;
23
+ style?: CSSProperties;
24
+ children?: ReactNode;
25
+ }
26
+ export declare function FragCanvas({ material, renderTargets, passes, clearColor, outputColorSpace, renderMode, autoRender, maxDelta, adapterOptions, deviceDescriptor, dpr, showErrorOverlay, errorRenderer, onError, errorHistoryLimit, onErrorHistory, className, style, children }: FragCanvasProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,218 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createCurrentWritable as currentWritable } from '../core/current-value.js';
3
+ import { toMotionGPUErrorReport } from '../core/error-report.js';
4
+ import { createFrameRegistry } from '../core/frame-registry.js';
5
+ import { createMotionGPURuntimeLoop } from '../core/runtime-loop.js';
6
+ import { useEffect, useRef, useState } from 'react';
7
+ import { FrameRegistryReactContext } from './frame-context.js';
8
+ import { MotionGPUErrorOverlay } from './MotionGPUErrorOverlay.js';
9
+ import { MotionGPUReactContext } from './motiongpu-context.js';
10
+ function getInitialDpr() {
11
+ if (typeof window === 'undefined') {
12
+ return 1;
13
+ }
14
+ return window.devicePixelRatio ?? 1;
15
+ }
16
+ function createRuntimeState(initialDpr) {
17
+ const registry = createFrameRegistry({ maxDelta: 0.1 });
18
+ const canvasRef = { current: undefined };
19
+ const requestFrameSignalRef = { current: null };
20
+ const requestFrame = () => {
21
+ requestFrameSignalRef.current?.();
22
+ };
23
+ const invalidateFrame = () => {
24
+ registry.invalidate();
25
+ requestFrame();
26
+ };
27
+ const advanceFrame = () => {
28
+ registry.advance();
29
+ requestFrame();
30
+ };
31
+ const size = currentWritable({ width: 0, height: 0 });
32
+ const dprState = currentWritable(initialDpr, requestFrame);
33
+ const maxDeltaState = currentWritable(0.1, (value) => {
34
+ registry.setMaxDelta(value);
35
+ requestFrame();
36
+ });
37
+ const renderModeState = currentWritable('always', (value) => {
38
+ registry.setRenderMode(value);
39
+ requestFrame();
40
+ });
41
+ const autoRenderState = currentWritable(true, (value) => {
42
+ registry.setAutoRender(value);
43
+ requestFrame();
44
+ });
45
+ const userState = currentWritable({});
46
+ const context = {
47
+ get canvas() {
48
+ return canvasRef.current;
49
+ },
50
+ size,
51
+ dpr: dprState,
52
+ maxDelta: maxDeltaState,
53
+ renderMode: renderModeState,
54
+ autoRender: autoRenderState,
55
+ user: userState,
56
+ invalidate: invalidateFrame,
57
+ advance: advanceFrame,
58
+ scheduler: {
59
+ createStage: registry.createStage,
60
+ getStage: registry.getStage,
61
+ setDiagnosticsEnabled: registry.setDiagnosticsEnabled,
62
+ getDiagnosticsEnabled: registry.getDiagnosticsEnabled,
63
+ getLastRunTimings: registry.getLastRunTimings,
64
+ getSchedule: registry.getSchedule,
65
+ setProfilingEnabled: registry.setProfilingEnabled,
66
+ setProfilingWindow: registry.setProfilingWindow,
67
+ resetProfiling: registry.resetProfiling,
68
+ getProfilingEnabled: registry.getProfilingEnabled,
69
+ getProfilingWindow: registry.getProfilingWindow,
70
+ getProfilingSnapshot: registry.getProfilingSnapshot
71
+ }
72
+ };
73
+ return {
74
+ registry,
75
+ context,
76
+ canvasRef,
77
+ size,
78
+ dprState,
79
+ maxDeltaState,
80
+ renderModeState,
81
+ autoRenderState,
82
+ requestFrameSignalRef,
83
+ requestFrame,
84
+ invalidateFrame,
85
+ advanceFrame
86
+ };
87
+ }
88
+ function getNormalizedErrorHistoryLimit(value) {
89
+ if (!Number.isFinite(value) || value <= 0) {
90
+ return 0;
91
+ }
92
+ return Math.floor(value);
93
+ }
94
+ export function FragCanvas({ material, renderTargets = {}, passes = [], clearColor = [0, 0, 0, 1], outputColorSpace = 'srgb', renderMode = 'always', autoRender = true, maxDelta = 0.1, adapterOptions = undefined, deviceDescriptor = undefined, dpr = getInitialDpr(), showErrorOverlay = true, errorRenderer, onError = undefined, errorHistoryLimit = 0, onErrorHistory = undefined, className = '', style, children }) {
95
+ const runtimeRef = useRef(null);
96
+ if (!runtimeRef.current) {
97
+ runtimeRef.current = createRuntimeState(getInitialDpr());
98
+ }
99
+ const runtime = runtimeRef.current;
100
+ const runtimePropsRef = useRef({
101
+ material,
102
+ renderTargets,
103
+ passes,
104
+ clearColor,
105
+ outputColorSpace,
106
+ adapterOptions,
107
+ deviceDescriptor,
108
+ onError,
109
+ errorHistoryLimit,
110
+ onErrorHistory
111
+ });
112
+ runtimePropsRef.current = {
113
+ material,
114
+ renderTargets,
115
+ passes,
116
+ clearColor,
117
+ outputColorSpace,
118
+ adapterOptions,
119
+ deviceDescriptor,
120
+ onError,
121
+ errorHistoryLimit,
122
+ onErrorHistory
123
+ };
124
+ const [errorReport, setErrorReport] = useState(null);
125
+ const [errorHistory, setErrorHistory] = useState([]);
126
+ useEffect(() => {
127
+ runtime.renderModeState.set(renderMode);
128
+ }, [renderMode, runtime]);
129
+ useEffect(() => {
130
+ runtime.autoRenderState.set(autoRender);
131
+ }, [autoRender, runtime]);
132
+ useEffect(() => {
133
+ runtime.maxDeltaState.set(maxDelta);
134
+ }, [maxDelta, runtime]);
135
+ useEffect(() => {
136
+ runtime.dprState.set(dpr);
137
+ }, [dpr, runtime]);
138
+ useEffect(() => {
139
+ const limit = getNormalizedErrorHistoryLimit(errorHistoryLimit);
140
+ if (limit <= 0) {
141
+ if (errorHistory.length === 0) {
142
+ return;
143
+ }
144
+ setErrorHistory([]);
145
+ onErrorHistory?.([]);
146
+ return;
147
+ }
148
+ if (errorHistory.length <= limit) {
149
+ return;
150
+ }
151
+ const trimmed = errorHistory.slice(errorHistory.length - limit);
152
+ setErrorHistory(trimmed);
153
+ onErrorHistory?.(trimmed);
154
+ }, [errorHistory, errorHistoryLimit, onErrorHistory]);
155
+ useEffect(() => {
156
+ const canvas = runtime.canvasRef.current;
157
+ if (!canvas) {
158
+ const report = toMotionGPUErrorReport(new Error('Canvas element is not available'), 'initialization');
159
+ setErrorReport(report);
160
+ const historyLimit = getNormalizedErrorHistoryLimit(runtimePropsRef.current.errorHistoryLimit);
161
+ if (historyLimit > 0) {
162
+ const nextHistory = [report].slice(-historyLimit);
163
+ setErrorHistory(nextHistory);
164
+ runtimePropsRef.current.onErrorHistory?.(nextHistory);
165
+ }
166
+ runtimePropsRef.current.onError?.(report);
167
+ return () => {
168
+ runtime.registry.clear();
169
+ };
170
+ }
171
+ const runtimeLoop = createMotionGPURuntimeLoop({
172
+ canvas,
173
+ registry: runtime.registry,
174
+ size: runtime.size,
175
+ dpr: runtime.dprState,
176
+ maxDelta: runtime.maxDeltaState,
177
+ getMaterial: () => runtimePropsRef.current.material,
178
+ getRenderTargets: () => runtimePropsRef.current.renderTargets,
179
+ getPasses: () => runtimePropsRef.current.passes,
180
+ getClearColor: () => runtimePropsRef.current.clearColor,
181
+ getOutputColorSpace: () => runtimePropsRef.current.outputColorSpace,
182
+ getAdapterOptions: () => runtimePropsRef.current.adapterOptions,
183
+ getDeviceDescriptor: () => runtimePropsRef.current.deviceDescriptor,
184
+ getOnError: () => runtimePropsRef.current.onError,
185
+ getErrorHistoryLimit: () => runtimePropsRef.current.errorHistoryLimit,
186
+ getOnErrorHistory: () => runtimePropsRef.current.onErrorHistory,
187
+ reportErrorHistory: (history) => {
188
+ setErrorHistory(history);
189
+ },
190
+ reportError: (report) => {
191
+ setErrorReport(report);
192
+ }
193
+ });
194
+ runtime.requestFrameSignalRef.current = runtimeLoop.requestFrame;
195
+ return () => {
196
+ runtime.requestFrameSignalRef.current = null;
197
+ runtimeLoop.destroy();
198
+ };
199
+ }, [runtime]);
200
+ const canvasStyle = {
201
+ position: 'absolute',
202
+ inset: 0,
203
+ display: 'block',
204
+ width: '100%',
205
+ height: '100%',
206
+ ...style
207
+ };
208
+ return (_jsx(FrameRegistryReactContext.Provider, { value: runtime.registry, children: _jsx(MotionGPUReactContext.Provider, { value: runtime.context, children: _jsxs("div", { className: "motiongpu-canvas-wrap", style: {
209
+ position: 'relative',
210
+ width: '100%',
211
+ height: '100%',
212
+ minWidth: 0,
213
+ minHeight: 0,
214
+ overflow: 'hidden'
215
+ }, children: [_jsx("canvas", { className: className, style: canvasStyle, ref: (node) => {
216
+ runtime.canvasRef.current = node ?? undefined;
217
+ } }), showErrorOverlay && errorReport ? (errorRenderer ? (errorRenderer(errorReport)) : (_jsx(MotionGPUErrorOverlay, { report: errorReport }))) : null, children] }) }) }));
218
+ }