@motion-core/motion-gpu 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import { FullscreenPass } from './FullscreenPass.js';
1
2
  const FULLSCREEN_BLIT_SHADER = `
2
3
  struct MotionGPUVertexOut {
3
4
  @builtin(position) position: vec4f,
@@ -30,129 +31,17 @@ fn motiongpuBlitFragment(in: MotionGPUVertexOut) -> @location(0) vec4f {
30
31
  /**
31
32
  * Fullscreen texture blit pass.
32
33
  */
33
- export class BlitPass {
34
- enabled;
35
- needsSwap;
36
- input;
37
- output;
38
- clear;
39
- clearColor;
40
- preserve;
41
- filter;
42
- device = null;
43
- sampler = null;
44
- bindGroupLayout = null;
45
- shaderModule = null;
46
- pipelineByFormat = new Map();
47
- bindGroupByView = new WeakMap();
48
- constructor(options = {}) {
49
- this.enabled = options.enabled ?? true;
50
- this.needsSwap = options.needsSwap ?? true;
51
- this.input = options.input ?? 'source';
52
- this.output = options.output ?? (this.needsSwap ? 'target' : 'source');
53
- this.clear = options.clear ?? false;
54
- this.clearColor = options.clearColor ?? [0, 0, 0, 1];
55
- this.preserve = options.preserve ?? true;
56
- this.filter = options.filter ?? 'linear';
57
- }
58
- ensureResources(device, format) {
59
- if (this.device !== device) {
60
- this.device = device;
61
- this.sampler = null;
62
- this.bindGroupLayout = null;
63
- this.shaderModule = null;
64
- this.pipelineByFormat.clear();
65
- this.bindGroupByView = new WeakMap();
66
- }
67
- if (!this.sampler) {
68
- this.sampler = device.createSampler({
69
- magFilter: this.filter,
70
- minFilter: this.filter,
71
- addressModeU: 'clamp-to-edge',
72
- addressModeV: 'clamp-to-edge'
73
- });
74
- }
75
- if (!this.bindGroupLayout) {
76
- this.bindGroupLayout = device.createBindGroupLayout({
77
- entries: [
78
- {
79
- binding: 0,
80
- visibility: GPUShaderStage.FRAGMENT,
81
- sampler: { type: 'filtering' }
82
- },
83
- {
84
- binding: 1,
85
- visibility: GPUShaderStage.FRAGMENT,
86
- texture: {
87
- sampleType: 'float',
88
- viewDimension: '2d',
89
- multisampled: false
90
- }
91
- }
92
- ]
93
- });
94
- }
95
- if (!this.shaderModule) {
96
- this.shaderModule = device.createShaderModule({
97
- code: FULLSCREEN_BLIT_SHADER
98
- });
99
- }
100
- let pipeline = this.pipelineByFormat.get(format);
101
- if (!pipeline) {
102
- const pipelineLayout = device.createPipelineLayout({
103
- bindGroupLayouts: [this.bindGroupLayout]
104
- });
105
- pipeline = device.createRenderPipeline({
106
- layout: pipelineLayout,
107
- vertex: {
108
- module: this.shaderModule,
109
- entryPoint: 'motiongpuBlitVertex'
110
- },
111
- fragment: {
112
- module: this.shaderModule,
113
- entryPoint: 'motiongpuBlitFragment',
114
- targets: [{ format }]
115
- },
116
- primitive: { topology: 'triangle-list' }
117
- });
118
- this.pipelineByFormat.set(format, pipeline);
119
- }
120
- return {
121
- sampler: this.sampler,
122
- bindGroupLayout: this.bindGroupLayout,
123
- pipeline
124
- };
34
+ export class BlitPass extends FullscreenPass {
35
+ getProgram() {
36
+ return FULLSCREEN_BLIT_SHADER;
125
37
  }
126
- setSize(width, height) {
127
- void width;
128
- void height;
38
+ constructor(options = {}) {
39
+ super(options);
129
40
  }
130
- render(context) {
131
- const { sampler, bindGroupLayout, pipeline } = this.ensureResources(context.device, context.output.format);
132
- const inputView = context.input.view;
133
- let bindGroup = this.bindGroupByView.get(inputView);
134
- if (!bindGroup) {
135
- bindGroup = context.device.createBindGroup({
136
- layout: bindGroupLayout,
137
- entries: [
138
- { binding: 0, resource: sampler },
139
- { binding: 1, resource: inputView }
140
- ]
141
- });
142
- this.bindGroupByView.set(inputView, bindGroup);
143
- }
144
- const pass = context.beginRenderPass();
145
- pass.setPipeline(pipeline);
146
- pass.setBindGroup(0, bindGroup);
147
- pass.draw(3);
148
- pass.end();
41
+ getVertexEntryPoint() {
42
+ return 'motiongpuBlitVertex';
149
43
  }
150
- dispose() {
151
- this.device = null;
152
- this.sampler = null;
153
- this.bindGroupLayout = null;
154
- this.shaderModule = null;
155
- this.pipelineByFormat.clear();
156
- this.bindGroupByView = new WeakMap();
44
+ getFragmentEntryPoint() {
45
+ return 'motiongpuBlitFragment';
157
46
  }
158
47
  }
@@ -0,0 +1,37 @@
1
+ import type { RenderPass, RenderPassContext, RenderPassFlags, RenderPassInputSlot, RenderPassOutputSlot } from '../core/types.js';
2
+ export interface FullscreenPassOptions extends RenderPassFlags {
3
+ enabled?: boolean;
4
+ needsSwap?: boolean;
5
+ input?: RenderPassInputSlot;
6
+ output?: RenderPassOutputSlot;
7
+ filter?: GPUFilterMode;
8
+ }
9
+ /**
10
+ * Shared base for fullscreen texture sampling passes.
11
+ */
12
+ export declare abstract class FullscreenPass implements RenderPass {
13
+ enabled: boolean;
14
+ needsSwap: boolean;
15
+ input: RenderPassInputSlot;
16
+ output: RenderPassOutputSlot;
17
+ clear: boolean;
18
+ clearColor: [number, number, number, number];
19
+ preserve: boolean;
20
+ private readonly filter;
21
+ private device;
22
+ private sampler;
23
+ private bindGroupLayout;
24
+ private shaderModule;
25
+ private readonly pipelineByFormat;
26
+ private bindGroupByView;
27
+ protected constructor(options?: FullscreenPassOptions);
28
+ protected abstract getProgram(): string;
29
+ protected abstract getVertexEntryPoint(): string;
30
+ protected abstract getFragmentEntryPoint(): string;
31
+ protected invalidateFullscreenCache(): void;
32
+ private ensureResources;
33
+ setSize(width: number, height: number): void;
34
+ protected renderFullscreen(context: RenderPassContext): void;
35
+ render(context: RenderPassContext): void;
36
+ dispose(): void;
37
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Shared base for fullscreen texture sampling passes.
3
+ */
4
+ export class FullscreenPass {
5
+ enabled;
6
+ needsSwap;
7
+ input;
8
+ output;
9
+ clear;
10
+ clearColor;
11
+ preserve;
12
+ filter;
13
+ device = null;
14
+ sampler = null;
15
+ bindGroupLayout = null;
16
+ shaderModule = null;
17
+ pipelineByFormat = new Map();
18
+ bindGroupByView = new WeakMap();
19
+ constructor(options = {}) {
20
+ this.enabled = options.enabled ?? true;
21
+ this.needsSwap = options.needsSwap ?? true;
22
+ this.input = options.input ?? 'source';
23
+ this.output = options.output ?? (this.needsSwap ? 'target' : 'source');
24
+ this.clear = options.clear ?? false;
25
+ this.clearColor = options.clearColor ?? [0, 0, 0, 1];
26
+ this.preserve = options.preserve ?? true;
27
+ this.filter = options.filter ?? 'linear';
28
+ }
29
+ invalidateFullscreenCache() {
30
+ this.shaderModule = null;
31
+ this.pipelineByFormat.clear();
32
+ this.bindGroupByView = new WeakMap();
33
+ }
34
+ ensureResources(device, format) {
35
+ if (this.device !== device) {
36
+ this.device = device;
37
+ this.sampler = null;
38
+ this.bindGroupLayout = null;
39
+ this.invalidateFullscreenCache();
40
+ }
41
+ if (!this.sampler) {
42
+ this.sampler = device.createSampler({
43
+ magFilter: this.filter,
44
+ minFilter: this.filter,
45
+ addressModeU: 'clamp-to-edge',
46
+ addressModeV: 'clamp-to-edge'
47
+ });
48
+ }
49
+ if (!this.bindGroupLayout) {
50
+ this.bindGroupLayout = device.createBindGroupLayout({
51
+ entries: [
52
+ {
53
+ binding: 0,
54
+ visibility: GPUShaderStage.FRAGMENT,
55
+ sampler: { type: 'filtering' }
56
+ },
57
+ {
58
+ binding: 1,
59
+ visibility: GPUShaderStage.FRAGMENT,
60
+ texture: {
61
+ sampleType: 'float',
62
+ viewDimension: '2d',
63
+ multisampled: false
64
+ }
65
+ }
66
+ ]
67
+ });
68
+ }
69
+ if (!this.shaderModule) {
70
+ this.shaderModule = device.createShaderModule({ code: this.getProgram() });
71
+ }
72
+ let pipeline = this.pipelineByFormat.get(format);
73
+ if (!pipeline) {
74
+ const pipelineLayout = device.createPipelineLayout({
75
+ bindGroupLayouts: [this.bindGroupLayout]
76
+ });
77
+ pipeline = device.createRenderPipeline({
78
+ layout: pipelineLayout,
79
+ vertex: {
80
+ module: this.shaderModule,
81
+ entryPoint: this.getVertexEntryPoint()
82
+ },
83
+ fragment: {
84
+ module: this.shaderModule,
85
+ entryPoint: this.getFragmentEntryPoint(),
86
+ targets: [{ format }]
87
+ },
88
+ primitive: { topology: 'triangle-list' }
89
+ });
90
+ this.pipelineByFormat.set(format, pipeline);
91
+ }
92
+ return {
93
+ sampler: this.sampler,
94
+ bindGroupLayout: this.bindGroupLayout,
95
+ pipeline
96
+ };
97
+ }
98
+ setSize(width, height) {
99
+ void width;
100
+ void height;
101
+ }
102
+ renderFullscreen(context) {
103
+ const { sampler, bindGroupLayout, pipeline } = this.ensureResources(context.device, context.output.format);
104
+ const inputView = context.input.view;
105
+ let bindGroup = this.bindGroupByView.get(inputView);
106
+ if (!bindGroup) {
107
+ bindGroup = context.device.createBindGroup({
108
+ layout: bindGroupLayout,
109
+ entries: [
110
+ { binding: 0, resource: sampler },
111
+ { binding: 1, resource: inputView }
112
+ ]
113
+ });
114
+ this.bindGroupByView.set(inputView, bindGroup);
115
+ }
116
+ const pass = context.beginRenderPass();
117
+ pass.setPipeline(pipeline);
118
+ pass.setBindGroup(0, bindGroup);
119
+ pass.draw(3);
120
+ pass.end();
121
+ }
122
+ render(context) {
123
+ this.renderFullscreen(context);
124
+ }
125
+ dispose() {
126
+ this.device = null;
127
+ this.sampler = null;
128
+ this.bindGroupLayout = null;
129
+ this.invalidateFullscreenCache();
130
+ }
131
+ }
@@ -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
  }
@@ -31,6 +31,8 @@
31
31
  showErrorOverlay?: boolean;
32
32
  errorRenderer?: Snippet<[MotionGPUErrorReport]>;
33
33
  onError?: (report: MotionGPUErrorReport) => void;
34
+ errorHistoryLimit?: number;
35
+ onErrorHistory?: (history: MotionGPUErrorReport[]) => void;
34
36
  class?: string;
35
37
  style?: string;
36
38
  children?: Snippet;
@@ -53,6 +55,8 @@
53
55
  showErrorOverlay = true,
54
56
  errorRenderer = undefined,
55
57
  onError = undefined,
58
+ errorHistoryLimit = 0,
59
+ onErrorHistory = undefined,
56
60
  class: className = '',
57
61
  style = '',
58
62
  children
@@ -60,6 +64,14 @@
60
64
 
61
65
  let canvas: HTMLCanvasElement | undefined;
62
66
  let errorReport = $state<MotionGPUErrorReport | null>(null);
67
+ let errorHistory = $state<MotionGPUErrorReport[]>([]);
68
+
69
+ const getNormalizedErrorHistoryLimit = (): number => {
70
+ if (!Number.isFinite(errorHistoryLimit) || errorHistoryLimit <= 0) {
71
+ return 0;
72
+ }
73
+ return Math.floor(errorHistoryLimit);
74
+ };
63
75
 
64
76
  const bindCanvas = (node: HTMLCanvasElement) => {
65
77
  canvas = node;
@@ -150,6 +162,26 @@
150
162
  requestFrame();
151
163
  });
152
164
 
165
+ $effect(() => {
166
+ const limit = getNormalizedErrorHistoryLimit();
167
+ if (limit <= 0) {
168
+ if (errorHistory.length === 0) {
169
+ return;
170
+ }
171
+ errorHistory = [];
172
+ onErrorHistory?.([]);
173
+ return;
174
+ }
175
+
176
+ if (errorHistory.length <= limit) {
177
+ return;
178
+ }
179
+
180
+ const trimmed = errorHistory.slice(errorHistory.length - limit);
181
+ errorHistory = trimmed;
182
+ onErrorHistory?.(trimmed);
183
+ });
184
+
153
185
  onMount(() => {
154
186
  if (!canvas) {
155
187
  const report = toMotionGPUErrorReport(
@@ -157,6 +189,12 @@
157
189
  'initialization'
158
190
  );
159
191
  errorReport = report;
192
+ const historyLimit = getNormalizedErrorHistoryLimit();
193
+ if (historyLimit > 0) {
194
+ const nextHistory = [report].slice(-historyLimit);
195
+ errorHistory = nextHistory;
196
+ onErrorHistory?.(nextHistory);
197
+ }
160
198
  onError?.(report);
161
199
  return () => registry.clear();
162
200
  }
@@ -175,6 +213,11 @@
175
213
  getAdapterOptions: () => adapterOptions,
176
214
  getDeviceDescriptor: () => deviceDescriptor,
177
215
  getOnError: () => onError,
216
+ getErrorHistoryLimit: () => errorHistoryLimit,
217
+ getOnErrorHistory: () => onErrorHistory,
218
+ reportErrorHistory: (history) => {
219
+ errorHistory = history;
220
+ },
178
221
  reportError: (report) => {
179
222
  errorReport = report;
180
223
  }
@@ -17,6 +17,8 @@ interface Props {
17
17
  showErrorOverlay?: boolean;
18
18
  errorRenderer?: Snippet<[MotionGPUErrorReport]>;
19
19
  onError?: (report: MotionGPUErrorReport) => void;
20
+ errorHistoryLimit?: number;
21
+ onErrorHistory?: (history: MotionGPUErrorReport[]) => void;
20
22
  class?: string;
21
23
  style?: string;
22
24
  children?: Snippet;