@motion-core/motion-gpu 0.1.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.
- package/LICENSE +21 -0
- package/README.md +325 -0
- package/dist/FragCanvas.svelte +511 -0
- package/dist/FragCanvas.svelte.d.ts +26 -0
- package/dist/MotionGPUErrorOverlay.svelte +394 -0
- package/dist/MotionGPUErrorOverlay.svelte.d.ts +7 -0
- package/dist/Portal.svelte +46 -0
- package/dist/Portal.svelte.d.ts +8 -0
- package/dist/advanced-scheduler.d.ts +44 -0
- package/dist/advanced-scheduler.js +58 -0
- package/dist/advanced.d.ts +14 -0
- package/dist/advanced.js +9 -0
- package/dist/core/error-diagnostics.d.ts +40 -0
- package/dist/core/error-diagnostics.js +111 -0
- package/dist/core/error-report.d.ts +67 -0
- package/dist/core/error-report.js +190 -0
- package/dist/core/material-preprocess.d.ts +63 -0
- package/dist/core/material-preprocess.js +166 -0
- package/dist/core/material.d.ts +157 -0
- package/dist/core/material.js +358 -0
- package/dist/core/recompile-policy.d.ts +27 -0
- package/dist/core/recompile-policy.js +15 -0
- package/dist/core/render-graph.d.ts +55 -0
- package/dist/core/render-graph.js +73 -0
- package/dist/core/render-targets.d.ts +39 -0
- package/dist/core/render-targets.js +63 -0
- package/dist/core/renderer.d.ts +9 -0
- package/dist/core/renderer.js +1097 -0
- package/dist/core/shader.d.ts +42 -0
- package/dist/core/shader.js +196 -0
- package/dist/core/texture-loader.d.ts +129 -0
- package/dist/core/texture-loader.js +295 -0
- package/dist/core/textures.d.ts +114 -0
- package/dist/core/textures.js +136 -0
- package/dist/core/types.d.ts +523 -0
- package/dist/core/types.js +4 -0
- package/dist/core/uniforms.d.ts +48 -0
- package/dist/core/uniforms.js +222 -0
- package/dist/current-writable.d.ts +31 -0
- package/dist/current-writable.js +27 -0
- package/dist/frame-context.d.ts +287 -0
- package/dist/frame-context.js +731 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +11 -0
- package/dist/motiongpu-context.d.ts +77 -0
- package/dist/motiongpu-context.js +26 -0
- package/dist/passes/BlitPass.d.ts +32 -0
- package/dist/passes/BlitPass.js +158 -0
- package/dist/passes/CopyPass.d.ts +25 -0
- package/dist/passes/CopyPass.js +53 -0
- package/dist/passes/ShaderPass.d.ts +40 -0
- package/dist/passes/ShaderPass.js +182 -0
- package/dist/passes/index.d.ts +3 -0
- package/dist/passes/index.js +3 -0
- package/dist/use-motiongpu-user-context.d.ts +35 -0
- package/dist/use-motiongpu-user-context.js +74 -0
- package/dist/use-texture.d.ts +35 -0
- package/dist/use-texture.js +147 -0
- package/package.json +94 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public MotionGPU package entrypoint.
|
|
3
|
+
*
|
|
4
|
+
* Exposes the production-ready core API for fullscreen WGSL rendering workflows.
|
|
5
|
+
*/
|
|
6
|
+
export { default as FragCanvas } from './FragCanvas.svelte';
|
|
7
|
+
export { defineMaterial } from './core/material';
|
|
8
|
+
export { BlitPass, CopyPass, ShaderPass } from './passes';
|
|
9
|
+
export { useMotionGPU } from './motiongpu-context';
|
|
10
|
+
export { useFrame } from './frame-context';
|
|
11
|
+
export { useTexture } from './use-texture';
|
|
12
|
+
export type { FrameInvalidationToken, FrameState, OutputColorSpace, RenderPass, RenderPassContext, RenderPassFlags, RenderPassInputSlot, RenderPassOutputSlot, RenderMode, RenderTarget, RenderTargetDefinition, RenderTargetDefinitionMap, TextureData, TextureDefinition, TextureDefinitionMap, TextureUpdateMode, TextureMap, TextureSource, TextureValue, TypedUniform, UniformMat4Value, UniformMap, UniformType, UniformValue } from './core/types';
|
|
13
|
+
export type { LoadedTexture, TextureDecodeOptions, TextureLoadOptions } from './core/texture-loader';
|
|
14
|
+
export type { FragMaterial, FragMaterialInput, MaterialIncludes, MaterialDefineValue, MaterialDefines, TypedMaterialDefineValue } from './core/material';
|
|
15
|
+
export type { MotionGPUContext } from './motiongpu-context';
|
|
16
|
+
export type { UseFrameOptions, UseFrameResult } from './frame-context';
|
|
17
|
+
export type { TextureUrlInput, UseTextureResult } from './use-texture';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public MotionGPU package entrypoint.
|
|
3
|
+
*
|
|
4
|
+
* Exposes the production-ready core API for fullscreen WGSL rendering workflows.
|
|
5
|
+
*/
|
|
6
|
+
export { default as FragCanvas } from './FragCanvas.svelte';
|
|
7
|
+
export { defineMaterial } from './core/material';
|
|
8
|
+
export { BlitPass, CopyPass, ShaderPass } from './passes';
|
|
9
|
+
export { useMotionGPU } from './motiongpu-context';
|
|
10
|
+
export { useFrame } from './frame-context';
|
|
11
|
+
export { useTexture } from './use-texture';
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { RenderMode } from './core/types';
|
|
2
|
+
import type { CurrentReadable, CurrentWritable } from './current-writable';
|
|
3
|
+
import type { FrameProfilingSnapshot, FrameRegistry, FrameRunTimings, FrameScheduleSnapshot } from './frame-context';
|
|
4
|
+
/**
|
|
5
|
+
* Exposed subset of frame scheduler controls intended for public consumption.
|
|
6
|
+
*/
|
|
7
|
+
export type MotionGPUScheduler = Pick<FrameRegistry, 'createStage' | 'getStage' | 'setDiagnosticsEnabled' | 'getDiagnosticsEnabled' | 'getLastRunTimings' | 'getSchedule' | 'setProfilingEnabled' | 'setProfilingWindow' | 'resetProfiling' | 'getProfilingEnabled' | 'getProfilingWindow' | 'getProfilingSnapshot'>;
|
|
8
|
+
export type { FrameProfilingSnapshot, FrameRunTimings, FrameScheduleSnapshot };
|
|
9
|
+
/**
|
|
10
|
+
* Namespace identifier for user-owned context entries.
|
|
11
|
+
*/
|
|
12
|
+
export type MotionGPUUserNamespace = string | symbol;
|
|
13
|
+
/**
|
|
14
|
+
* Shared user context store exposed by `FragCanvas`.
|
|
15
|
+
*/
|
|
16
|
+
export type MotionGPUUserContext = CurrentWritable<Record<MotionGPUUserNamespace, unknown>>;
|
|
17
|
+
/**
|
|
18
|
+
* Public `FragCanvas` runtime context available to hooks and user components.
|
|
19
|
+
*/
|
|
20
|
+
export interface MotionGPUContext {
|
|
21
|
+
/**
|
|
22
|
+
* Underlying canvas element used by the renderer.
|
|
23
|
+
*/
|
|
24
|
+
canvas: HTMLCanvasElement | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Reactive canvas pixel size.
|
|
27
|
+
*/
|
|
28
|
+
size: CurrentReadable<{
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
}>;
|
|
32
|
+
/**
|
|
33
|
+
* Device pixel ratio multiplier.
|
|
34
|
+
*/
|
|
35
|
+
dpr: CurrentWritable<number>;
|
|
36
|
+
/**
|
|
37
|
+
* Max frame delta clamp passed to scheduled callbacks.
|
|
38
|
+
*/
|
|
39
|
+
maxDelta: CurrentWritable<number>;
|
|
40
|
+
/**
|
|
41
|
+
* Scheduler render mode (`always`, `on-demand`, `manual`).
|
|
42
|
+
*/
|
|
43
|
+
renderMode: CurrentWritable<RenderMode>;
|
|
44
|
+
/**
|
|
45
|
+
* Global toggle for automatic rendering.
|
|
46
|
+
*/
|
|
47
|
+
autoRender: CurrentWritable<boolean>;
|
|
48
|
+
/**
|
|
49
|
+
* Namespaced user context store shared within the canvas subtree.
|
|
50
|
+
*/
|
|
51
|
+
user: MotionGPUUserContext;
|
|
52
|
+
/**
|
|
53
|
+
* Marks current frame as invalidated.
|
|
54
|
+
*/
|
|
55
|
+
invalidate: () => void;
|
|
56
|
+
/**
|
|
57
|
+
* Requests one manual frame advance.
|
|
58
|
+
*/
|
|
59
|
+
advance: () => void;
|
|
60
|
+
/**
|
|
61
|
+
* Public scheduler API.
|
|
62
|
+
*/
|
|
63
|
+
scheduler: MotionGPUScheduler;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Registers the motiongpu context in the current Svelte component tree.
|
|
67
|
+
*
|
|
68
|
+
* @param context - Context payload to provide.
|
|
69
|
+
*/
|
|
70
|
+
export declare function provideMotionGPUContext(context: MotionGPUContext): void;
|
|
71
|
+
/**
|
|
72
|
+
* Returns the active motiongpu context.
|
|
73
|
+
*
|
|
74
|
+
* @returns Active context.
|
|
75
|
+
* @throws {Error} When called outside `<FragCanvas>`.
|
|
76
|
+
*/
|
|
77
|
+
export declare function useMotionGPU(): MotionGPUContext;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
/**
|
|
3
|
+
* Svelte context key used to expose `FragCanvas` runtime state.
|
|
4
|
+
*/
|
|
5
|
+
const MOTIONGPU_CONTEXT_KEY = Symbol('motiongpu.context');
|
|
6
|
+
/**
|
|
7
|
+
* Registers the motiongpu context in the current Svelte component tree.
|
|
8
|
+
*
|
|
9
|
+
* @param context - Context payload to provide.
|
|
10
|
+
*/
|
|
11
|
+
export function provideMotionGPUContext(context) {
|
|
12
|
+
setContext(MOTIONGPU_CONTEXT_KEY, context);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Returns the active motiongpu context.
|
|
16
|
+
*
|
|
17
|
+
* @returns Active context.
|
|
18
|
+
* @throws {Error} When called outside `<FragCanvas>`.
|
|
19
|
+
*/
|
|
20
|
+
export function useMotionGPU() {
|
|
21
|
+
const context = getContext(MOTIONGPU_CONTEXT_KEY);
|
|
22
|
+
if (!context) {
|
|
23
|
+
throw new Error('useMotionGPU must be used inside <FragCanvas>');
|
|
24
|
+
}
|
|
25
|
+
return context;
|
|
26
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { RenderPass, RenderPassContext, RenderPassFlags, RenderPassInputSlot, RenderPassOutputSlot } from '../core/types';
|
|
2
|
+
export interface BlitPassOptions extends RenderPassFlags {
|
|
3
|
+
enabled?: boolean;
|
|
4
|
+
needsSwap?: boolean;
|
|
5
|
+
input?: RenderPassInputSlot;
|
|
6
|
+
output?: RenderPassOutputSlot;
|
|
7
|
+
filter?: GPUFilterMode;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Fullscreen texture blit pass.
|
|
11
|
+
*/
|
|
12
|
+
export declare class BlitPass 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
|
+
constructor(options?: BlitPassOptions);
|
|
28
|
+
private ensureResources;
|
|
29
|
+
setSize(width: number, height: number): void;
|
|
30
|
+
render(context: RenderPassContext): void;
|
|
31
|
+
dispose(): void;
|
|
32
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const FULLSCREEN_BLIT_SHADER = `
|
|
2
|
+
struct MotionGPUVertexOut {
|
|
3
|
+
@builtin(position) position: vec4f,
|
|
4
|
+
@location(0) uv: vec2f,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
@group(0) @binding(0) var motiongpuBlitSampler: sampler;
|
|
8
|
+
@group(0) @binding(1) var motiongpuBlitTexture: texture_2d<f32>;
|
|
9
|
+
|
|
10
|
+
@vertex
|
|
11
|
+
fn motiongpuBlitVertex(@builtin(vertex_index) index: u32) -> MotionGPUVertexOut {
|
|
12
|
+
var positions = array<vec2f, 3>(
|
|
13
|
+
vec2f(-1.0, -3.0),
|
|
14
|
+
vec2f(-1.0, 1.0),
|
|
15
|
+
vec2f(3.0, 1.0)
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
let position = positions[index];
|
|
19
|
+
var out: MotionGPUVertexOut;
|
|
20
|
+
out.position = vec4f(position, 0.0, 1.0);
|
|
21
|
+
out.uv = (position + vec2f(1.0, 1.0)) * 0.5;
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@fragment
|
|
26
|
+
fn motiongpuBlitFragment(in: MotionGPUVertexOut) -> @location(0) vec4f {
|
|
27
|
+
return textureSample(motiongpuBlitTexture, motiongpuBlitSampler, in.uv);
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
/**
|
|
31
|
+
* Fullscreen texture blit pass.
|
|
32
|
+
*/
|
|
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
|
+
};
|
|
125
|
+
}
|
|
126
|
+
setSize(width, height) {
|
|
127
|
+
void width;
|
|
128
|
+
void height;
|
|
129
|
+
}
|
|
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();
|
|
149
|
+
}
|
|
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();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RenderPass, RenderPassContext, RenderPassFlags, RenderPassInputSlot, RenderPassOutputSlot } from '../core/types';
|
|
2
|
+
export interface CopyPassOptions extends RenderPassFlags {
|
|
3
|
+
enabled?: boolean;
|
|
4
|
+
needsSwap?: boolean;
|
|
5
|
+
input?: RenderPassInputSlot;
|
|
6
|
+
output?: RenderPassOutputSlot;
|
|
7
|
+
filter?: GPUFilterMode;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Texture copy pass with fullscreen-blit fallback.
|
|
11
|
+
*/
|
|
12
|
+
export declare class CopyPass 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 fallbackBlit;
|
|
21
|
+
constructor(options?: CopyPassOptions);
|
|
22
|
+
setSize(width: number, height: number): void;
|
|
23
|
+
render(context: RenderPassContext): void;
|
|
24
|
+
dispose(): void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { BlitPass } from './BlitPass';
|
|
2
|
+
/**
|
|
3
|
+
* Texture copy pass with fullscreen-blit fallback.
|
|
4
|
+
*/
|
|
5
|
+
export class CopyPass {
|
|
6
|
+
enabled;
|
|
7
|
+
needsSwap;
|
|
8
|
+
input;
|
|
9
|
+
output;
|
|
10
|
+
clear;
|
|
11
|
+
clearColor;
|
|
12
|
+
preserve;
|
|
13
|
+
fallbackBlit;
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.enabled = options.enabled ?? true;
|
|
16
|
+
this.needsSwap = options.needsSwap ?? true;
|
|
17
|
+
this.input = options.input ?? 'source';
|
|
18
|
+
this.output = options.output ?? (this.needsSwap ? 'target' : 'source');
|
|
19
|
+
this.clear = options.clear ?? false;
|
|
20
|
+
this.clearColor = options.clearColor ?? [0, 0, 0, 1];
|
|
21
|
+
this.preserve = options.preserve ?? true;
|
|
22
|
+
this.fallbackBlit = new BlitPass({
|
|
23
|
+
enabled: true,
|
|
24
|
+
needsSwap: false,
|
|
25
|
+
input: this.input,
|
|
26
|
+
output: this.output,
|
|
27
|
+
...(options.filter !== undefined ? { filter: options.filter } : {})
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
setSize(width, height) {
|
|
31
|
+
this.fallbackBlit.setSize(width, height);
|
|
32
|
+
}
|
|
33
|
+
render(context) {
|
|
34
|
+
const source = context.input;
|
|
35
|
+
const target = context.output;
|
|
36
|
+
const canDirectCopy = context.clear === false &&
|
|
37
|
+
context.preserve === true &&
|
|
38
|
+
source.texture !== target.texture &&
|
|
39
|
+
source.texture !== context.canvas.texture &&
|
|
40
|
+
target.texture !== context.canvas.texture &&
|
|
41
|
+
source.width === target.width &&
|
|
42
|
+
source.height === target.height &&
|
|
43
|
+
source.format === target.format;
|
|
44
|
+
if (canDirectCopy) {
|
|
45
|
+
context.commandEncoder.copyTextureToTexture({ texture: source.texture }, { texture: target.texture }, { width: source.width, height: source.height, depthOrArrayLayers: 1 });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.fallbackBlit.render(context);
|
|
49
|
+
}
|
|
50
|
+
dispose() {
|
|
51
|
+
this.fallbackBlit.dispose();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { RenderPass, RenderPassContext, RenderPassFlags, RenderPassInputSlot, RenderPassOutputSlot } from '../core/types';
|
|
2
|
+
export interface ShaderPassOptions extends RenderPassFlags {
|
|
3
|
+
fragment: string;
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
needsSwap?: boolean;
|
|
6
|
+
input?: RenderPassInputSlot;
|
|
7
|
+
output?: RenderPassOutputSlot;
|
|
8
|
+
filter?: GPUFilterMode;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Fullscreen programmable shader pass.
|
|
12
|
+
*/
|
|
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;
|
|
22
|
+
private fragment;
|
|
23
|
+
private program;
|
|
24
|
+
private device;
|
|
25
|
+
private sampler;
|
|
26
|
+
private bindGroupLayout;
|
|
27
|
+
private shaderModule;
|
|
28
|
+
private readonly pipelineByFormat;
|
|
29
|
+
private bindGroupByView;
|
|
30
|
+
constructor(options: ShaderPassOptions);
|
|
31
|
+
/**
|
|
32
|
+
* Replaces current shader fragment and invalidates pipeline cache.
|
|
33
|
+
*/
|
|
34
|
+
setFragment(fragment: string): void;
|
|
35
|
+
getFragment(): string;
|
|
36
|
+
private ensureResources;
|
|
37
|
+
setSize(width: number, height: number): void;
|
|
38
|
+
render(context: RenderPassContext): void;
|
|
39
|
+
dispose(): void;
|
|
40
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
const SHADER_PASS_CONTRACT = /\bfn\s+shade\s*\(\s*inputColor\s*:\s*vec4f\s*,\s*uv\s*:\s*vec2f\s*\)\s*->\s*vec4f/;
|
|
2
|
+
function buildShaderPassProgram(fragment) {
|
|
3
|
+
if (!SHADER_PASS_CONTRACT.test(fragment)) {
|
|
4
|
+
throw new Error('ShaderPass fragment must declare `fn shade(inputColor: vec4f, uv: vec2f) -> vec4f`.');
|
|
5
|
+
}
|
|
6
|
+
return `
|
|
7
|
+
struct MotionGPUVertexOut {
|
|
8
|
+
@builtin(position) position: vec4f,
|
|
9
|
+
@location(0) uv: vec2f,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
@group(0) @binding(0) var motiongpuShaderPassSampler: sampler;
|
|
13
|
+
@group(0) @binding(1) var motiongpuShaderPassTexture: texture_2d<f32>;
|
|
14
|
+
|
|
15
|
+
@vertex
|
|
16
|
+
fn motiongpuShaderPassVertex(@builtin(vertex_index) index: u32) -> MotionGPUVertexOut {
|
|
17
|
+
var positions = array<vec2f, 3>(
|
|
18
|
+
vec2f(-1.0, -3.0),
|
|
19
|
+
vec2f(-1.0, 1.0),
|
|
20
|
+
vec2f(3.0, 1.0)
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
let position = positions[index];
|
|
24
|
+
var out: MotionGPUVertexOut;
|
|
25
|
+
out.position = vec4f(position, 0.0, 1.0);
|
|
26
|
+
out.uv = (position + vec2f(1.0, 1.0)) * 0.5;
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
${fragment}
|
|
31
|
+
|
|
32
|
+
@fragment
|
|
33
|
+
fn motiongpuShaderPassFragment(in: MotionGPUVertexOut) -> @location(0) vec4f {
|
|
34
|
+
let inputColor = textureSample(motiongpuShaderPassTexture, motiongpuShaderPassSampler, in.uv);
|
|
35
|
+
return shade(inputColor, in.uv);
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Fullscreen programmable shader pass.
|
|
41
|
+
*/
|
|
42
|
+
export class ShaderPass {
|
|
43
|
+
enabled;
|
|
44
|
+
needsSwap;
|
|
45
|
+
input;
|
|
46
|
+
output;
|
|
47
|
+
clear;
|
|
48
|
+
clearColor;
|
|
49
|
+
preserve;
|
|
50
|
+
filter;
|
|
51
|
+
fragment;
|
|
52
|
+
program;
|
|
53
|
+
device = null;
|
|
54
|
+
sampler = null;
|
|
55
|
+
bindGroupLayout = null;
|
|
56
|
+
shaderModule = null;
|
|
57
|
+
pipelineByFormat = new Map();
|
|
58
|
+
bindGroupByView = new WeakMap();
|
|
59
|
+
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';
|
|
68
|
+
this.fragment = options.fragment;
|
|
69
|
+
this.program = buildShaderPassProgram(options.fragment);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Replaces current shader fragment and invalidates pipeline cache.
|
|
73
|
+
*/
|
|
74
|
+
setFragment(fragment) {
|
|
75
|
+
this.fragment = fragment;
|
|
76
|
+
this.program = buildShaderPassProgram(fragment);
|
|
77
|
+
this.shaderModule = null;
|
|
78
|
+
this.pipelineByFormat.clear();
|
|
79
|
+
this.bindGroupByView = new WeakMap();
|
|
80
|
+
}
|
|
81
|
+
getFragment() {
|
|
82
|
+
return this.fragment;
|
|
83
|
+
}
|
|
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
|
+
};
|
|
149
|
+
}
|
|
150
|
+
setSize(width, height) {
|
|
151
|
+
void width;
|
|
152
|
+
void height;
|
|
153
|
+
}
|
|
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();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CurrentReadable } from './current-writable';
|
|
2
|
+
import { type MotionGPUUserNamespace } from './motiongpu-context';
|
|
3
|
+
/**
|
|
4
|
+
* Internal shape of the user context store.
|
|
5
|
+
*/
|
|
6
|
+
type UserContextStore = Record<MotionGPUUserNamespace, unknown>;
|
|
7
|
+
/**
|
|
8
|
+
* Controls how a namespaced user context value behaves when already present.
|
|
9
|
+
*/
|
|
10
|
+
export interface SetMotionGPUUserContextOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Conflict strategy when namespace already exists:
|
|
13
|
+
* - `skip`: keep current value
|
|
14
|
+
* - `replace`: replace current value
|
|
15
|
+
* - `merge`: shallow merge object values, fallback to replace otherwise
|
|
16
|
+
*
|
|
17
|
+
* @default 'skip'
|
|
18
|
+
*/
|
|
19
|
+
existing?: 'merge' | 'replace' | 'skip';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Returns a read-only view of the entire motiongpu user context store.
|
|
23
|
+
*/
|
|
24
|
+
export declare function useMotionGPUUserContext<UC extends UserContextStore = UserContextStore>(): CurrentReadable<UC>;
|
|
25
|
+
/**
|
|
26
|
+
* Reads a namespaced user context value as a reactive readable store.
|
|
27
|
+
*/
|
|
28
|
+
export declare function useMotionGPUUserContext<UCT = unknown>(namespace: MotionGPUUserNamespace): CurrentReadable<UCT | undefined>;
|
|
29
|
+
/**
|
|
30
|
+
* Sets a namespaced user context value with explicit write semantics.
|
|
31
|
+
*
|
|
32
|
+
* Returns the effective value stored under the namespace.
|
|
33
|
+
*/
|
|
34
|
+
export declare function setMotionGPUUserContext<UCT = unknown>(namespace: MotionGPUUserNamespace, value: UCT | (() => UCT), options?: SetMotionGPUUserContextOptions): UCT | undefined;
|
|
35
|
+
export {};
|