@pexip/media-processor 19.1.0 → 20.0.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 (69) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/common/backends/webgl/blur.js +7 -5
  3. package/dist/common/backends/webgl/renderer.d.ts +1 -1
  4. package/dist/common/backends/webgl/renderer.js +27 -34
  5. package/dist/common/backends/webgl/smoothingMask.d.ts +1 -1
  6. package/dist/common/backends/webgl/webglUtils.d.ts +1 -1
  7. package/dist/common/backends/webgl/webglUtils.js +9 -10
  8. package/dist/common/backends/webgpu/blender.d.ts +8 -0
  9. package/dist/common/backends/webgpu/blender.js +78 -0
  10. package/dist/common/backends/webgpu/constants.d.ts +1 -0
  11. package/dist/common/backends/webgpu/constants.js +1 -0
  12. package/dist/common/backends/webgpu/dualFilterBlur.d.ts +5 -0
  13. package/dist/common/backends/webgpu/dualFilterBlur.js +201 -0
  14. package/dist/common/backends/webgpu/renderer.d.ts +2 -0
  15. package/dist/common/backends/webgpu/renderer.js +127 -0
  16. package/dist/common/backends/webgpu/smoothingMask.d.ts +5 -0
  17. package/dist/common/backends/webgpu/smoothingMask.js +66 -0
  18. package/dist/common/backends/webgpu/texture.d.ts +5 -0
  19. package/dist/common/backends/webgpu/texture.js +35 -0
  20. package/dist/common/backends/webgpu/textureToTexture.d.ts +4 -0
  21. package/dist/common/backends/webgpu/textureToTexture.js +35 -0
  22. package/dist/common/backends/webgpu/types.d.ts +5 -0
  23. package/dist/common/backends/webgpu/types.js +1 -0
  24. package/dist/common/backends/webgpu/webgpuUtils.d.ts +7 -0
  25. package/dist/common/backends/webgpu/webgpuUtils.js +14 -0
  26. package/dist/common/canvasRenderUtils.d.ts +1 -1
  27. package/dist/common/canvasRenderUtils.js +8 -10
  28. package/dist/common/constants.d.ts +5 -0
  29. package/dist/common/constants.js +4 -0
  30. package/dist/common/index.d.ts +0 -1
  31. package/dist/common/index.js +0 -1
  32. package/dist/common/m3.d.ts +14 -8
  33. package/dist/common/m3.js +0 -1
  34. package/dist/common/tsconfig.tsbuildinfo +1 -1
  35. package/dist/common/typeGuards.d.ts +2 -0
  36. package/dist/common/typeGuards.js +7 -0
  37. package/dist/common/types/render.d.ts +7 -4
  38. package/dist/common/types/segmentation.d.ts +5 -1
  39. package/dist/common/utils.d.ts +4 -1
  40. package/dist/common/utils.js +36 -1
  41. package/dist/main/audio.d.ts +1 -1
  42. package/dist/main/audio.js +7 -5
  43. package/dist/main/math.js +1 -1
  44. package/dist/main/process.d.ts +3 -3
  45. package/dist/main/process.js +2 -2
  46. package/dist/main/processor.d.ts +0 -1
  47. package/dist/main/transformer.d.ts +1 -1
  48. package/dist/main/tsconfig.tsbuildinfo +1 -1
  49. package/dist/main/typeGuards.d.ts +1 -1
  50. package/dist/main/types.d.ts +2 -1
  51. package/dist/main/utils.js +6 -6
  52. package/dist/main/video/canvasTransform.d.ts +1 -1
  53. package/dist/main/video/canvasTransform.js +66 -12
  54. package/dist/main/video/segmenters/mediapipe.d.ts +1 -11
  55. package/dist/main/video/segmenters/mediapipe.js +37 -22
  56. package/dist/main/video/transformer.d.ts +1 -1
  57. package/dist/main/video/typeGuards.d.ts +2 -1
  58. package/dist/main/video/types.d.ts +2 -12
  59. package/dist/main/video/utils.d.ts +2 -2
  60. package/dist/main/video/utils.js +2 -2
  61. package/dist/main/video/video.d.ts +3 -2
  62. package/dist/main/video/video.js +5 -3
  63. package/dist/main/video/videoStreamTrackProcessor.d.ts +1 -1
  64. package/dist/main/video/videoStreamTrackProcessor.js +2 -2
  65. package/dist/workers/mediaWorker.js +16 -16
  66. package/dist/workers/tsconfig.tsbuildinfo +1 -1
  67. package/dist/worklets/tsconfig.tsbuildinfo +1 -1
  68. package/dist/worklets/types.d.ts +0 -2
  69. package/package.json +44 -8
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 20.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 2bfd9e1: Adds esm entry points.
8
+
9
+ ### Minor Changes
10
+
11
+ - 4baa8a4: Implements dual filter blur wglsl shader.
12
+ - 3555d9d: Adds `build:sourcemap` script.
13
+ - 0416d6c: Introduces WebGPU option for video processing.
14
+ - ed53b05: Upgrade Typescript
15
+ - 5855473: Enables dynamic updates of processing width and height.
16
+
17
+ ### Patch Changes
18
+
19
+ - d3909ed: Enable recommended linter rules
20
+ - e76a15e: Migrate to biome linter
21
+ - Updated dependencies [2bfd9e1]
22
+ - Updated dependencies [4fe77de]
23
+ - Updated dependencies [2bbff64]
24
+ - Updated dependencies [4ea0492]
25
+ - Updated dependencies [3555d9d]
26
+ - Updated dependencies [1601985]
27
+ - Updated dependencies [84956b8]
28
+ - Updated dependencies [ed53b05]
29
+ - Updated dependencies [d681fd3]
30
+ - Updated dependencies [e76a15e]
31
+ - @pexip/utils@17.0.0
32
+
3
33
  ## 19.1.0
4
34
 
5
35
  ### Minor Changes
@@ -64,8 +64,8 @@ export const createBlurRenderer = (processor, pass) => {
64
64
  const texture = createAndSetupTexture(gl);
65
65
  assert(texture);
66
66
  // Avoid fractional value
67
- const width = Math.trunc(processor.width / Math.pow(2, idx));
68
- const height = Math.trunc(processor.height / Math.pow(2, idx));
67
+ const width = Math.trunc(processor.width / 2 ** idx);
68
+ const height = Math.trunc(processor.height / 2 ** idx);
69
69
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
70
70
  const frameBuffer = gl.createFramebuffer();
71
71
  assert(frameBuffer);
@@ -99,7 +99,9 @@ export const createBlurRenderer = (processor, pass) => {
99
99
  // Adjust pass number accordingly
100
100
  const currentPass = pass + 1;
101
101
  if (currentPass > blurBuffers.length) {
102
- generateBlurBuffers(currentPass - blurBuffers.length, blurBuffers.length).forEach(buffer => blurBuffers.push(buffer));
102
+ for (const buffer of generateBlurBuffers(currentPass - blurBuffers.length, blurBuffers.length)) {
103
+ blurBuffers.push(buffer);
104
+ }
103
105
  }
104
106
  while (currentPass < blurBuffers.length) {
105
107
  const buffer = blurBuffers.pop();
@@ -158,10 +160,10 @@ export const createBlurRenderer = (processor, pass) => {
158
160
  };
159
161
  },
160
162
  release: () => {
161
- blurBuffers.forEach(({ frameBuffer, texture }) => {
163
+ for (const { frameBuffer, texture } of blurBuffers) {
162
164
  gl.deleteTexture(texture);
163
165
  gl.deleteFramebuffer(frameBuffer);
164
- });
166
+ }
165
167
  gl.deleteBuffer(positionBuffer);
166
168
  gl.deleteBuffer(texCoordBuffer);
167
169
  gl.deleteVertexArray(vao);
@@ -1,2 +1,2 @@
1
1
  import type { Renderer, RenderEventHandlers } from '../../types/render';
2
- export declare const createRenderer: (canvas: OffscreenCanvas | undefined, renderEventHandlers: RenderEventHandlers) => Renderer;
2
+ export declare const createRenderer: (renderEventHandlers: RenderEventHandlers, canvas?: OffscreenCanvas) => Renderer;
@@ -1,27 +1,16 @@
1
1
  import { assert } from '@pexip/utils';
2
2
  import { PROCESSING_WIDTH, PROCESSING_HEIGHT, EDGE_BLUR_AMOUNT, BACKGROUND_BLUR_AMOUNT, RENDERING_EVENTS, } from '../../constants';
3
- import { toTextureInfo } from './webglUtils';
4
3
  import { createTextureRenderer } from './texture';
5
4
  import { createBlurRenderer } from './blur';
6
5
  import { createBlendRenderer } from './blender';
7
6
  import { createCanvasRenderer } from './canvas';
8
7
  import { createTextureToTextureRenderer } from './textureToTexture';
9
8
  import { createSmoothingMaskRenderer } from './smoothingMask';
10
- const createLazyProps = (stateBuilder) => {
11
- const props = {};
12
- return new Proxy(props, {
13
- get(target, p, receiver) {
14
- const prop = target[p];
15
- if (!prop) {
16
- target[p] = stateBuilder[p]?.(props);
17
- }
18
- return Reflect.get(target, p, receiver);
19
- },
20
- });
21
- };
22
- export const createRenderer = (canvas = new OffscreenCanvas(PROCESSING_WIDTH, PROCESSING_HEIGHT), renderEventHandlers) => {
9
+ import { createLazyProps, handleWebGLContextLoss } from '../../utils';
10
+ export const createRenderer = (renderEventHandlers, canvas = new OffscreenCanvas(PROCESSING_WIDTH, PROCESSING_HEIGHT)) => {
23
11
  const rendererCreator = {
24
12
  videoFrameRenderer: () => createTextureRenderer(canvas),
13
+ maskRenderer: () => createTextureRenderer(canvas),
25
14
  canvasRenderer: () => createCanvasRenderer(canvas),
26
15
  backgroundImageRenderer: () => createTextureRenderer(canvas),
27
16
  blendRenderer: () => createBlendRenderer(canvas),
@@ -29,17 +18,26 @@ export const createRenderer = (canvas = new OffscreenCanvas(PROCESSING_WIDTH, PR
29
18
  maskBlurRenderer: () => createBlurRenderer(canvas, EDGE_BLUR_AMOUNT),
30
19
  textureRenderer: () => createTextureToTextureRenderer(canvas),
31
20
  smoothingMaskRenderer: () => createSmoothingMaskRenderer(canvas),
32
- release: (states) => () => Object.keys(states).forEach(key => key !== 'release' &&
33
- states[key]?.release()),
21
+ release: (states) => () => {
22
+ for (const key in states) {
23
+ if (key !== 'release') {
24
+ states[key]?.release();
25
+ }
26
+ }
27
+ },
34
28
  };
35
29
  let lazyProps = createLazyProps(rendererCreator);
36
30
  const props = {
37
31
  canvas,
38
32
  gl: null,
33
+ canvasContextEventsCleanup: () => undefined,
39
34
  // TODO: refactor to avoid repeated codes
40
35
  get videoFrameRenderer() {
41
36
  return lazyProps.videoFrameRenderer;
42
37
  },
38
+ get maskRenderer() {
39
+ return lazyProps.maskRenderer;
40
+ },
43
41
  get canvasRenderer() {
44
42
  return lazyProps.canvasRenderer;
45
43
  },
@@ -63,21 +61,12 @@ export const createRenderer = (canvas = new OffscreenCanvas(PROCESSING_WIDTH, PR
63
61
  return lazyProps.smoothingMaskRenderer;
64
62
  },
65
63
  };
66
- const handleContextLost = (event) => {
67
- event.preventDefault();
68
- cleanup();
69
- renderEventHandlers.contextLost(event.statusMessage);
70
- };
71
- const handleContextRestored = (event) => {
72
- renderEventHandlers.contextRestored(event.statusMessage);
73
- };
74
- const handleContextCreationError = (event) => {
75
- renderEventHandlers.contextCreationError(event.statusMessage);
76
- };
77
64
  const init = () => {
78
- canvas.addEventListener('webglcontextlost', handleContextLost);
79
- canvas.addEventListener('webglcontextrestored', handleContextRestored);
80
- canvas.addEventListener('webglcontextcreationerror', handleContextCreationError);
65
+ props.canvasContextEventsCleanup = handleWebGLContextLoss(canvas, {
66
+ contextLost: renderEventHandlers.contextLost,
67
+ contextRestored: renderEventHandlers.contextRestored,
68
+ contextCreationError: renderEventHandlers.contextCreationError,
69
+ }, cleanup);
81
70
  props.gl = canvas.getContext('webgl2', {
82
71
  premultipliedAlpha: false,
83
72
  powerPreference: 'high-performance',
@@ -86,8 +75,14 @@ export const createRenderer = (canvas = new OffscreenCanvas(PROCESSING_WIDTH, PR
86
75
  };
87
76
  const render = (mask, frame, options) => {
88
77
  assert(!props.gl?.isContextLost(), RENDERING_EVENTS.ContextLost);
78
+ const maskTexture = props.maskRenderer.render({
79
+ frame: mask,
80
+ width: mask.width,
81
+ height: mask.height,
82
+ timestamp: frame.timestamp,
83
+ });
89
84
  const smoothedMask = props.smoothingMaskRenderer.render({
90
- mask: toTextureInfo(mask),
85
+ mask: maskTexture,
91
86
  prevMask: props.prevMask,
92
87
  }, options);
93
88
  const frameTexture = props.videoFrameRenderer.render(frame);
@@ -113,14 +108,12 @@ export const createRenderer = (canvas = new OffscreenCanvas(PROCESSING_WIDTH, PR
113
108
  props.prevMask = props.textureRenderer.render(smoothedMask);
114
109
  };
115
110
  const cleanup = () => {
111
+ props.canvasContextEventsCleanup();
116
112
  lazyProps.release();
117
113
  lazyProps = createLazyProps(rendererCreator);
118
114
  props.prevMask = undefined;
119
115
  };
120
116
  const release = () => {
121
- canvas.removeEventListener('webglcontextlost', handleContextLost);
122
- canvas.removeEventListener('webglcontextrestored', handleContextRestored);
123
- canvas.removeEventListener('webglcontextcreationerror', handleContextCreationError);
124
117
  cleanup();
125
118
  };
126
119
  const isContextLost = () => {
@@ -5,7 +5,7 @@ import type { TextureInfo } from './types';
5
5
  */
6
6
  export declare const createSmoothingMaskRenderer: (processor: OffscreenCanvas) => {
7
7
  render: ({ mask, prevMask, }: {
8
- prevMask?: TextureInfo | undefined;
8
+ prevMask?: TextureInfo;
9
9
  mask: TextureInfo;
10
10
  }, options: RendererOptions) => TextureInfo;
11
11
  release: () => void;
@@ -61,7 +61,7 @@ export declare const createProgram: (gl: WebGLRenderingContext, shaders: [WebGLS
61
61
  * A parallel array to opt_attribs letting you assign locations.
62
62
  */
63
63
  export declare const createProgramFromSources: (gl: WebGLRenderingContext, [vs, fs]: [GLShaderSource, GLShaderSource], attribs?: string[], locations?: number[]) => [WebGLProgram, Release];
64
- export declare const createAndSetupTexture: (gl: WebGL2RenderingContext) => WebGLTexture | null;
64
+ export declare const createAndSetupTexture: (gl: WebGL2RenderingContext) => WebGLTexture;
65
65
  type Location<S extends string> = S extends `u_${string}` ? ReturnType<WebGL2RenderingContext['getUniformLocation']> : S extends `a_${string}` ? ReturnType<WebGL2RenderingContext['getAttribLocation']> : never;
66
66
  export declare const getLocations: <T extends Record<string, string>>(gl: WebGL2RenderingContext, program: WebGLProgram, locations: T) => { [Key in keyof T]: Location<T[Key]>; };
67
67
  export declare const createGLProgram: <A extends Record<string, string>, U extends Record<string, string>>(processCanvas: OffscreenCanvas, { attribs, uniforms, vertexSrc, fragmentSrc, }: {
@@ -37,7 +37,7 @@ function addLineNumbersWithError(src, log = '', lineOffset = 0) {
37
37
  if (!m[1] || m.index === undefined) {
38
38
  return [0, log];
39
39
  }
40
- const lineNo = parseInt(m[1], 10);
40
+ const lineNo = Number.parseInt(m[1], 10);
41
41
  const next = matches[ndx + 1];
42
42
  const end = next ? next.index : log.length;
43
43
  const msg = log.substring(m.index, end);
@@ -93,10 +93,10 @@ export const createProgram = (gl, shaders, attribs, locations) => {
93
93
  if (!program) {
94
94
  throw new Error('Unable to create WebGL program');
95
95
  }
96
- shaders.forEach(function (shader) {
96
+ for (const shader of shaders) {
97
97
  gl.attachShader(program, shader);
98
- });
99
- attribs?.forEach(function (attrib, ndx) {
98
+ }
99
+ attribs?.forEach((attrib, ndx) => {
100
100
  const locationIdx = locations?.[ndx] ?? ndx;
101
101
  gl.bindAttribLocation(program, locationIdx, attrib);
102
102
  });
@@ -118,10 +118,10 @@ export const createProgram = (gl, shaders, attribs, locations) => {
118
118
  return [
119
119
  program,
120
120
  () => {
121
- shaders.forEach(shader => {
121
+ for (const shader of shaders) {
122
122
  gl.detachShader(program, shader);
123
123
  gl.deleteShader(shader);
124
- });
124
+ }
125
125
  gl.deleteProgram(program);
126
126
  },
127
127
  ];
@@ -156,12 +156,12 @@ export const getLocations = (gl, program, locations) => Object.keys(locations).r
156
156
  if (!location) {
157
157
  return acc;
158
158
  }
159
- return {
160
- ...acc,
159
+ Object.assign(acc, {
161
160
  [key]: location.startsWith('u_')
162
161
  ? gl.getUniformLocation(program, location)
163
162
  : gl.getAttribLocation(program, location),
164
- };
163
+ });
164
+ return acc;
165
165
  }, {});
166
166
  export const createGLProgram = (processCanvas, { attribs, uniforms, vertexSrc, fragmentSrc, }) => {
167
167
  const gl = processCanvas.getContext('webgl2', {
@@ -192,4 +192,3 @@ export const toTextureInfo = (mask) => ({
192
192
  width: mask.width,
193
193
  height: mask.height,
194
194
  });
195
- /* eslint-enable max-params --- this is utils */
@@ -0,0 +1,8 @@
1
+ /**
2
+ *
3
+ * Blend foreground and background according to the mask
4
+ */
5
+ export declare const createBlendRenderer: (processor: OffscreenCanvas, device: GPUDevice, presentationFormat: GPUTextureFormat, isContextLost: () => boolean) => {
6
+ render: (frameTexture: GPUTexture, bgTexture: GPUTexture, maskTexture: GPUTexture) => void;
7
+ release: () => void;
8
+ };
@@ -0,0 +1,78 @@
1
+ import { assert } from '@pexip/utils';
2
+ import { RENDERING_EVENTS } from '../../constants';
3
+ import blenderShader from './shaders/blenderShader.wgsl';
4
+ import { RENDER_PASS_COLOR_ATTACHMENT_CLEAR_VALUE } from './constants';
5
+ /**
6
+ *
7
+ * Blend foreground and background according to the mask
8
+ */
9
+ export const createBlendRenderer = (processor, device, presentationFormat, isContextLost) => {
10
+ const context = processor.getContext('webgpu');
11
+ if (!context) {
12
+ throw Error('WebGPU not supported on the canvas.');
13
+ }
14
+ context.configure({
15
+ device,
16
+ format: presentationFormat,
17
+ alphaMode: 'premultiplied',
18
+ });
19
+ return {
20
+ render: (frameTexture, bgTexture, maskTexture) => {
21
+ assert(!isContextLost(), RENDERING_EVENTS.ContextLost);
22
+ const pipeline = device.createRenderPipeline({
23
+ label: 'blender pipeline',
24
+ layout: 'auto',
25
+ vertex: {
26
+ module: device.createShaderModule({
27
+ label: 'blender vertex',
28
+ code: blenderShader,
29
+ }),
30
+ },
31
+ fragment: {
32
+ module: device.createShaderModule({
33
+ label: 'blender fragment',
34
+ code: blenderShader,
35
+ }),
36
+ targets: [{ format: presentationFormat }],
37
+ },
38
+ });
39
+ const encoder = device.createCommandEncoder({
40
+ label: 'blender encoder',
41
+ });
42
+ const pass = encoder.beginRenderPass({
43
+ label: 'blender renderPass',
44
+ colorAttachments: [
45
+ {
46
+ view: context.getCurrentTexture().createView(),
47
+ clearValue: RENDER_PASS_COLOR_ATTACHMENT_CLEAR_VALUE,
48
+ loadOp: 'clear',
49
+ storeOp: 'store',
50
+ },
51
+ ],
52
+ });
53
+ pass.setPipeline(pipeline);
54
+ const sampler = device.createSampler({
55
+ addressModeU: 'repeat',
56
+ addressModeV: 'repeat',
57
+ magFilter: 'nearest',
58
+ });
59
+ const bindGroup = device.createBindGroup({
60
+ layout: pipeline.getBindGroupLayout(0),
61
+ entries: [
62
+ { binding: 0, resource: sampler },
63
+ { binding: 1, resource: bgTexture.createView() },
64
+ { binding: 2, resource: frameTexture.createView() },
65
+ { binding: 3, resource: maskTexture.createView() },
66
+ ],
67
+ });
68
+ pass.setBindGroup(0, bindGroup);
69
+ pass.draw(6); // call our vertex shader 6 times
70
+ pass.end();
71
+ const commandBuffer = encoder.finish();
72
+ device.queue.submit([commandBuffer]);
73
+ },
74
+ release: () => {
75
+ // Nothing
76
+ },
77
+ };
78
+ };
@@ -0,0 +1 @@
1
+ export declare const RENDER_PASS_COLOR_ATTACHMENT_CLEAR_VALUE: number[];
@@ -0,0 +1 @@
1
+ export const RENDER_PASS_COLOR_ATTACHMENT_CLEAR_VALUE = [0.0, 0.0, 0.0, 0];
@@ -0,0 +1,5 @@
1
+ import type { VideoFrameLike } from '../../types/media';
2
+ export declare const createBlurRenderer: (processor: OffscreenCanvas, device: GPUDevice, presentationFormat: GPUTextureFormat, isContextLost: () => boolean) => {
3
+ render: (frame: VideoFrameLike, pass: number) => GPUTexture;
4
+ release: () => void;
5
+ };
@@ -0,0 +1,201 @@
1
+ import { assert } from '@pexip/utils';
2
+ import { RENDERING_EVENTS } from '../../constants';
3
+ import blurExample from './shaders/dualBlurShader.wgsl';
4
+ import { clamping } from '../../utils';
5
+ import { RENDER_PASS_COLOR_ATTACHMENT_CLEAR_VALUE } from './constants';
6
+ const clampBlurAmount = clamping(1, 9);
7
+ export const createBlurRenderer = (processor, device, presentationFormat, isContextLost) => {
8
+ const context = processor.getContext('webgpu');
9
+ if (!context) {
10
+ throw Error('WebGPU not supported on the canvas.');
11
+ }
12
+ context.configure({
13
+ device,
14
+ format: presentationFormat,
15
+ alphaMode: 'premultiplied',
16
+ });
17
+ // Helper to create a texture of given size
18
+ const createTexture = (width, height) => device.createTexture({
19
+ format: presentationFormat,
20
+ size: [width, height],
21
+ usage: GPUTextureUsage.TEXTURE_BINDING |
22
+ GPUTextureUsage.COPY_DST |
23
+ GPUTextureUsage.RENDER_ATTACHMENT,
24
+ });
25
+ // Helper to create a uniform buffer to flip downsampling vs upsampling
26
+ const createFlipBuffer = (value) => {
27
+ const buffer = device.createBuffer({
28
+ size: 4,
29
+ mappedAtCreation: true,
30
+ usage: GPUBufferUsage.UNIFORM,
31
+ });
32
+ new Uint32Array(buffer.getMappedRange())[0] = value;
33
+ buffer.unmap();
34
+ return buffer;
35
+ };
36
+ const sampler = device.createSampler({
37
+ magFilter: 'linear',
38
+ minFilter: 'linear',
39
+ });
40
+ const shaderModule = device.createShaderModule({ code: blurExample });
41
+ const pipeline = device.createRenderPipeline({
42
+ label: 'dual blur render pipeline',
43
+ layout: 'auto',
44
+ vertex: {
45
+ module: shaderModule,
46
+ entryPoint: 'vert_main',
47
+ buffers: [],
48
+ },
49
+ fragment: {
50
+ module: shaderModule,
51
+ entryPoint: 'frag_main',
52
+ targets: [{ format: presentationFormat }],
53
+ },
54
+ primitive: { topology: 'triangle-list' },
55
+ });
56
+ // --- State for persistent textures and buffers ---
57
+ let blurTextures = [];
58
+ let lastWidth = -1;
59
+ let lastHeight = -1;
60
+ // Helper to (re)generate textures if needed
61
+ function ensureBlurTextures(pass, width, height) {
62
+ const length = clampBlurAmount(pass + 1);
63
+ let needsUpdate = false;
64
+ if (blurTextures.length !== length ||
65
+ lastWidth !== width ||
66
+ lastHeight !== height) {
67
+ // Destroy old textures
68
+ for (const t of blurTextures) {
69
+ t.texture.destroy();
70
+ }
71
+ blurTextures = [];
72
+ for (let i = 0; i < length; i++) {
73
+ const w = Math.max(Math.trunc(width / 2 ** i), 1);
74
+ const h = Math.max(Math.trunc(height / 2 ** i), 1);
75
+ blurTextures.push({
76
+ texture: createTexture(w, h),
77
+ width: w,
78
+ height: h,
79
+ });
80
+ }
81
+ lastWidth = width;
82
+ lastHeight = height;
83
+ needsUpdate = true;
84
+ }
85
+ return needsUpdate;
86
+ }
87
+ const flipDownBuffer = createFlipBuffer(0); // create once
88
+ const flipUpBuffer = createFlipBuffer(1); // create once
89
+ return {
90
+ render: (frame, pass) => {
91
+ assert(!isContextLost(), RENDERING_EVENTS.ContextLost);
92
+ // Ensure textures and buffers are up-to-date
93
+ ensureBlurTextures(pass, processor.width, processor.height);
94
+ // Upload frame to the largest texture (level 0)
95
+ const largestTexture = blurTextures[0];
96
+ assert(largestTexture);
97
+ device.queue.copyExternalImageToTexture({ source: frame.frame, flipY: false }, { texture: largestTexture.texture }, { width: largestTexture.width, height: largestTexture.height });
98
+ // Downsampling passes
99
+ for (let i = 1; i < blurTextures.length; i++) {
100
+ const src = blurTextures[i - 1];
101
+ assert(src);
102
+ const dst = blurTextures[i];
103
+ assert(dst);
104
+ const bindGroup = device.createBindGroup({
105
+ layout: pipeline.getBindGroupLayout(0),
106
+ entries: [
107
+ { binding: 0, resource: sampler },
108
+ { binding: 1, resource: src.texture.createView() },
109
+ { binding: 2, resource: { buffer: flipDownBuffer } },
110
+ ],
111
+ });
112
+ const encoder = device.createCommandEncoder();
113
+ const passEncoder = encoder.beginRenderPass({
114
+ colorAttachments: [
115
+ {
116
+ view: dst.texture.createView(),
117
+ loadOp: 'clear',
118
+ storeOp: 'store',
119
+ clearValue: RENDER_PASS_COLOR_ATTACHMENT_CLEAR_VALUE,
120
+ },
121
+ ],
122
+ });
123
+ passEncoder.setPipeline(pipeline);
124
+ passEncoder.setBindGroup(0, bindGroup);
125
+ passEncoder.draw(6);
126
+ passEncoder.end();
127
+ device.queue.submit([encoder.finish()]);
128
+ }
129
+ // Upsampling passes
130
+ for (let i = blurTextures.length - 2; i >= 0; i--) {
131
+ const src = blurTextures[i + 1];
132
+ assert(src);
133
+ const dst = blurTextures[i];
134
+ assert(dst);
135
+ const bindGroup = device.createBindGroup({
136
+ layout: pipeline.getBindGroupLayout(0),
137
+ entries: [
138
+ { binding: 0, resource: sampler },
139
+ { binding: 1, resource: src.texture.createView() },
140
+ { binding: 2, resource: { buffer: flipUpBuffer } },
141
+ ],
142
+ });
143
+ const encoder = device.createCommandEncoder();
144
+ const passEncoder = encoder.beginRenderPass({
145
+ colorAttachments: [
146
+ {
147
+ view: dst.texture.createView(),
148
+ loadOp: 'clear',
149
+ storeOp: 'store',
150
+ clearValue: RENDER_PASS_COLOR_ATTACHMENT_CLEAR_VALUE,
151
+ },
152
+ ],
153
+ });
154
+ passEncoder.setPipeline(pipeline);
155
+ passEncoder.setBindGroup(0, bindGroup);
156
+ passEncoder.draw(6);
157
+ passEncoder.end();
158
+ device.queue.submit([encoder.finish()]);
159
+ }
160
+ // Final pass: draw to canvas
161
+ const finalTexture = blurTextures[0];
162
+ assert(finalTexture);
163
+ const finalBindGroup = device.createBindGroup({
164
+ layout: pipeline.getBindGroupLayout(0),
165
+ entries: [
166
+ { binding: 0, resource: sampler },
167
+ {
168
+ binding: 1,
169
+ resource: finalTexture.texture.createView(),
170
+ },
171
+ { binding: 2, resource: { buffer: flipUpBuffer } },
172
+ ],
173
+ });
174
+ const encoder = device.createCommandEncoder();
175
+ const passEncoder = encoder.beginRenderPass({
176
+ colorAttachments: [
177
+ {
178
+ view: context.getCurrentTexture().createView(),
179
+ loadOp: 'clear',
180
+ storeOp: 'store',
181
+ clearValue: RENDER_PASS_COLOR_ATTACHMENT_CLEAR_VALUE,
182
+ },
183
+ ],
184
+ });
185
+ passEncoder.setPipeline(pipeline);
186
+ passEncoder.setBindGroup(0, finalBindGroup);
187
+ passEncoder.draw(6);
188
+ passEncoder.end();
189
+ device.queue.submit([encoder.finish()]);
190
+ return finalTexture.texture;
191
+ },
192
+ release: () => {
193
+ for (const t of blurTextures) {
194
+ t.texture.destroy();
195
+ }
196
+ blurTextures = [];
197
+ flipDownBuffer.destroy();
198
+ flipUpBuffer.destroy();
199
+ },
200
+ };
201
+ };
@@ -0,0 +1,2 @@
1
+ import type { Renderer, RenderEventHandlers } from '../../types/render';
2
+ export declare const createRenderer: (renderEventHandlers: RenderEventHandlers, canvas?: OffscreenCanvas) => Renderer;