@pexip/media-processor 20.3.5 → 21.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 (118) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/api-docs/README.mdx +33 -6
  3. package/api-docs/functions/createBenchmark.mdx +17 -5
  4. package/api-docs/functions/createWindowedStats.mdx +23 -0
  5. package/api-docs/functions/iou.mdx +19 -0
  6. package/api-docs/functions/labelComponents.mdx +20 -0
  7. package/api-docs/functions/nearestPowerOfTwo.mdx +21 -0
  8. package/api-docs/functions/playVideo.mdx +16 -0
  9. package/api-docs/functions/scoreComponent.mdx +22 -0
  10. package/api-docs/functions/setVideoElementSrc.mdx +14 -0
  11. package/api-docs/interfaces/Benchmark.mdx +12 -0
  12. package/api-docs/interfaces/RendererOptions.mdx +26 -9
  13. package/api-docs/interfaces/SegmentationParams.mdx +29 -12
  14. package/api-docs/interfaces/SelectionOptions.mdx +15 -0
  15. package/api-docs/interfaces/Stats.mdx +11 -0
  16. package/api-docs/interfaces/Weights.mdx +12 -0
  17. package/api-docs/type-aliases/OptionalKeys.mdx +10 -0
  18. package/api-docs/variables/BACKGROUND_THRESHOLD.mdx +3 -0
  19. package/api-docs/variables/DOWN_SAMPLE_FACTOR.mdx +3 -0
  20. package/api-docs/variables/EDGE_BLUR_AMOUNT.mdx +1 -1
  21. package/api-docs/variables/EXCLUDE_BYSTANDERS.mdx +3 -0
  22. package/api-docs/variables/FOREGROUND_THRESHOLD.mdx +1 -1
  23. package/api-docs/variables/LIGHT_WRAP_BLUR_AMOUNT.mdx +3 -0
  24. package/api-docs/variables/LIGHT_WRAP_EDGE_BAND.mdx +3 -0
  25. package/api-docs/variables/LIGHT_WRAP_INTENSITY.mdx +3 -0
  26. package/api-docs/variables/LIGHT_WRAP_TIGHTNESS.mdx +3 -0
  27. package/api-docs/variables/MASK_COMBINE_RATIO.mdx +1 -1
  28. package/api-docs/variables/MORPH_DILATE_RADIUS_PX.mdx +3 -0
  29. package/api-docs/variables/MORPH_ERODE_RADIUS_PX.mdx +3 -0
  30. package/api-docs/variables/MORPH_PASS.mdx +3 -0
  31. package/api-docs/variables/PERSON_CENTER.mdx +3 -0
  32. package/api-docs/variables/SIGMA_RANGE.mdx +3 -0
  33. package/api-docs/variables/SIGMA_SPACE.mdx +3 -0
  34. package/api-docs/variables/STRONG_EDGE_BLUR_AMOUNT.mdx +3 -0
  35. package/api-docs/variables/getCanvasRenderingContext2D.mdx +14 -0
  36. package/api-docs/variables/getImageSize.mdx +13 -0
  37. package/dist/common/backends/webgl/binaryMask.d.ts +8 -0
  38. package/dist/common/backends/webgl/binaryMask.js +95 -0
  39. package/dist/common/backends/webgl/blender.d.ts +2 -1
  40. package/dist/common/backends/webgl/blender.js +18 -15
  41. package/dist/common/backends/webgl/blur.d.ts +1 -1
  42. package/dist/common/backends/webgl/blur.js +23 -16
  43. package/dist/common/backends/webgl/canvas.js +9 -4
  44. package/dist/common/backends/webgl/diffAbsMask.d.ts +10 -0
  45. package/dist/common/backends/webgl/diffAbsMask.js +103 -0
  46. package/dist/common/backends/webgl/geodesicReconstruction.d.ts +13 -0
  47. package/dist/common/backends/webgl/geodesicReconstruction.js +176 -0
  48. package/dist/common/backends/webgl/jointBilateralFilter.d.ts +12 -0
  49. package/dist/common/backends/webgl/jointBilateralFilter.js +108 -0
  50. package/dist/common/backends/webgl/morphErode.d.ts +13 -0
  51. package/dist/common/backends/webgl/morphErode.js +121 -0
  52. package/dist/common/backends/webgl/renderer.js +194 -34
  53. package/dist/common/backends/webgl/replace.d.ts +17 -0
  54. package/dist/common/backends/webgl/replace.js +126 -0
  55. package/dist/common/backends/webgl/selectComponent.d.ts +15 -0
  56. package/dist/common/backends/webgl/selectComponent.js +102 -0
  57. package/dist/common/backends/webgl/smoothingMask.d.ts +3 -3
  58. package/dist/common/backends/webgl/smoothingMask.js +22 -21
  59. package/dist/common/backends/webgl/tentBlur.d.ts +9 -0
  60. package/dist/common/backends/webgl/tentBlur.js +118 -0
  61. package/dist/common/backends/webgl/texture.d.ts +1 -1
  62. package/dist/common/backends/webgl/texture.js +19 -17
  63. package/dist/common/backends/webgl/textureToTexture.d.ts +1 -1
  64. package/dist/common/backends/webgl/textureToTexture.js +19 -16
  65. package/dist/common/backends/webgl/webglUtils.d.ts +7 -1
  66. package/dist/common/backends/webgl/webglUtils.js +64 -6
  67. package/dist/common/backends/webgpu/blender.d.ts +6 -1
  68. package/dist/common/backends/webgpu/blender.js +96 -51
  69. package/dist/common/backends/webgpu/constants.js +1 -1
  70. package/dist/common/backends/webgpu/dualFilterBlur.d.ts +2 -3
  71. package/dist/common/backends/webgpu/dualFilterBlur.js +146 -140
  72. package/dist/common/backends/webgpu/jointBilateralFilter.d.ts +7 -0
  73. package/dist/common/backends/webgpu/jointBilateralFilter.js +162 -0
  74. package/dist/common/backends/webgpu/renderer.js +211 -39
  75. package/dist/common/backends/webgpu/replace.d.ts +11 -0
  76. package/dist/common/backends/webgpu/replace.js +175 -0
  77. package/dist/common/backends/webgpu/resize.d.ts +4 -0
  78. package/dist/common/backends/webgpu/resize.js +76 -0
  79. package/dist/common/backends/webgpu/smoothingMask.d.ts +2 -2
  80. package/dist/common/backends/webgpu/smoothingMask.js +74 -40
  81. package/dist/common/backends/webgpu/tentBlur.d.ts +5 -0
  82. package/dist/common/backends/webgpu/tentBlur.js +131 -0
  83. package/dist/common/backends/webgpu/texture.d.ts +2 -2
  84. package/dist/common/backends/webgpu/texture.js +24 -19
  85. package/dist/common/backends/webgpu/textureToTexture.d.ts +1 -1
  86. package/dist/common/backends/webgpu/textureToTexture.js +28 -15
  87. package/dist/common/backends/webgpu/types.d.ts +2 -0
  88. package/dist/common/backends/webgpu/webgpuUtils.d.ts +24 -4
  89. package/dist/common/backends/webgpu/webgpuUtils.js +60 -11
  90. package/dist/common/constants.d.ts +19 -5
  91. package/dist/common/constants.js +19 -5
  92. package/dist/common/index.d.ts +1 -0
  93. package/dist/common/index.js +1 -0
  94. package/dist/common/tsconfig.tsbuildinfo +1 -1
  95. package/dist/common/types/render.d.ts +142 -13
  96. package/dist/common/types/utils.d.ts +21 -0
  97. package/dist/common/utils.d.ts +16 -0
  98. package/dist/common/utils.js +152 -0
  99. package/dist/main/benchUtils.d.ts +33 -1
  100. package/dist/main/benchUtils.js +145 -11
  101. package/dist/main/tsconfig.tsbuildinfo +1 -1
  102. package/dist/main/video/canvasTransform.d.ts +1 -1
  103. package/dist/main/video/canvasTransform.js +135 -47
  104. package/dist/main/video/index.d.ts +1 -1
  105. package/dist/main/video/index.js +1 -1
  106. package/dist/workers/mediaWorker.js +35 -162
  107. package/dist/workers/tsconfig.tsbuildinfo +1 -1
  108. package/package.json +8 -7
  109. package/dist/common/backends/webgl/shaders/blendFragmentShader.d.ts +0 -1
  110. package/dist/common/backends/webgl/shaders/blendFragmentShader.js +0 -21
  111. package/dist/common/backends/webgl/shaders/dualFilterBlurFragmentShader.d.ts +0 -1
  112. package/dist/common/backends/webgl/shaders/dualFilterBlurFragmentShader.js +0 -45
  113. package/dist/common/backends/webgl/shaders/smoothingMaskFragmentShader.d.ts +0 -1
  114. package/dist/common/backends/webgl/shaders/smoothingMaskFragmentShader.js +0 -57
  115. package/dist/common/backends/webgl/shaders/textureFragmentShader.d.ts +0 -1
  116. package/dist/common/backends/webgl/shaders/textureFragmentShader.js +0 -16
  117. package/dist/common/backends/webgl/shaders/vertexShader.d.ts +0 -1
  118. package/dist/common/backends/webgl/shaders/vertexShader.js +0 -19
@@ -0,0 +1,162 @@
1
+ import { makeShaderDataDefinitions, makeStructuredView } from 'webgpu-utils';
2
+ import { assert } from '@pexip/utils';
3
+ import { RENDERING_EVENTS } from '../../constants';
4
+ import jointBilateralFilterHorizontalShader from './shaders/jointBilateralFilterHorizontal.wgsl';
5
+ import jointBilateralFilterVerticalShader from './shaders/jointBilateralFilterVertical.wgsl';
6
+ import { updateTextureStorageFormat } from './webgpuUtils';
7
+ export const createJointBilateralFilterRenderer = (processor, device, isContextLost, width, height, maskFormat) => {
8
+ // Dispatch workgroups based on the shader's workgroup size (64x4)
9
+ const TILE_W = 32;
10
+ const TILE_ROWS = 4;
11
+ const TILE_H = 32;
12
+ const TILE_COLS = 4;
13
+ // Create intermediate texture for the filtered result
14
+ const tempTexture = device.createTexture({
15
+ label: 'Joint Bilateral Filtered temporary mask texture',
16
+ format: maskFormat,
17
+ size: [width, height],
18
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING,
19
+ });
20
+ const outputTexture = device.createTexture({
21
+ label: 'Joint Bilateral Filtered Mask Texture',
22
+ format: maskFormat,
23
+ size: [width, height],
24
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING,
25
+ });
26
+ const defs = makeShaderDataDefinitions(jointBilateralFilterHorizontalShader);
27
+ // @ts-expect-error This is what we get from the API
28
+ const uniforms = makeStructuredView(defs.uniforms.params);
29
+ // Create params buffer
30
+ const paramsBuffer = device.createBuffer({
31
+ size: uniforms.arrayBuffer.byteLength,
32
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
33
+ });
34
+ const scaleFactor = width / processor.width;
35
+ const texelWidth = 1 / width;
36
+ const texelHeight = 1 / height;
37
+ uniforms.set({
38
+ width,
39
+ height,
40
+ texelWidth,
41
+ texelHeight,
42
+ });
43
+ const updateParams = (params) => {
44
+ uniforms.set({
45
+ ...params,
46
+ sigmaSpace: params.sigmaSpace * scaleFactor,
47
+ });
48
+ device.queue.writeBuffer(paramsBuffer, 0, uniforms.arrayBuffer);
49
+ };
50
+ // Generate shader code with appropriate format
51
+ const shaderHCode = updateTextureStorageFormat(jointBilateralFilterHorizontalShader, maskFormat);
52
+ const shaderVCode = updateTextureStorageFormat(jointBilateralFilterVerticalShader, maskFormat);
53
+ // Cache shader module and pipeline at init time
54
+ const shaderHModule = device.createShaderModule({
55
+ label: 'joint bilateral filter horizontal compute',
56
+ code: shaderHCode,
57
+ });
58
+ const shaderVModule = device.createShaderModule({
59
+ label: 'joint bilateral filter vertical compute',
60
+ code: shaderVCode,
61
+ });
62
+ const computeHPipeline = device.createComputePipeline({
63
+ label: 'joint bilateral filter horizontal pipeline',
64
+ layout: 'auto',
65
+ compute: { module: shaderHModule },
66
+ });
67
+ const computeVPipeline = device.createComputePipeline({
68
+ label: 'joint bilateral filter vertical pipeline',
69
+ layout: 'auto',
70
+ compute: { module: shaderVModule },
71
+ });
72
+ const uniformsHBindGroups = device.createBindGroup({
73
+ layout: computeHPipeline.getBindGroupLayout(1),
74
+ entries: [
75
+ { binding: 0, resource: { buffer: paramsBuffer } },
76
+ { binding: 1, resource: tempTexture.createView() },
77
+ ],
78
+ });
79
+ const uniformsVBindGroups = device.createBindGroup({
80
+ layout: computeVPipeline.getBindGroupLayout(1),
81
+ entries: [
82
+ { binding: 0, resource: { buffer: paramsBuffer } },
83
+ { binding: 1, resource: outputTexture.createView() },
84
+ ],
85
+ });
86
+ const compute = (label, encoder, pipeline, bindGroup0, bindGroup1, workgroupCountX, workgroupCountY) => {
87
+ const computePass = encoder.beginComputePass({ label });
88
+ computePass.setPipeline(pipeline);
89
+ computePass.setBindGroup(0, bindGroup0);
90
+ computePass.setBindGroup(1, bindGroup1);
91
+ computePass.dispatchWorkgroups(workgroupCountX, workgroupCountY);
92
+ computePass.end();
93
+ };
94
+ const wgCntXH = Math.ceil(width / TILE_W);
95
+ const wgCntYH = Math.ceil(height / TILE_ROWS);
96
+ const wgCntXV = Math.ceil(width / TILE_COLS);
97
+ const wgCntYV = Math.ceil(height / TILE_H);
98
+ let cacheMaskTexture = null;
99
+ let cacheGuideTexture = null;
100
+ let cacheBindGroupH = null;
101
+ let cacheBindGroupV = null;
102
+ const cache = {
103
+ sigmaSpace: -1,
104
+ sigmaRangeLo: -1,
105
+ sigmaRangeHi: -1,
106
+ backgroundThreshold: -1,
107
+ foregroundThreshold: -1,
108
+ };
109
+ return {
110
+ render: (encoder, maskTexture, guideTexture, params) => {
111
+ assert(!isContextLost(), RENDERING_EVENTS.ContextLost);
112
+ if (!(cache.sigmaSpace === params.sigmaSpace &&
113
+ cache.sigmaRangeLo === params.sigmaRangeLo &&
114
+ cache.sigmaRangeHi === params.sigmaRangeHi &&
115
+ cache.backgroundThreshold === params.backgroundThreshold &&
116
+ cache.foregroundThreshold === params.foregroundThreshold)) {
117
+ updateParams(params);
118
+ cache.sigmaSpace = params.sigmaSpace;
119
+ cache.sigmaRangeLo = params.sigmaRangeLo;
120
+ cache.sigmaRangeHi = params.sigmaRangeHi;
121
+ cache.backgroundThreshold = params.backgroundThreshold;
122
+ cache.foregroundThreshold = params.foregroundThreshold;
123
+ }
124
+ // Horizontal
125
+ if (cacheMaskTexture !== maskTexture ||
126
+ cacheGuideTexture !== guideTexture) {
127
+ cacheBindGroupH = device.createBindGroup({
128
+ layout: computeHPipeline.getBindGroupLayout(0),
129
+ entries: [
130
+ { binding: 0, resource: maskTexture.createView() },
131
+ { binding: 1, resource: guideTexture.createView() },
132
+ ],
133
+ });
134
+ cacheMaskTexture = maskTexture;
135
+ }
136
+ // Horizontal pipeline
137
+ compute('Joint Bilateral Filter Horizontal pass', encoder, computeHPipeline, cacheBindGroupH, uniformsHBindGroups, wgCntXH, wgCntYH);
138
+ // Vertical
139
+ if (cacheGuideTexture !== guideTexture) {
140
+ cacheBindGroupV = device.createBindGroup({
141
+ layout: computeVPipeline.getBindGroupLayout(0),
142
+ entries: [
143
+ { binding: 0, resource: tempTexture.createView() },
144
+ { binding: 1, resource: guideTexture.createView() },
145
+ ],
146
+ });
147
+ cacheGuideTexture = guideTexture;
148
+ }
149
+ compute('Joint Bilateral Filter Vertical pass', encoder, computeVPipeline, cacheBindGroupV, uniformsVBindGroups, wgCntXV, wgCntYV);
150
+ return outputTexture;
151
+ },
152
+ release: () => {
153
+ tempTexture.destroy();
154
+ outputTexture.destroy();
155
+ paramsBuffer.destroy();
156
+ cacheMaskTexture = null;
157
+ cacheGuideTexture = null;
158
+ cacheBindGroupH = null;
159
+ cacheBindGroupV = null;
160
+ },
161
+ };
162
+ };
@@ -1,43 +1,95 @@
1
- import { PROCESSING_WIDTH, PROCESSING_HEIGHT, } from '../../constants';
1
+ import { assert } from '@pexip/utils';
2
+ import { PROCESSING_WIDTH, PROCESSING_HEIGHT, STRONG_EDGE_BLUR_AMOUNT, } from '../../constants';
2
3
  import { createObjectUpdater, createLazyProps } from '../../utils';
3
4
  import { createTextureRenderer } from './texture';
4
5
  import { createBlendRenderer } from './blender';
5
- import { getGPUDevice, hasBGRA8unormStorage } from './webgpuUtils';
6
+ import { getGPUDevice, hasR16FloatTextureStorage, supportsTextureFormat, hasF16, } from './webgpuUtils';
6
7
  import { createBlurRenderer } from './dualFilterBlur';
7
8
  import { createSmoothingMaskRenderer } from './smoothingMask';
8
9
  import { createTextureToTextureRenderer } from './textureToTexture';
10
+ import { createJointBilateralFilterRenderer } from './jointBilateralFilter';
11
+ import { createTextureResizeRenderer } from './resize';
12
+ import { createTentBlurRenderer } from './tentBlur';
13
+ import { createReplaceRenderer } from './replace';
9
14
  export const createRenderer = (adapter, renderEventHandlers, canvas = new OffscreenCanvas(PROCESSING_WIDTH, PROCESSING_HEIGHT)) => {
15
+ const halfWidth = Math.trunc(canvas.width / 2);
16
+ const halfHeight = Math.trunc(canvas.height / 2);
17
+ const quarterWidth = Math.trunc(canvas.width / 4);
18
+ const quarterHeight = Math.trunc(canvas.height / 4);
19
+ const fullChannelTextureFormat = 'rgba8unorm';
10
20
  const rendererCreator = {
21
+ edgeBlurRenderer: () => {
22
+ assert(props.device);
23
+ return createTentBlurRenderer(props.device, isContextLost, canvas.width, canvas.height, props.maskFormat, props.supportF16);
24
+ },
11
25
  videoFrameRenderer: () => {
12
- if (!props.device) {
13
- throw new Error('No device.');
14
- }
15
- return createTextureRenderer(canvas, props.device, isContextLost);
26
+ assert(props.device);
27
+ return createTextureRenderer(props.device, isContextLost, fullChannelTextureFormat, canvas.width, canvas.height, 'linear');
28
+ },
29
+ maskRenderer: () => {
30
+ assert(props.device);
31
+ return createTextureRenderer(props.device, isContextLost, props.maskFormat, canvas.width, canvas.height, 'nearest');
32
+ },
33
+ maskDownsampler2: () => {
34
+ assert(props.device);
35
+ return createTextureResizeRenderer(props.device, isContextLost, halfWidth, halfHeight, props.maskFormat, 'nearest');
36
+ },
37
+ maskDownsampler4: () => {
38
+ assert(props.device);
39
+ return createTextureResizeRenderer(props.device, isContextLost, quarterWidth, quarterHeight, props.maskFormat, 'nearest');
40
+ },
41
+ maskUpsampler: () => {
42
+ assert(props.device);
43
+ return createTextureResizeRenderer(props.device, isContextLost, canvas.width, canvas.height, props.maskFormat, 'nearest');
44
+ },
45
+ frameDownsampler2: () => {
46
+ assert(props.device);
47
+ return createTextureResizeRenderer(props.device, isContextLost, halfWidth, halfHeight, fullChannelTextureFormat, 'linear');
48
+ },
49
+ frameDownsampler4: () => {
50
+ assert(props.device);
51
+ return createTextureResizeRenderer(props.device, isContextLost, quarterWidth, quarterHeight, fullChannelTextureFormat, 'linear');
16
52
  },
17
53
  backgroundImageRenderer: () => {
18
- if (!props.device) {
19
- throw new Error('No device.');
20
- }
54
+ assert(props.device);
55
+ return createTextureRenderer(props.device, isContextLost, fullChannelTextureFormat, canvas.width, canvas.height, 'linear');
56
+ },
57
+ blendRenderer: () => {
58
+ assert(props.device);
21
59
  return createBlendRenderer(canvas, props.device, props.presentationFormat, isContextLost);
22
60
  },
23
61
  blurRenderer: () => {
24
- if (!props.device) {
25
- throw new Error('No device.');
26
- }
27
- return createBlurRenderer(canvas, props.device, props.presentationFormat, isContextLost);
62
+ assert(props.device);
63
+ return createBlurRenderer(props.device, canvas.width, canvas.height, fullChannelTextureFormat, isContextLost);
28
64
  },
29
- maskSmoothingRenderer: () => {
30
- if (!props.device) {
31
- throw new Error('No device.');
32
- }
33
- return createSmoothingMaskRenderer(canvas, props.device, isContextLost);
65
+ jointBilateralFilterRenderer2: () => {
66
+ assert(props.device);
67
+ return createJointBilateralFilterRenderer(canvas, props.device, isContextLost, halfWidth, halfHeight, props.maskFormat);
68
+ },
69
+ jointBilateralFilterRenderer4: () => {
70
+ assert(props.device);
71
+ return createJointBilateralFilterRenderer(canvas, props.device, isContextLost, quarterWidth, quarterHeight, props.maskFormat);
72
+ },
73
+ maskSmoothingRenderer2: () => {
74
+ assert(props.device);
75
+ return createSmoothingMaskRenderer(props.device, isContextLost, props.maskFormat, halfWidth, halfHeight, props.supportF16);
76
+ },
77
+ maskSmoothingRenderer4: () => {
78
+ assert(props.device);
79
+ return createSmoothingMaskRenderer(props.device, isContextLost, props.maskFormat, quarterWidth, quarterHeight, props.supportF16);
34
80
  },
35
81
  prevMaskRenderer: () => {
36
- if (!props.device) {
37
- throw new Error('No device.');
38
- }
82
+ assert(props.device);
39
83
  return createTextureToTextureRenderer(props.device, isContextLost);
40
84
  },
85
+ strongMaskBlurRenderer: () => {
86
+ assert(props.device);
87
+ return createBlurRenderer(props.device, canvas.width, canvas.height, props.maskFormat, isContextLost);
88
+ },
89
+ replaceRenderer: () => {
90
+ assert(props.device);
91
+ return createReplaceRenderer(canvas, props.device, props.presentationFormat, isContextLost);
92
+ },
41
93
  release: (states) => () => {
42
94
  for (const key of Object.keys(states)) {
43
95
  key !== 'release' && states[key]?.release();
@@ -48,33 +100,80 @@ export const createRenderer = (adapter, renderEventHandlers, canvas = new Offscr
48
100
  const props = {
49
101
  canvas,
50
102
  presentationFormat: 'rgba8unorm',
103
+ supportR16FloatTextureStorage: false,
104
+ maskFormat: 'rgba16float',
105
+ supportF16: false,
51
106
  backend: 'webgpu',
52
107
  deviceLost: false,
53
108
  get videoFrameRenderer() {
54
109
  return lazyProps.videoFrameRenderer;
55
110
  },
111
+ get edgeBlurRenderer() {
112
+ return lazyProps.edgeBlurRenderer;
113
+ },
114
+ get maskRenderer() {
115
+ return lazyProps.maskRenderer;
116
+ },
117
+ get maskDownsampler2() {
118
+ return lazyProps.maskDownsampler2;
119
+ },
120
+ get maskDownsampler4() {
121
+ return lazyProps.maskDownsampler4;
122
+ },
123
+ get maskUpsampler() {
124
+ return lazyProps.maskUpsampler;
125
+ },
126
+ get frameDownsampler2() {
127
+ return lazyProps.frameDownsampler2;
128
+ },
129
+ get frameDownsampler4() {
130
+ return lazyProps.frameDownsampler4;
131
+ },
56
132
  get blendRenderer() {
57
- return lazyProps.backgroundImageRenderer;
133
+ return lazyProps.blendRenderer;
58
134
  },
59
135
  get blurRenderer() {
60
136
  return lazyProps.blurRenderer;
61
137
  },
62
- get maskSmoothingRenderer() {
63
- return lazyProps.maskSmoothingRenderer;
138
+ get backgroundImageRenderer() {
139
+ return lazyProps.backgroundImageRenderer;
140
+ },
141
+ get maskSmoothingRenderer2() {
142
+ return lazyProps.maskSmoothingRenderer2;
143
+ },
144
+ get maskSmoothingRenderer4() {
145
+ return lazyProps.maskSmoothingRenderer4;
64
146
  },
65
147
  get prevMaskRenderer() {
66
148
  return lazyProps.prevMaskRenderer;
67
149
  },
150
+ get jointBilateralFilterRenderer2() {
151
+ return lazyProps.jointBilateralFilterRenderer2;
152
+ },
153
+ get jointBilateralFilterRenderer4() {
154
+ return lazyProps.jointBilateralFilterRenderer4;
155
+ },
156
+ get strongMaskBlurRenderer() {
157
+ return lazyProps.strongMaskBlurRenderer;
158
+ },
159
+ get replaceRenderer() {
160
+ return lazyProps.replaceRenderer;
161
+ },
68
162
  };
69
163
  const updateProps = createObjectUpdater(props);
70
164
  const init = async () => {
71
165
  if (!navigator.gpu) {
72
166
  throw Error('WebGPU not supported.');
73
167
  }
74
- props.presentationFormat = hasBGRA8unormStorage(adapter)
75
- ? navigator.gpu.getPreferredCanvasFormat()
76
- : 'rgba8unorm';
168
+ props.presentationFormat = navigator.gpu.getPreferredCanvasFormat();
77
169
  props.device = await getGPUDevice(adapter);
170
+ props.supportR16FloatTextureStorage =
171
+ hasR16FloatTextureStorage(adapter) &&
172
+ supportsTextureFormat(props.device, 'r16float');
173
+ if (props.supportR16FloatTextureStorage) {
174
+ props.maskFormat = 'r16float';
175
+ }
176
+ props.supportF16 = hasF16(adapter);
78
177
  props.device.lost.then(info => {
79
178
  if (info.reason === 'destroyed') {
80
179
  return;
@@ -88,34 +187,107 @@ export const createRenderer = (adapter, renderEventHandlers, canvas = new Offscr
88
187
  if (options) {
89
188
  updateProps(options);
90
189
  }
91
- const frameTexture = props.videoFrameRenderer.render(frame);
92
- const maskTexture = props.videoFrameRenderer.render({
190
+ assert(props.device);
191
+ const encoder = props.device.createCommandEncoder({
192
+ label: 'Renderer Encoder',
193
+ });
194
+ const frameUploadPass = encoder.beginComputePass({
195
+ label: 'frame upload pass',
196
+ });
197
+ const frameTexture = props.videoFrameRenderer.render(frame, frameUploadPass);
198
+ const maskTexture = props.maskRenderer.render({
93
199
  frame: mask,
94
- width: frame.width,
95
- height: frame.height,
200
+ width: mask.width,
201
+ height: mask.height,
96
202
  timestamp: frame.timestamp,
97
- });
98
- const smoothedMaskTexture = props.maskSmoothingRenderer.render(maskTexture, props.prevMask, options);
203
+ }, frameUploadPass);
204
+ frameUploadPass.end();
205
+ let smoothedMask;
206
+ let compositeInputMask;
207
+ switch (options.downSampleFactor) {
208
+ case 4: {
209
+ const downsamplePass = encoder.beginComputePass({
210
+ label: 'downsample pass',
211
+ });
212
+ const maskLowRes = props.maskDownsampler4.render(downsamplePass, maskTexture);
213
+ const frameLowRes = props.frameDownsampler4.render(downsamplePass, frameTexture);
214
+ downsamplePass.end();
215
+ const filteredMask = props.jointBilateralFilterRenderer4.render(encoder, maskLowRes, frameLowRes, options);
216
+ smoothedMask = props.maskSmoothingRenderer4.render(encoder, filteredMask, props.prevMask4 ?? filteredMask, options);
217
+ const upsamplePass = encoder.beginComputePass({
218
+ label: 'upsample pass',
219
+ });
220
+ const upsampledMask = props.maskUpsampler.render(upsamplePass, smoothedMask);
221
+ upsamplePass.end();
222
+ compositeInputMask = props.strongMaskBlurRenderer.render(encoder, upsampledMask, STRONG_EDGE_BLUR_AMOUNT);
223
+ break;
224
+ }
225
+ default: {
226
+ const downsamplePass = encoder.beginComputePass({
227
+ label: 'downsample pass',
228
+ });
229
+ const maskLowRes = props.maskDownsampler2.render(downsamplePass, maskTexture);
230
+ const frameLowRes = props.frameDownsampler2.render(downsamplePass, frameTexture);
231
+ downsamplePass.end();
232
+ const filteredMask = props.jointBilateralFilterRenderer2.render(encoder, maskLowRes, frameLowRes, options);
233
+ smoothedMask = props.maskSmoothingRenderer2.render(encoder, filteredMask, props.prevMask2 ?? filteredMask, options);
234
+ const upsamplePass = encoder.beginComputePass({
235
+ label: 'upsample pass',
236
+ });
237
+ const upsampledMask = props.maskUpsampler.render(upsamplePass, smoothedMask);
238
+ upsamplePass.end();
239
+ compositeInputMask = props.edgeBlurRenderer.render(encoder, upsampledMask, options);
240
+ break;
241
+ }
242
+ }
99
243
  if (options.effects === 'blur') {
100
- const blurredTexture = props.blurRenderer.render(frame, options.backgroundBlurAmount);
101
- props.blendRenderer.render(frameTexture, blurredTexture, smoothedMaskTexture);
244
+ const blurredTexture = props.blurRenderer.render(encoder, frameTexture, options.backgroundBlurAmount);
245
+ props.blendRenderer.render(encoder, {
246
+ foreground: frameTexture,
247
+ background: blurredTexture,
248
+ mask: compositeInputMask,
249
+ }, options);
102
250
  }
103
251
  else if (options.effects === 'overlay') {
104
252
  if (options.backgroundImage) {
105
- const bgTexture = props.videoFrameRenderer.render({
253
+ const bgImgUploadPass = encoder.beginComputePass({
254
+ label: 'background image upload pass',
255
+ });
256
+ const bgTexture = props.backgroundImageRenderer.render({
106
257
  frame: options.backgroundImage,
107
258
  width: options.backgroundImage.width,
108
259
  height: options.backgroundImage.height,
109
260
  timestamp: frame.timestamp,
110
- });
111
- props.blendRenderer.render(frameTexture, bgTexture, smoothedMaskTexture);
261
+ }, bgImgUploadPass);
262
+ bgImgUploadPass.end();
263
+ const blurredBackgroundImageTexture = props.blurRenderer.render(encoder, bgTexture, options.backgroundBlurAmount);
264
+ props.replaceRenderer.render(encoder, {
265
+ foreground: frameTexture,
266
+ background: bgTexture,
267
+ mask: compositeInputMask,
268
+ blurBackground: blurredBackgroundImageTexture,
269
+ timestamp: frame.timestamp,
270
+ }, options);
112
271
  }
113
272
  }
114
- props.prevMask = props.prevMaskRenderer.render(smoothedMaskTexture);
273
+ switch (options.downSampleFactor) {
274
+ case 4:
275
+ props.prevMask4 = props.prevMaskRenderer.render(encoder, smoothedMask);
276
+ break;
277
+ default:
278
+ props.prevMask2 = props.prevMaskRenderer.render(encoder, smoothedMask);
279
+ break;
280
+ }
281
+ props.device.queue.submit([encoder.finish()]);
115
282
  };
116
283
  const release = () => {
117
284
  lazyProps.release();
118
285
  props.deviceLost = false;
286
+ props.prevMask2 = undefined;
287
+ props.prevMask4 = undefined;
288
+ props.presentationFormat = 'rgba8unorm';
289
+ props.supportR16FloatTextureStorage = false;
290
+ props.maskFormat = 'rgba16float';
119
291
  lazyProps = createLazyProps(rendererCreator);
120
292
  };
121
293
  const isContextLost = () => props.deviceLost;
@@ -0,0 +1,11 @@
1
+ import type { RendererOptions } from '../../types/render';
2
+ export declare const createReplaceRenderer: (processor: OffscreenCanvas, device: GPUDevice, presentationFormat: GPUTextureFormat, isContextLost: () => boolean) => {
3
+ render: (encoder: GPUCommandEncoder, { foreground, background, blurBackground, mask, timestamp, }: {
4
+ foreground: GPUTexture;
5
+ background: GPUTexture;
6
+ blurBackground: GPUTexture;
7
+ mask: GPUTexture;
8
+ timestamp: number;
9
+ }, options: RendererOptions) => void;
10
+ release: () => void;
11
+ };
@@ -0,0 +1,175 @@
1
+ import { makeShaderDataDefinitions, makeStructuredView } from 'webgpu-utils';
2
+ import { assert } from '@pexip/utils';
3
+ import { RENDERING_EVENTS } from '../../constants';
4
+ import shaderCode from './shaders/replace.wgsl';
5
+ export const createReplaceRenderer = (processor, device, presentationFormat, isContextLost) => {
6
+ const context = processor.getContext('webgpu');
7
+ if (!context) {
8
+ throw Error('WebGPU not supported on the canvas.');
9
+ }
10
+ context.configure({
11
+ device,
12
+ format: presentationFormat,
13
+ alphaMode: 'premultiplied',
14
+ });
15
+ const defs = makeShaderDataDefinitions(shaderCode);
16
+ // @ts-expect-error This is what we get from the API
17
+ const uniformsSparams = makeStructuredView(defs.uniforms.sparams);
18
+ // @ts-expect-error This is what we get from the API
19
+ const uniformsDparams = makeStructuredView(defs.uniforms.dparams);
20
+ // Cache shader module and pipeline at init time
21
+ const shaderModule = device.createShaderModule({
22
+ label: 'replace shader',
23
+ code: shaderCode,
24
+ });
25
+ const pipeline = device.createRenderPipeline({
26
+ label: 'replace pipeline',
27
+ layout: 'auto',
28
+ vertex: {
29
+ module: shaderModule,
30
+ },
31
+ fragment: {
32
+ module: shaderModule,
33
+ targets: [{ format: presentationFormat }],
34
+ },
35
+ });
36
+ // Cache sampler at init time
37
+ const linearSampler = device.createSampler({
38
+ addressModeU: 'clamp-to-edge',
39
+ addressModeV: 'clamp-to-edge',
40
+ magFilter: 'linear',
41
+ minFilter: 'linear',
42
+ });
43
+ const nearestSampler = device.createSampler({
44
+ addressModeU: 'clamp-to-edge',
45
+ addressModeV: 'clamp-to-edge',
46
+ magFilter: 'nearest',
47
+ minFilter: 'nearest',
48
+ });
49
+ const sparamsBuffer = device.createBuffer({
50
+ size: uniformsSparams.arrayBuffer.byteLength,
51
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
52
+ });
53
+ const dparamsBuffer = device.createBuffer({
54
+ size: uniformsDparams.arrayBuffer.byteLength,
55
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
56
+ });
57
+ const updateSparams = (options) => {
58
+ uniformsSparams.set({
59
+ background: options.backgroundThreshold,
60
+ foreground: options.foregroundThreshold,
61
+ lightWrap: options.lightWrapIntensity,
62
+ lightWrapTightness: options.lightWrapTightness,
63
+ lightWrapEdgeBand: options.lightWrapEdgeBand,
64
+ width: options.width,
65
+ height: options.height,
66
+ });
67
+ device.queue.writeBuffer(sparamsBuffer, 0, uniformsSparams.arrayBuffer);
68
+ };
69
+ const updateDparams = ({ time, parity }) => {
70
+ uniformsDparams.set({ time, parity });
71
+ device.queue.writeBuffer(dparamsBuffer, 0, uniformsDparams.arrayBuffer);
72
+ };
73
+ const colorAttachment0 = {
74
+ loadOp: 'load',
75
+ storeOp: 'store',
76
+ };
77
+ const renderPassDescriptor = {
78
+ label: 'replace renderPass',
79
+ colorAttachments: [colorAttachment0],
80
+ };
81
+ const bindGroup0 = device.createBindGroup({
82
+ layout: pipeline.getBindGroupLayout(0),
83
+ entries: [
84
+ { binding: 0, resource: linearSampler },
85
+ { binding: 1, resource: nearestSampler },
86
+ { binding: 2, resource: { buffer: sparamsBuffer } },
87
+ { binding: 3, resource: { buffer: dparamsBuffer } },
88
+ ],
89
+ });
90
+ let cachedBindGroup1 = null;
91
+ let cachedMask = null;
92
+ let cachedBackground = null;
93
+ let cachedForeground = null;
94
+ let cachedBlurBackground = null;
95
+ let cachedBackgroundThreshold = -1;
96
+ let cachedForegroundThreshold = -1;
97
+ let cachedLightWrapIntensity = -1;
98
+ let cachedLightWrapTightness = -1;
99
+ let cachedLightWrapEdgeBand = -1;
100
+ let cachedWidth = -1;
101
+ let cachedHeight = -1;
102
+ let parity = -1;
103
+ return {
104
+ render: (encoder, { foreground, background, blurBackground, mask, timestamp, }, options) => {
105
+ assert(!isContextLost(), RENDERING_EVENTS.ContextLost);
106
+ colorAttachment0.view = context.getCurrentTexture().createView();
107
+ if (!(cachedBackground === background &&
108
+ cachedForeground === foreground &&
109
+ cachedMask === mask &&
110
+ cachedBlurBackground === blurBackground)) {
111
+ cachedBindGroup1 = device.createBindGroup({
112
+ layout: pipeline.getBindGroupLayout(1),
113
+ entries: [
114
+ { binding: 0, resource: foreground.createView() },
115
+ { binding: 1, resource: background.createView() },
116
+ { binding: 2, resource: mask.createView() },
117
+ { binding: 3, resource: blurBackground.createView() },
118
+ ],
119
+ });
120
+ cachedForeground = foreground;
121
+ cachedBackground = background;
122
+ cachedMask = mask;
123
+ cachedBlurBackground = blurBackground;
124
+ }
125
+ if (!(cachedBackgroundThreshold === options.backgroundThreshold &&
126
+ cachedForegroundThreshold === options.foregroundThreshold &&
127
+ cachedLightWrapIntensity === options.lightWrapIntensity &&
128
+ cachedLightWrapTightness === options.lightWrapTightness &&
129
+ cachedLightWrapEdgeBand === options.lightWrapEdgeBand &&
130
+ cachedWidth === background.width &&
131
+ cachedHeight === background.height)) {
132
+ updateSparams({
133
+ backgroundThreshold: options.backgroundThreshold,
134
+ foregroundThreshold: options.foregroundThreshold,
135
+ lightWrapIntensity: options.lightWrapIntensity,
136
+ lightWrapTightness: options.lightWrapTightness,
137
+ lightWrapEdgeBand: options.lightWrapEdgeBand,
138
+ width: background.width,
139
+ height: background.height,
140
+ });
141
+ cachedBackgroundThreshold = options.backgroundThreshold;
142
+ cachedForegroundThreshold = options.foregroundThreshold;
143
+ cachedLightWrapIntensity = options.lightWrapIntensity;
144
+ cachedLightWrapTightness = options.lightWrapTightness;
145
+ cachedLightWrapEdgeBand = options.lightWrapEdgeBand;
146
+ cachedWidth = background.width;
147
+ cachedHeight = background.height;
148
+ }
149
+ parity = -parity;
150
+ updateDparams({ time: (timestamp * 1e-6) % 1024.0, parity });
151
+ const pass = encoder.beginRenderPass(renderPassDescriptor);
152
+ pass.setPipeline(pipeline);
153
+ pass.setBindGroup(0, bindGroup0);
154
+ pass.setBindGroup(1, cachedBindGroup1);
155
+ pass.draw(6); // call our vertex shader 6 times
156
+ pass.end();
157
+ },
158
+ release: () => {
159
+ sparamsBuffer.destroy();
160
+ dparamsBuffer.destroy();
161
+ cachedBackgroundThreshold = -1;
162
+ cachedForegroundThreshold = -1;
163
+ cachedLightWrapIntensity = -1;
164
+ cachedLightWrapTightness = -1;
165
+ cachedLightWrapEdgeBand = -1;
166
+ cachedWidth = -1;
167
+ cachedHeight = -1;
168
+ cachedBlurBackground = null;
169
+ cachedBackground = null;
170
+ cachedForeground = null;
171
+ cachedMask = null;
172
+ cachedBindGroup1 = null;
173
+ },
174
+ };
175
+ };
@@ -0,0 +1,4 @@
1
+ export declare const createTextureResizeRenderer: (device: GPUDevice, isContextLost: () => boolean, width: number, height: number, textureFormat: GPUTextureFormat, filterMode: GPUFilterMode) => {
2
+ render(pass: GPUComputePassEncoder, texture: GPUTexture): GPUTexture;
3
+ release(): void;
4
+ };