@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.
- package/README.md +78 -1
- package/dist/core/current-value.js +3 -0
- package/dist/core/error-diagnostics.d.ts +14 -0
- package/dist/core/error-diagnostics.js +41 -1
- package/dist/core/error-report.d.ts +37 -0
- package/dist/core/error-report.js +60 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/material-preprocess.d.ts +5 -5
- package/dist/core/material-preprocess.js +1 -4
- package/dist/core/material.d.ts +32 -23
- package/dist/core/material.js +14 -7
- package/dist/core/renderer.d.ts +10 -0
- package/dist/core/renderer.js +66 -4
- package/dist/core/runtime-loop.d.ts +3 -0
- package/dist/core/runtime-loop.js +72 -1
- package/dist/core/types.d.ts +24 -10
- package/dist/passes/BlitPass.d.ts +6 -27
- package/dist/passes/BlitPass.js +10 -121
- package/dist/passes/FullscreenPass.d.ts +37 -0
- package/dist/passes/FullscreenPass.js +131 -0
- package/dist/passes/ShaderPass.d.ts +6 -26
- package/dist/passes/ShaderPass.js +10 -121
- package/dist/react/FragCanvas.d.ts +26 -0
- package/dist/react/FragCanvas.js +218 -0
- package/dist/react/FragCanvas.tsx +345 -0
- package/dist/react/MotionGPUErrorOverlay.d.ts +6 -0
- package/dist/react/MotionGPUErrorOverlay.js +52 -0
- package/dist/react/MotionGPUErrorOverlay.tsx +129 -0
- package/dist/react/Portal.d.ts +6 -0
- package/dist/react/Portal.js +24 -0
- package/dist/react/Portal.tsx +34 -0
- package/dist/react/advanced.d.ts +11 -0
- package/dist/react/advanced.js +6 -0
- package/dist/react/frame-context.d.ts +14 -0
- package/dist/react/frame-context.js +98 -0
- package/dist/react/index.d.ts +15 -0
- package/dist/react/index.js +9 -0
- package/dist/react/motiongpu-context.d.ts +73 -0
- package/dist/react/motiongpu-context.js +18 -0
- package/dist/react/use-motiongpu-user-context.d.ts +49 -0
- package/dist/react/use-motiongpu-user-context.js +94 -0
- package/dist/react/use-texture.d.ts +40 -0
- package/dist/react/use-texture.js +162 -0
- package/dist/svelte/FragCanvas.svelte +45 -16
- package/dist/svelte/FragCanvas.svelte.d.ts +2 -0
- package/dist/svelte/MotionGPUErrorOverlay.svelte +10 -19
- package/dist/svelte/Portal.svelte +6 -21
- package/dist/svelte/use-motiongpu-user-context.d.ts +9 -1
- package/dist/svelte/use-motiongpu-user-context.js +4 -1
- package/dist/svelte/use-texture.d.ts +11 -2
- package/dist/svelte/use-texture.js +13 -3
- package/package.json +28 -3
package/dist/core/renderer.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import type { Renderer, RendererOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Computes dirty float ranges between two uniform snapshots.
|
|
4
|
+
*
|
|
5
|
+
* Adjacent dirty ranges separated by a gap smaller than or equal to
|
|
6
|
+
* {@link DIRTY_RANGE_MERGE_GAP} are merged to reduce `writeBuffer` calls.
|
|
7
|
+
*/
|
|
8
|
+
export declare function findDirtyFloatRanges(previous: Float32Array, next: Float32Array, mergeGapThreshold?: number): Array<{
|
|
9
|
+
start: number;
|
|
10
|
+
count: number;
|
|
11
|
+
}>;
|
|
2
12
|
/**
|
|
3
13
|
* Creates the WebGPU renderer used by `FragCanvas`.
|
|
4
14
|
*
|
package/dist/core/renderer.js
CHANGED
|
@@ -78,9 +78,45 @@ async function assertCompilation(module, options) {
|
|
|
78
78
|
...(options?.defineBlockSource !== undefined
|
|
79
79
|
? { defineBlockSource: options.defineBlockSource }
|
|
80
80
|
: {}),
|
|
81
|
-
materialSource: options?.materialSource ?? null
|
|
81
|
+
materialSource: options?.materialSource ?? null,
|
|
82
|
+
...(options?.runtimeContext !== undefined ? { runtimeContext: options.runtimeContext } : {})
|
|
82
83
|
});
|
|
83
84
|
}
|
|
85
|
+
function toSortedUniqueStrings(values) {
|
|
86
|
+
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
87
|
+
}
|
|
88
|
+
function buildPassGraphSnapshot(passes) {
|
|
89
|
+
const declaredPasses = passes ?? [];
|
|
90
|
+
let enabledPassCount = 0;
|
|
91
|
+
const inputs = [];
|
|
92
|
+
const outputs = [];
|
|
93
|
+
for (const pass of declaredPasses) {
|
|
94
|
+
if (pass.enabled === false) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
enabledPassCount += 1;
|
|
98
|
+
const needsSwap = pass.needsSwap ?? true;
|
|
99
|
+
const input = pass.input ?? 'source';
|
|
100
|
+
const output = pass.output ?? (needsSwap ? 'target' : 'source');
|
|
101
|
+
inputs.push(input);
|
|
102
|
+
outputs.push(output);
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
passCount: declaredPasses.length,
|
|
106
|
+
enabledPassCount,
|
|
107
|
+
inputs: toSortedUniqueStrings(inputs),
|
|
108
|
+
outputs: toSortedUniqueStrings(outputs)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function buildShaderCompilationRuntimeContext(options) {
|
|
112
|
+
const passList = options.getPasses?.() ?? options.passes;
|
|
113
|
+
const renderTargetMap = options.getRenderTargets?.() ?? options.renderTargets;
|
|
114
|
+
return {
|
|
115
|
+
...(options.materialSignature ? { materialSignature: options.materialSignature } : {}),
|
|
116
|
+
passGraph: buildPassGraphSnapshot(passList),
|
|
117
|
+
activeRenderTargets: Object.keys(renderTargetMap ?? {}).sort((a, b) => a.localeCompare(b))
|
|
118
|
+
};
|
|
119
|
+
}
|
|
84
120
|
/**
|
|
85
121
|
* Creates a 1x1 white fallback texture used before user textures become available.
|
|
86
122
|
*/
|
|
@@ -182,10 +218,19 @@ function createBindGroupLayoutEntries(textureBindings) {
|
|
|
182
218
|
}
|
|
183
219
|
return entries;
|
|
184
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Maximum gap (in floats) between two dirty ranges that triggers merge.
|
|
223
|
+
*
|
|
224
|
+
* Set to 4 (16 bytes) which covers one vec4f alignment slot.
|
|
225
|
+
*/
|
|
226
|
+
const DIRTY_RANGE_MERGE_GAP = 4;
|
|
185
227
|
/**
|
|
186
228
|
* Computes dirty float ranges between two uniform snapshots.
|
|
229
|
+
*
|
|
230
|
+
* Adjacent dirty ranges separated by a gap smaller than or equal to
|
|
231
|
+
* {@link DIRTY_RANGE_MERGE_GAP} are merged to reduce `writeBuffer` calls.
|
|
187
232
|
*/
|
|
188
|
-
function findDirtyFloatRanges(previous, next) {
|
|
233
|
+
export function findDirtyFloatRanges(previous, next, mergeGapThreshold = DIRTY_RANGE_MERGE_GAP) {
|
|
189
234
|
const ranges = [];
|
|
190
235
|
let start = -1;
|
|
191
236
|
for (let index = 0; index < next.length; index += 1) {
|
|
@@ -203,7 +248,22 @@ function findDirtyFloatRanges(previous, next) {
|
|
|
203
248
|
if (start !== -1) {
|
|
204
249
|
ranges.push({ start, count: next.length - start });
|
|
205
250
|
}
|
|
206
|
-
|
|
251
|
+
if (ranges.length <= 1) {
|
|
252
|
+
return ranges;
|
|
253
|
+
}
|
|
254
|
+
const merged = [ranges[0]];
|
|
255
|
+
for (let index = 1; index < ranges.length; index += 1) {
|
|
256
|
+
const prev = merged[merged.length - 1];
|
|
257
|
+
const curr = ranges[index];
|
|
258
|
+
const gap = curr.start - (prev.start + prev.count);
|
|
259
|
+
if (gap <= mergeGapThreshold) {
|
|
260
|
+
prev.count = curr.start + curr.count - prev.start;
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
merged.push(curr);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return merged;
|
|
207
267
|
}
|
|
208
268
|
/**
|
|
209
269
|
* Determines whether shader output should perform linear-to-sRGB conversion.
|
|
@@ -339,6 +399,7 @@ export async function createRenderer(options) {
|
|
|
339
399
|
};
|
|
340
400
|
device.addEventListener('uncapturederror', handleUncapturedError);
|
|
341
401
|
try {
|
|
402
|
+
const runtimeContext = buildShaderCompilationRuntimeContext(options);
|
|
342
403
|
const convertLinearToSrgb = shouldConvertLinearToSrgb(options.outputColorSpace, format);
|
|
343
404
|
const builtShader = buildShaderSourceWithMap(options.fragmentWgsl, options.uniformLayout, options.textureKeys, {
|
|
344
405
|
convertLinearToSrgb,
|
|
@@ -352,7 +413,8 @@ export async function createRenderer(options) {
|
|
|
352
413
|
...(options.defineBlockSource !== undefined
|
|
353
414
|
? { defineBlockSource: options.defineBlockSource }
|
|
354
415
|
: {}),
|
|
355
|
-
materialSource: options.materialSource ?? null
|
|
416
|
+
materialSource: options.materialSource ?? null,
|
|
417
|
+
runtimeContext
|
|
356
418
|
});
|
|
357
419
|
const normalizedTextureDefinitions = normalizeTextureDefinitions(options.textureDefinitions, options.textureKeys);
|
|
358
420
|
const textureBindings = options.textureKeys.map((key, index) => {
|
|
@@ -21,6 +21,9 @@ export interface MotionGPURuntimeLoopOptions {
|
|
|
21
21
|
getDeviceDescriptor: () => GPUDeviceDescriptor | undefined;
|
|
22
22
|
getOnError: () => ((report: MotionGPUErrorReport) => void) | undefined;
|
|
23
23
|
reportError: (report: MotionGPUErrorReport | null) => void;
|
|
24
|
+
getErrorHistoryLimit?: () => number | undefined;
|
|
25
|
+
getOnErrorHistory?: () => ((history: MotionGPUErrorReport[]) => void) | undefined;
|
|
26
|
+
reportErrorHistory?: (history: MotionGPUErrorReport[]) => void;
|
|
24
27
|
}
|
|
25
28
|
export interface MotionGPURuntimeLoop {
|
|
26
29
|
requestFrame: () => void;
|
|
@@ -33,12 +33,81 @@ export function createMotionGPURuntimeLoop(options) {
|
|
|
33
33
|
const renderTextures = {};
|
|
34
34
|
const canvasSize = { width: 0, height: 0 };
|
|
35
35
|
let shouldContinueAfterFrame = false;
|
|
36
|
+
let activeErrorKey = null;
|
|
37
|
+
let errorHistory = [];
|
|
38
|
+
const getHistoryLimit = () => {
|
|
39
|
+
const value = options.getErrorHistoryLimit?.() ?? 0;
|
|
40
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
return Math.floor(value);
|
|
44
|
+
};
|
|
45
|
+
const publishErrorHistory = () => {
|
|
46
|
+
options.reportErrorHistory?.(errorHistory);
|
|
47
|
+
const onErrorHistory = options.getOnErrorHistory?.();
|
|
48
|
+
if (!onErrorHistory) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
onErrorHistory(errorHistory);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// User-provided error history handlers must not break runtime error recovery.
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const syncErrorHistory = () => {
|
|
59
|
+
const limit = getHistoryLimit();
|
|
60
|
+
if (limit <= 0) {
|
|
61
|
+
if (errorHistory.length === 0) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
errorHistory = [];
|
|
65
|
+
publishErrorHistory();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (errorHistory.length <= limit) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
errorHistory = errorHistory.slice(errorHistory.length - limit);
|
|
72
|
+
publishErrorHistory();
|
|
73
|
+
};
|
|
36
74
|
const setError = (error, phase) => {
|
|
37
75
|
const report = toMotionGPUErrorReport(error, phase);
|
|
76
|
+
const reportKey = JSON.stringify({
|
|
77
|
+
phase: report.phase,
|
|
78
|
+
title: report.title,
|
|
79
|
+
message: report.message,
|
|
80
|
+
rawMessage: report.rawMessage
|
|
81
|
+
});
|
|
82
|
+
if (activeErrorKey === reportKey) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
activeErrorKey = reportKey;
|
|
86
|
+
const historyLimit = getHistoryLimit();
|
|
87
|
+
if (historyLimit > 0) {
|
|
88
|
+
errorHistory = [...errorHistory, report];
|
|
89
|
+
if (errorHistory.length > historyLimit) {
|
|
90
|
+
errorHistory = errorHistory.slice(errorHistory.length - historyLimit);
|
|
91
|
+
}
|
|
92
|
+
publishErrorHistory();
|
|
93
|
+
}
|
|
38
94
|
options.reportError(report);
|
|
39
|
-
options.getOnError()
|
|
95
|
+
const onError = options.getOnError();
|
|
96
|
+
if (!onError) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
onError(report);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// User-provided error handlers must not break runtime error recovery.
|
|
104
|
+
}
|
|
40
105
|
};
|
|
41
106
|
const clearError = () => {
|
|
107
|
+
if (activeErrorKey === null) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
activeErrorKey = null;
|
|
42
111
|
options.reportError(null);
|
|
43
112
|
};
|
|
44
113
|
const scheduleFrame = () => {
|
|
@@ -127,6 +196,7 @@ export function createMotionGPURuntimeLoop(options) {
|
|
|
127
196
|
if (isDisposed) {
|
|
128
197
|
return;
|
|
129
198
|
}
|
|
199
|
+
syncErrorHistory();
|
|
130
200
|
let materialState;
|
|
131
201
|
try {
|
|
132
202
|
materialState = resolveActiveMaterial();
|
|
@@ -165,6 +235,7 @@ export function createMotionGPURuntimeLoop(options) {
|
|
|
165
235
|
includeSources: materialState.includeSources,
|
|
166
236
|
defineBlockSource: materialState.defineBlockSource,
|
|
167
237
|
materialSource: materialState.source,
|
|
238
|
+
materialSignature: materialState.signature,
|
|
168
239
|
uniformLayout: materialState.uniformLayout,
|
|
169
240
|
textureKeys: materialState.textureKeys,
|
|
170
241
|
textureDefinitions: materialState.textures,
|
package/dist/core/types.d.ts
CHANGED
|
@@ -11,7 +11,21 @@ export type UniformType = 'f32' | 'vec2f' | 'vec3f' | 'vec4f' | 'mat4x4f';
|
|
|
11
11
|
* @typeParam TType - WGSL type tag.
|
|
12
12
|
* @typeParam TValue - Runtime value shape for the selected type.
|
|
13
13
|
*/
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Accepted matrix value formats for `mat4x4f` uniforms.
|
|
16
|
+
*/
|
|
17
|
+
export type UniformMat4Value = number[] | Float32Array;
|
|
18
|
+
/**
|
|
19
|
+
* Runtime value shape by WGSL uniform type tag.
|
|
20
|
+
*/
|
|
21
|
+
export interface UniformValueByType {
|
|
22
|
+
f32: number;
|
|
23
|
+
vec2f: [number, number];
|
|
24
|
+
vec3f: [number, number, number];
|
|
25
|
+
vec4f: [number, number, number, number];
|
|
26
|
+
mat4x4f: UniformMat4Value;
|
|
27
|
+
}
|
|
28
|
+
export interface TypedUniform<TType extends UniformType = UniformType, TValue extends UniformValueByType[TType] = UniformValueByType[TType]> {
|
|
15
29
|
/**
|
|
16
30
|
* WGSL type tag.
|
|
17
31
|
*/
|
|
@@ -21,18 +35,14 @@ export interface TypedUniform<TType extends UniformType = UniformType, TValue =
|
|
|
21
35
|
*/
|
|
22
36
|
value: TValue;
|
|
23
37
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Accepted matrix value formats for `mat4x4f` uniforms.
|
|
26
|
-
*/
|
|
27
|
-
export type UniformMat4Value = number[] | Float32Array;
|
|
28
38
|
/**
|
|
29
39
|
* Supported uniform input shapes accepted by material and render APIs.
|
|
30
40
|
*/
|
|
31
|
-
export type UniformValue = number | [number, number] | [number, number, number] | [number, number, number, number] | TypedUniform<'f32'
|
|
41
|
+
export type UniformValue = number | [number, number] | [number, number, number] | [number, number, number, number] | TypedUniform<'f32'> | TypedUniform<'vec2f'> | TypedUniform<'vec3f'> | TypedUniform<'vec4f'> | TypedUniform<'mat4x4f'>;
|
|
32
42
|
/**
|
|
33
43
|
* Uniform map keyed by WGSL identifier names.
|
|
34
44
|
*/
|
|
35
|
-
export type UniformMap = Record<
|
|
45
|
+
export type UniformMap<TKey extends string = string> = Record<TKey, UniformValue>;
|
|
36
46
|
/**
|
|
37
47
|
* Resolved layout metadata for a single uniform field inside the packed uniform buffer.
|
|
38
48
|
*/
|
|
@@ -168,11 +178,11 @@ export interface TextureDefinition {
|
|
|
168
178
|
/**
|
|
169
179
|
* Texture definition map keyed by uniform-compatible texture names.
|
|
170
180
|
*/
|
|
171
|
-
export type TextureDefinitionMap = Record<
|
|
181
|
+
export type TextureDefinitionMap<TKey extends string = string> = Record<TKey, TextureDefinition>;
|
|
172
182
|
/**
|
|
173
183
|
* Runtime texture value map keyed by texture uniform names.
|
|
174
184
|
*/
|
|
175
|
-
export type TextureMap = Record<
|
|
185
|
+
export type TextureMap<TKey extends string = string> = Record<TKey, TextureValue>;
|
|
176
186
|
/**
|
|
177
187
|
* Output color space requested for final canvas presentation.
|
|
178
188
|
*/
|
|
@@ -226,7 +236,7 @@ export interface RenderTarget {
|
|
|
226
236
|
/**
|
|
227
237
|
* Named render target definitions keyed by output slot names.
|
|
228
238
|
*/
|
|
229
|
-
export type RenderTargetDefinitionMap = Record<
|
|
239
|
+
export type RenderTargetDefinitionMap<TKey extends string = string> = Record<TKey, RenderTargetDefinition>;
|
|
230
240
|
/**
|
|
231
241
|
* User-defined render slot name (mapped to `renderTargets` keys).
|
|
232
242
|
*/
|
|
@@ -443,6 +453,10 @@ export interface RendererOptions {
|
|
|
443
453
|
column?: number;
|
|
444
454
|
functionName?: string;
|
|
445
455
|
} | null;
|
|
456
|
+
/**
|
|
457
|
+
* Stable material signature captured during resolution.
|
|
458
|
+
*/
|
|
459
|
+
materialSignature?: string;
|
|
446
460
|
/**
|
|
447
461
|
* Resolved uniform layout.
|
|
448
462
|
*/
|
|
@@ -1,32 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
export
|
|
3
|
-
enabled?: boolean;
|
|
4
|
-
needsSwap?: boolean;
|
|
5
|
-
input?: RenderPassInputSlot;
|
|
6
|
-
output?: RenderPassOutputSlot;
|
|
7
|
-
filter?: GPUFilterMode;
|
|
8
|
-
}
|
|
1
|
+
import { FullscreenPass, type FullscreenPassOptions } from './FullscreenPass.js';
|
|
2
|
+
export type BlitPassOptions = FullscreenPassOptions;
|
|
9
3
|
/**
|
|
10
4
|
* Fullscreen texture blit pass.
|
|
11
5
|
*/
|
|
12
|
-
export declare class BlitPass
|
|
13
|
-
|
|
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;
|
|
6
|
+
export declare class BlitPass extends FullscreenPass {
|
|
7
|
+
protected getProgram(): string;
|
|
27
8
|
constructor(options?: BlitPassOptions);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
render(context: RenderPassContext): void;
|
|
31
|
-
dispose(): void;
|
|
9
|
+
protected getVertexEntryPoint(): string;
|
|
10
|
+
protected getFragmentEntryPoint(): string;
|
|
32
11
|
}
|
package/dist/passes/BlitPass.js
CHANGED
|
@@ -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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
void height;
|
|
38
|
+
constructor(options = {}) {
|
|
39
|
+
super(options);
|
|
129
40
|
}
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
+
}
|