>;
export type EffectFactory = {} extends P
? (params?: P & {readonly disabled?: boolean}) => EffectDescriptor
: (params: P & {readonly disabled?: boolean}) => EffectDescriptor;
+
+export type EffectsOutputSize = {
+ readonly width: number;
+ readonly height: number;
+};
diff --git a/packages/core/src/effects/index.ts b/packages/core/src/effects/index.ts
index f8a2cf5fdc4..e1f7440cf1a 100644
--- a/packages/core/src/effects/index.ts
+++ b/packages/core/src/effects/index.ts
@@ -7,6 +7,7 @@ export type {
EffectDefinitionAndStack,
EffectDescriptor,
EffectsProp,
+ EffectsOutputSize,
EffectDefinition,
EffectFactory,
} from './effect-types.js';
diff --git a/packages/core/src/effects/run-effect-chain.ts b/packages/core/src/effects/run-effect-chain.ts
index 9970352b972..4253ac90e69 100644
--- a/packages/core/src/effects/run-effect-chain.ts
+++ b/packages/core/src/effects/run-effect-chain.ts
@@ -7,6 +7,8 @@ import type {
import {getGpuDevice} from './gpu-device.js';
export type EffectChainState = {
+ readonly width: number;
+ readonly height: number;
pool: CanvasPool;
setupCache: WeakMap<
EffectDefinition,
@@ -25,6 +27,8 @@ export const createEffectChainState = (
height: number,
): EffectChainState => {
const state: EffectChainState = {
+ width,
+ height,
pool: new CanvasPool(width, height, (canvas) => {
// The GL objects these setups hold died with the context and stay
// invalid on a restored one, so the next run has to recreate them.
@@ -104,14 +108,10 @@ export type RunEffectChainOptions = {
// Runs the effect pipeline imperatively. Returns `true` if the pipeline
// completed and wrote to `output`, `false` if it was superseded by a newer
// run (caller should not act on a stale result).
-export const runEffectChain = async ({
- state,
- source,
- effects,
- output,
- width,
- height,
-}: RunEffectChainOptions): Promise => {
+const runEffectChainWithoutResize = async (
+ {state, source, effects, output, width, height}: RunEffectChainOptions,
+ initialFlipSourceY: boolean,
+): Promise => {
const runId = ++state.currentRunId;
const isCancelled = () => state.currentRunId !== runId;
@@ -160,7 +160,7 @@ export const runEffectChain = async ({
// Canvas sources are DOM-oriented. Flip them when uploading into WebGL so
// texture coordinates match clip-space output. `ImageBitmap` bridges below
// opt out because they are already oriented for upload.
- let flipWebGLSourceY = true;
+ let flipWebGLSourceY = initialFlipSourceY;
for (let runIndex = 0; runIndex < runs.length; runIndex++) {
const run = runs[runIndex];
@@ -182,8 +182,8 @@ export const runEffectChain = async ({
target: dst,
state: setupState,
params: eff.params,
- width,
- height,
+ width: state.width,
+ height: state.height,
gpuDevice,
flipSourceY: run.backend === 'webgl2' ? flipWebGLSourceY : false,
});
@@ -242,3 +242,40 @@ export const runEffectChain = async ({
outCtx.drawImage(lastTarget, 0, 0, width, height);
return true;
};
+
+// Keep ownership of the optional resized source until the existing chain settles.
+export const runEffectChain = async (
+ options: RunEffectChainOptions,
+): Promise => {
+ const {state, source, width, height, effects} = options;
+ if (
+ (state.width === width && state.height === height) ||
+ !effects.some((effect) => !(effect.params as {disabled?: boolean}).disabled)
+ ) {
+ return runEffectChainWithoutResize(options, true);
+ }
+
+ const runId = ++state.currentRunId;
+ const firstEnabledEffect = effects.find(
+ (effect) => !(effect.params as {disabled?: boolean}).disabled,
+ )!;
+ const resized = await createImageBitmap(source as ImageBitmapSource, {
+ resizeWidth: state.width,
+ resizeHeight: state.height,
+ // WebGL ignores UNPACK_FLIP_Y_WEBGL for ImageBitmap uploads.
+ imageOrientation:
+ firstEnabledEffect.definition.backend === 'webgl2' ? 'flipY' : 'none',
+ });
+ try {
+ if (state.currentRunId !== runId) {
+ return false;
+ }
+
+ return await runEffectChainWithoutResize(
+ {...options, source: resized},
+ false,
+ );
+ } finally {
+ resized.close();
+ }
+};
diff --git a/packages/core/src/effects/use-effect-chain-state.ts b/packages/core/src/effects/use-effect-chain-state.ts
index 22d7309bbb8..298c6659342 100644
--- a/packages/core/src/effects/use-effect-chain-state.ts
+++ b/packages/core/src/effects/use-effect-chain-state.ts
@@ -1,13 +1,32 @@
import {useEffect, useMemo, useRef} from 'react';
+import {validateDimension} from '../validation/validate-dimensions.js';
+import type {EffectsOutputSize} from './effect-types.js';
import type {EffectChainState} from './run-effect-chain.js';
import {
cleanupEffectChainState,
createEffectChainState,
} from './run-effect-chain.js';
-export const useEffectChainState = (): {
+export const useEffectChainState = (
+ effectsOutputSize: EffectsOutputSize | null = null,
+): {
get: (width: number, height: number) => EffectChainState | null;
} => {
+ const effectWidth = effectsOutputSize?.width;
+ const effectHeight = effectsOutputSize?.height;
+ if (effectsOutputSize) {
+ validateDimension(
+ effectWidth,
+ 'effectsOutputSize.width',
+ 'on an effect-capable component',
+ );
+ validateDimension(
+ effectHeight,
+ 'effectsOutputSize.height',
+ 'on an effect-capable component',
+ );
+ }
+
const chainStateRef = useRef(null);
const sizeRef = useRef<{width: number; height: number} | null>(null);
const unmountedRef = useRef(false);
@@ -30,6 +49,8 @@ export const useEffectChainState = (): {
return useMemo(
() => ({
get: (width, height) => {
+ width = effectWidth ?? width;
+ height = effectHeight ?? height;
// Async work such as a late decoded video frame can still get here
// after unmount. Nothing would release a pool created at this point.
if (unmountedRef.current) {
@@ -52,6 +73,6 @@ export const useEffectChainState = (): {
return chainStateRef.current;
},
}),
- [],
+ [effectWidth, effectHeight],
);
};
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 19aab89a8d1..ba1618585e2 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -157,6 +157,7 @@ export {
type EffectDescriptor,
type EffectFactory,
type EffectsProp,
+ type EffectsOutputSize,
} from './effects/index.js';
/**
* @description Renders a solid-color rectangle on a `