From 559a0bf1fa60e5830e39f80c81ec6fc91c5bd9ea Mon Sep 17 00:00:00 2001 From: Enrico Schwendig Date: Thu, 9 Jul 2026 14:09:53 +0200 Subject: [PATCH] Revert "Revert "fix: optimize renderer for WebGL through ANGLE on Direct3D [WPB-25918]"" --- .../media/VideoBackgroundEffects.ts | 11 +- .../backgroundEffectsController.ts | 20 +- .../media/backgroundEffects/pipe/options.ts | 2 - .../media/backgroundEffects/pipe/renderer.ts | 200 +++++++++++------- .../media/backgroundEffectsHandler.ts | 10 +- 5 files changed, 151 insertions(+), 92 deletions(-) diff --git a/apps/webapp/src/script/repositories/media/VideoBackgroundEffects.ts b/apps/webapp/src/script/repositories/media/VideoBackgroundEffects.ts index 3457a397cf4..b07f4e59737 100644 --- a/apps/webapp/src/script/repositories/media/VideoBackgroundEffects.ts +++ b/apps/webapp/src/script/repositories/media/VideoBackgroundEffects.ts @@ -47,15 +47,20 @@ export type BackgroundSource = { */ export const DEFAULT_BACKGROUND_EFFECT: BackgroundEffectSelection = {type: 'none'}; +export interface BgStrength { + bgBlur: number; + bgBlurRadius: number; +} + /** * Blur strength values mapped to blur levels. * * Values range from 0.0 (no blur) to 1.0 (maximum blur). These are passed * directly to the background effects controller's blur strength parameter. */ -export const BLUR_STRENGTHS: Record = { - low: 0.7, - high: 1.0, +export const BLUR_STRENGTHS: Record = { + low: {bgBlur: 0.75, bgBlurRadius: 5}, + high: {bgBlur: 1.0, bgBlurRadius: 30}, }; type BuiltinBackgroundLabelKey = diff --git a/apps/webapp/src/script/repositories/media/backgroundEffects/backgroundEffectsController.ts b/apps/webapp/src/script/repositories/media/backgroundEffects/backgroundEffectsController.ts index 389c6b77cf5..2a98e3aafa3 100644 --- a/apps/webapp/src/script/repositories/media/backgroundEffects/backgroundEffectsController.ts +++ b/apps/webapp/src/script/repositories/media/backgroundEffects/backgroundEffectsController.ts @@ -28,7 +28,7 @@ import { } from 'Repositories/media/backgroundEffects/quality/definitions'; import {QUALITY_TIERS, QualityController} from 'Repositories/media/backgroundEffects/quality/qualityController'; import {backgroundEffectsStore} from 'Repositories/media/useBackgroundEffectsStore'; -import {BackgroundSource} from 'Repositories/media/VideoBackgroundEffects'; +import {BackgroundSource, BgStrength} from 'Repositories/media/VideoBackgroundEffects'; import {getLogger, Logger} from 'Util/logger'; import {type CapabilityInfo, type EffectMode, type Metrics, Mode} from './backgroundEffectsWorkerTypes'; @@ -46,7 +46,6 @@ import {runSegmenter, updateSegmenterOptions} from './pipe/segmenter'; // Blur strength (0–1) maps to Gaussian sigma in pixel units for the shader. // The shader's blur radius is 30 px, so a sigma in the ~10–20 px range gives // visually useful blur. Multiply by this factor to get from the 0–1 range. -const BLUR_SIGMA_SCALE = 15; const DEFAULT_FRAME_RATE = 15; const MAX_WIDTH = 256; const MAX_HEIGHT = 144; @@ -189,17 +188,17 @@ export class BackgroundEffectsController { public setMode(mode: EffectMode): void { this.logger.info('Background effects mode', mode); - const renderFlags = modeToRenderFlags(mode, this.options.blurStrength, this.options.bgBlurRadius); + const renderFlags = modeToRenderFlags(mode, this.options.bgBlur, this.options.bgBlurRadius); this.options = {...this.options, mode, ...renderFlags}; this.pushOptionsUpdate(); } - public setBlurStrength(value: number): void { + public setBlurStrength(value: BgStrength): void { this.options = { ...this.options, - blurStrength: value, // Only raise bgBlur when in blur mode; virtual/passthrough use bgBlur=0. - bgBlur: this.options.mode === 'blur' ? blurStrengthToBgBlur(value) : this.options.bgBlur, + bgBlur: this.options.mode === 'blur' ? value.bgBlur : this.options.bgBlur, + bgBlurRadius: value.bgBlurRadius, }; this.pushOptionsUpdate(); } @@ -355,16 +354,14 @@ export class BackgroundEffectsController { // Module-level pure helpers // --------------------------------------------------------------------------- -const blurStrengthToBgBlur = (strength: number): number => Math.max(strength * BLUR_SIGMA_SCALE, 1); - const modeToRenderFlags = ( mode: EffectMode, - blurStrength: number, + bgBlur: number, bgBlurRadius: number, ): Pick => { switch (mode) { case 'blur': - return {enabled: true, bgBlur: blurStrengthToBgBlur(blurStrength), bgBlurRadius}; + return {enabled: true, bgBlur, bgBlurRadius}; case 'virtual': return {enabled: true, bgBlur: 0, bgBlurRadius}; case 'passthrough': @@ -384,7 +381,7 @@ const withoutBitmap = (options: ProcessVideoTrackOptions): ProcessVideoTrackOpti }; const resolveOptions = async (options: ProcessVideoTrackOptions): Promise => { - const renderFlags = modeToRenderFlags(options.mode ?? 'blur', options.blurStrength, options.bgBlurRadius); + const renderFlags = modeToRenderFlags(options.mode ?? 'blur', options.bgBlur, options.bgBlurRadius); const resolved = {...options, ...renderFlags}; if (resolved.backgroundSource?.media instanceof HTMLImageElement) { @@ -421,7 +418,6 @@ const getWorkerOptions = ( modelPath: options.modelPath, useWorker: options.useWorker, mode: options.mode, - blurStrength: options.blurStrength, enabled: options.enabled, quality: options.quality, borderSmooth: options.borderSmooth, diff --git a/apps/webapp/src/script/repositories/media/backgroundEffects/pipe/options.ts b/apps/webapp/src/script/repositories/media/backgroundEffects/pipe/options.ts index 54d7e121e12..b50ebeeaebd 100644 --- a/apps/webapp/src/script/repositories/media/backgroundEffects/pipe/options.ts +++ b/apps/webapp/src/script/repositories/media/backgroundEffects/pipe/options.ts @@ -31,7 +31,6 @@ export type ProcessVideoTrackOptions = { // Virtual background options. mode?: EffectMode; /** Effect mode ('blur', 'virtual', or 'passthrough'). Default: 'blur'. */ - blurStrength: number; enabled: boolean; backgroundSource: BackgroundSource | null; @@ -86,7 +85,6 @@ export const defaultOpts = { // Virtual background options. mode: 'blur', - blurStrength: 0, enabled: true, backgroundSource: null, diff --git a/apps/webapp/src/script/repositories/media/backgroundEffects/pipe/renderer.ts b/apps/webapp/src/script/repositories/media/backgroundEffects/pipe/renderer.ts index 996fc400d46..90ff946c829 100644 --- a/apps/webapp/src/script/repositories/media/backgroundEffects/pipe/renderer.ts +++ b/apps/webapp/src/script/repositories/media/backgroundEffects/pipe/renderer.ts @@ -20,7 +20,12 @@ import {getSafeLogger} from 'Repositories/media/backgroundEffects/helper/logger'; import {BackgroundSource} from 'Repositories/media/VideoBackgroundEffects'; -export type ImageTexture = {texture: WebGLTexture; width: number; height: number; url: string}; +export type ImageTexture = { + texture: WebGLTexture; + width: number; + height: number; + url: string; +}; type ImageInfo = { type: 'image'; @@ -75,10 +80,19 @@ export class WebGLRenderer { private running = false; private static readonly DEFAULT_BG_COLOR: readonly [number, number, number, number] = [33, 150, 243, 255]; + // Windows/Chrome/Edge often run WebGL through ANGLE on Direct3D. + // Keeping the temporal mask smaller than the output canvas reduces D3D texture writes a lot. + private static readonly STATE_SCALE = 0.5; private currentStateIndex = 0; private backgroundRenderInfo: BackgroundRenderInfo | null = null; private activeBackgroundSourceIdentifier: string | null = null; + private frameTexture: WebGLTexture | null = null; + private frameTextureWidth = 0; + private frameTextureHeight = 0; + private stateTextureWidth = 0; + private stateTextureHeight = 0; + constructor(canvas: OffscreenCanvas) { this.canvas = canvas; const gl = this.canvas.getContext('webgl2', { @@ -156,12 +170,6 @@ export class WebGLRenderer { uniform float u_bgBlurRadius; uniform int u_enabled; - const float PI = 3.141592653589793; - - float gaussianWeight(float offset, float sigma) { - return exp(-(offset * offset) / (2.0 * sigma * sigma)); - } - vec4 getMixedFragColor(vec2 bgTexCoord, vec2 categoryCoord, vec2 offset) { vec4 backgroundColor = texture2D(u_backgroundTexture, bgTexCoord + offset); vec4 frameColor = texture2D(u_frameTexture, v_texCoord + offset); @@ -169,26 +177,28 @@ export class WebGLRenderer { return mix(backgroundColor, frameColor, categoryValue); } - vec4 blurColor(float blur, float radius, bool mixed) { - vec2 categoryCoord = vec2(v_texCoord.x, 1.0 - v_texCoord.y); + // Cheap 9-tap blur. The old radial blur sampled hundreds of pixels per output pixel, + // which is especially expensive on ANGLE/D3D11 on Windows. + vec4 cheapBlurFrame(vec2 coord, float radius) { vec2 texelSize = 1.0 / u_canvasDimensions; - vec4 blurredColor = vec4(0.0); - float totalWeight = 0.0; - for (float angle = 0.0; angle <= 2.0 * PI; angle += PI / 12.0) { - vec2 direction = vec2(cos(angle), sin(angle)); - for (int i = -10; i <= 10; i++) { - float offset = float(i) * (radius / 10.0); - float weight = gaussianWeight(offset, blur); - vec2 v_offset = direction * texelSize * offset; - if (mixed) { - blurredColor += getMixedFragColor(v_texCoord, categoryCoord, v_offset) * weight; - } else { - blurredColor += texture2D(u_frameTexture, v_texCoord + v_offset) * weight; - } - totalWeight += weight; - } - } - return blurredColor / totalWeight; + vec2 r1 = texelSize * radius; + vec2 r2 = texelSize * radius * 0.5; + + vec4 c = vec4(0.0); + + c += texture2D(u_frameTexture, coord) * 4.0; + + c += texture2D(u_frameTexture, coord + vec2( r1.x, 0.0)) * 2.0; + c += texture2D(u_frameTexture, coord + vec2(-r1.x, 0.0)) * 2.0; + c += texture2D(u_frameTexture, coord + vec2(0.0, r1.y)) * 2.0; + c += texture2D(u_frameTexture, coord + vec2(0.0, -r1.y)) * 2.0; + + c += texture2D(u_frameTexture, coord + vec2( r2.x, r2.y)); + c += texture2D(u_frameTexture, coord + vec2(-r2.x, r2.y)); + c += texture2D(u_frameTexture, coord + vec2( r2.x, -r2.y)); + c += texture2D(u_frameTexture, coord + vec2(-r2.x, -r2.y)); + + return c / 20.0; } void main() { @@ -201,11 +211,19 @@ export class WebGLRenderer { float categoryValue = texture2D(u_currentStateTexture, categoryCoord).r; if (u_bgBlur > 0.0 && u_bgBlurRadius > 0.0) { - if (categoryValue < 0.3) { - gl_FragColor = blurColor(u_bgBlur, u_bgBlurRadius, false); - } else { - gl_FragColor = texture2D(u_frameTexture, v_texCoord); - } + float mask = smoothstep(0.08, 0.55, categoryValue); + mask = clamp(mask * 1.25, 0.0, 1.0); + + vec4 sharp = texture2D(u_frameTexture, v_texCoord); + + float effectiveRadius = clamp(u_bgBlurRadius, 1.0, 10.0); + float blurStrength = clamp(u_bgBlur, 0.0, 1.0); + + vec4 blurred = cheapBlurFrame(v_texCoord, effectiveRadius); + + vec4 backgroundBlur = mix(sharp, blurred, blurStrength); + + gl_FragColor = mix(backgroundBlur, sharp, mask); return; } @@ -231,11 +249,15 @@ export class WebGLRenderer { bgTexCoord = vec2( (v_texCoord.x - offsetX) / scaleX, (v_texCoord.y - offsetY) / scaleY ); // Apply border smoothing - if (u_borderSmooth > 0.0 && categoryValue > 0.1 && categoryValue < 0.9) { - gl_FragColor = blurColor(u_borderSmooth, u_bgBlurRadius, false); - } else { - gl_FragColor = getMixedFragColor(bgTexCoord, categoryCoord, vec2(0.0, 0.0)); - } + // Quick fix against ghost edges. + // Strong blur needs a more conservative foreground mask. + float mask = smoothstep(0.08, 0.55, categoryValue); + mask = clamp(mask * 1.25, 0.0, 1.0); + + vec4 backgroundColor = texture2D(u_backgroundTexture, bgTexCoord); + vec4 frameColor = texture2D(u_frameTexture, v_texCoord); + + gl_FragColor = mix(backgroundColor, frameColor, mask); } `; this.blendProgram = this.createAndLinkProgram(blendVertexShaderSource, blendFragmentShaderSource); @@ -294,6 +316,17 @@ export class WebGLRenderer { this.fbo = gl.createFramebuffer(); + this.frameTexture = gl.createTexture(); + if (!this.frameTexture) { + throw new Error('Failed to create frame texture'); + } + gl.bindTexture(gl.TEXTURE_2D, this.frameTexture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.bindTexture(gl.TEXTURE_2D, null); + this.running = true; } @@ -339,7 +372,10 @@ export class WebGLRenderer { g: number, b: number, a: number, - ): {texture: WebGLTexture; color: readonly [number, number, number, number]} { + ): { + texture: WebGLTexture; + color: readonly [number, number, number, number]; + } { const texture = this.gl.createTexture(); if (!texture) { throw new Error('Failed to create texture for color'); @@ -355,6 +391,27 @@ export class WebGLRenderer { return {texture, color: [r, g, b, a] as const}; } + private uploadVideoFrame(videoFrame: VideoFrame, width: number, height: number) { + const gl = this.gl; + + if (!this.frameTexture) { + return; + } + + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, this.frameTexture); + + // texSubImage2D avoids reallocating the underlying D3D texture every frame. + // Fall back to texImage2D when the frame size changes. + if (this.frameTextureWidth === width && this.frameTextureHeight === height) { + gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, videoFrame); + } else { + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, videoFrame); + this.frameTextureWidth = width; + this.frameTextureHeight = height; + } + } + private updateBackgroundIfNeeded(newSource?: BackgroundSource | null) { const gl = this.gl; let newIdentifier: string; @@ -443,19 +500,22 @@ export class WebGLRenderer { const width = this.canvas.width; const height = this.canvas.height; + const stateWidth = Math.max(1, Math.floor(width * WebGLRenderer.STATE_SCALE)); + const stateHeight = Math.max(1, Math.floor(height * WebGLRenderer.STATE_SCALE)); + + if (!this.frameTexture) { + return; + } + + this.uploadVideoFrame(videoFrame, width, height); if (!categoryTexture || !confidenceTexture) { + gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); gl.useProgram(blendProgram); - const frameTexture = gl.createTexture(); gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, frameTexture); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, videoFrame); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.bindTexture(gl.TEXTURE_2D, this.frameTexture); gl.uniform1i(blendLocations.frameTexture, 0); gl.uniform1i(blendLocations.enabled, 0); @@ -468,7 +528,6 @@ export class WebGLRenderer { gl.drawArrays(gl.TRIANGLES, 0, 6); - gl.deleteTexture(frameTexture); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, null); @@ -476,6 +535,17 @@ export class WebGLRenderer { } // Determine read/write indices for state ping-pong + if (stateWidth !== this.stateTextureWidth || stateHeight !== this.stateTextureHeight) { + for (const texture of storedStateTextures) { + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, stateWidth, stateHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + } + + this.currentStateIndex = 0; + this.stateTextureWidth = stateWidth; + this.stateTextureHeight = stateHeight; + } + const readStateIndex = this.currentStateIndex; const writeStateIndex = (this.currentStateIndex + 1) % 2; const prevStateTexture = storedStateTextures[readStateIndex]; @@ -487,11 +557,7 @@ export class WebGLRenderer { gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, newStateTexture, 0); - // Ensure state texture is correctly sized - gl.bindTexture(gl.TEXTURE_2D, newStateTexture); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); - - gl.viewport(0, 0, width, height); + gl.viewport(0, 0, stateWidth, stateHeight); gl.useProgram(stateUpdateProgram); // Bind inputs: Current Cat (0), Current Conf (1), Prev State (2) @@ -529,25 +595,12 @@ export class WebGLRenderer { gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); gl.useProgram(blendProgram); - // Bind Frame Texture (Unit 0) - const frameTexture = gl.createTexture(); gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, frameTexture); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, videoFrame); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.bindTexture(gl.TEXTURE_2D, this.frameTexture); gl.uniform1i(blendLocations.frameTexture, 0); - gl.uniform1f(blendLocations.borderSmooth, options.borderSmooth); - gl.uniform1f(blendLocations.bgBlur, options.bgBlur); - gl.uniform1f(blendLocations.bgBlurRadius, options.bgBlurRadius); - gl.uniform1i(blendLocations.enabled, 1); - // Bind Current State Texture (Unit 1) - const currentStateTexture = storedStateTextures[writeStateIndex]; // Use the *newly written* state gl.activeTexture(gl.TEXTURE1); - gl.bindTexture(gl.TEXTURE_2D, currentStateTexture); + gl.bindTexture(gl.TEXTURE_2D, newStateTexture); gl.uniform1i(blendLocations.currentStateTexture, 1); // Bind Background Texture (Unit 2) @@ -569,7 +622,11 @@ export class WebGLRenderer { this.logger.warn('backgroundRenderInfo is null in render loop. Background may not render correctly.'); } - // Set vertex attributes + gl.uniform1f(blendLocations.borderSmooth, options.borderSmooth); + gl.uniform1f(blendLocations.bgBlur, options.bgBlur); + gl.uniform1f(blendLocations.bgBlurRadius, options.bgBlurRadius); + gl.uniform1i(blendLocations.enabled, 1); + gl.enableVertexAttribArray(blendLocations.position); gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer); gl.vertexAttribPointer(blendLocations.position, 2, gl.FLOAT, false, 0, 0); @@ -577,18 +634,13 @@ export class WebGLRenderer { gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer); gl.vertexAttribPointer(blendLocations.texCoord, 2, gl.FLOAT, false, 0, 0); - // Draw quad to blend onto canvas gl.drawArrays(gl.TRIANGLES, 0, 6); - // --- 3. Cleanup --- - gl.deleteTexture(frameTexture); - // Unbind textures (Units 0, 1, 2 used) for (let i = 0; i < 3; ++i) { gl.activeTexture(gl.TEXTURE0 + i); gl.bindTexture(gl.TEXTURE_2D, null); } - // Swap state index for next frame this.currentStateIndex = writeStateIndex; } @@ -617,5 +669,11 @@ export class WebGLRenderer { this.backgroundRenderInfo = null; } this.activeBackgroundSourceIdentifier = null; + if (this.frameTexture) { + gl.deleteTexture(this.frameTexture); + this.frameTexture = null; + this.frameTextureWidth = 0; + this.frameTextureHeight = 0; + } } } diff --git a/apps/webapp/src/script/repositories/media/backgroundEffectsHandler.ts b/apps/webapp/src/script/repositories/media/backgroundEffectsHandler.ts index dcf7ce5244f..b7fa8530116 100644 --- a/apps/webapp/src/script/repositories/media/backgroundEffectsHandler.ts +++ b/apps/webapp/src/script/repositories/media/backgroundEffectsHandler.ts @@ -30,6 +30,7 @@ import { import { BackgroundEffectSelection, BackgroundSource, + BgStrength, BLUR_STRENGTHS, DEFAULT_BACKGROUND_EFFECT, DEFAULT_BUILTIN_BACKGROUND_ID, @@ -52,7 +53,7 @@ const isVirtualEffect = (effect: BackgroundEffectSelection): boolean => { return effect.type === 'virtual' || effect.type === 'custom'; }; -const getBlurStrength = (effect: BackgroundEffectSelection) => { +const getBlurSettings = (effect: BackgroundEffectSelection): BgStrength => { return effect.type === 'blur' ? BLUR_STRENGTHS[effect.level] : BLUR_STRENGTHS.high; }; @@ -157,7 +158,7 @@ export class BackgroundEffectsHandler { } const isVirtual = isVirtualEffect(preferredEffect); - const blurStrength = getBlurStrength(preferredEffect); + const blurStrength = getBlurSettings(preferredEffect); const backgroundSource = isVirtual ? await this.loadBackgroundSource(preferredEffect) : null; if (this.controller.isProcessing() && this.currentReleasableStream) { @@ -177,9 +178,10 @@ export class BackgroundEffectsHandler { const outputTrack = await this.controller.start(videoTrack, { ...defaultOpts, mode: isVirtual ? 'virtual' : 'blur', - blurStrength, quality: 'auto', - backgroundSource, + backgroundSource: isVirtual ? backgroundSource : null, + bgBlur: blurStrength.bgBlur, + bgBlurRadius: blurStrength.bgBlurRadius, onMetrics: this.onMetrics, onModelChange: this.onModelChange, });