diff --git a/packages/core/src/canvas/renderer/retained-backing.ts b/packages/core/src/canvas/renderer/retained-backing.ts index 3d25bfe82..1bf0b3ef5 100644 --- a/packages/core/src/canvas/renderer/retained-backing.ts +++ b/packages/core/src/canvas/renderer/retained-backing.ts @@ -9,6 +9,7 @@ import type { RenderLayer } from './pipeline' const now = typeof performance !== 'undefined' ? () => performance.now() : () => 0 const SCENE_BACKING_SCALE = 3 +const MAX_SCENE_BACKING_DEVICE_PIXELS = 12_000_000 const FRAME_BUDGET_60HZ_MS = 1000 / 60 const MIN_SCENE_BACKING_IDLE_FRAMES = 2 const MAX_SCENE_BACKING_IDLE_FRAMES = 18 @@ -45,10 +46,7 @@ export function updateSceneBackingPreviewState(r: SkiaRenderer, layer: RenderLay if (layer !== 'scene') return const previous = r.lastSceneViewport const viewportChanged = - !previous || - previous.panX !== r.panX || - previous.panY !== r.panY || - previous.zoom !== r.zoom + !previous || previous.panX !== r.panX || previous.panY !== r.panY || previous.zoom !== r.zoom if (viewportChanged) { const timestamp = now() if (r.sceneBackingLastViewportEventAt > 0) { @@ -85,7 +83,12 @@ function backingScreenCoverageContainsViewport(r: SkiaRenderer): boolean { const scale = r.zoom / backing.zoom const x = r.panX - backing.panX * scale const y = r.panY - backing.panY * scale - return x <= 0 && y <= 0 && x + backing.width * scale >= r.viewportWidth && y + backing.height * scale >= r.viewportHeight + return ( + x <= 0 && + y <= 0 && + x + backing.width * scale >= r.viewportWidth && + y + backing.height * scale >= r.viewportHeight + ) } function backingWorldCoverageContainsLiveViewport(r: SkiaRenderer): boolean { @@ -129,12 +132,7 @@ function drawSceneBacking( const backing = r.sceneBacking if ( !backing || - !backingCoverageContainsLiveViewport( - r, - sceneVersion, - allowStaleZoom, - positionPreviewVersion - ) + !backingCoverageContainsLiveViewport(r, sceneVersion, allowStaleZoom, positionPreviewVersion) ) { return false } @@ -154,9 +152,23 @@ function drawSceneBacking( return true } +export function sceneBackingScaleForViewport( + viewportWidth: number, + viewportHeight: number, + dpr: number +): number { + const viewportDevicePixels = Math.max(1, viewportWidth * viewportHeight * dpr * dpr) + return clamp( + Math.sqrt(MAX_SCENE_BACKING_DEVICE_PIXELS / viewportDevicePixels), + 1, + SCENE_BACKING_SCALE + ) +} + function sceneBackingGeometry(r: SkiaRenderer) { - const marginX = r.viewportWidth * ((SCENE_BACKING_SCALE - 1) / 2) - const marginY = r.viewportHeight * ((SCENE_BACKING_SCALE - 1) / 2) + const backingScale = sceneBackingScaleForViewport(r.viewportWidth, r.viewportHeight, r.dpr) + const marginX = r.viewportWidth * ((backingScale - 1) / 2) + const marginY = r.viewportHeight * ((backingScale - 1) / 2) const width = Math.max(1, Math.ceil(r.viewportWidth + marginX * 2)) const height = Math.max(1, Math.ceil(r.viewportHeight + marginY * 2)) const backingPanX = r.panX + marginX @@ -464,4 +476,3 @@ export function renderSceneBacking( positionPreviewVersion ) } - diff --git a/tests/engine/render/canvas/retained-backing.test.ts b/tests/engine/render/canvas/retained-backing.test.ts index 7a6a44606..e4f0582fb 100644 --- a/tests/engine/render/canvas/retained-backing.test.ts +++ b/tests/engine/render/canvas/retained-backing.test.ts @@ -1,8 +1,12 @@ import { expect, mock, test } from 'bun:test' + import type { Canvas, Image as CKImage, Surface } from 'canvaskit-wasm' import type { SkiaRenderer } from '#core/canvas/renderer' -import { renderSceneBacking } from '#core/canvas/renderer/retained-backing' +import { + renderSceneBacking, + sceneBackingScaleForViewport +} from '#core/canvas/renderer/retained-backing' import type { SceneGraph } from '#core/scene-graph' function createRenderer(surfaceFactory: () => Surface | null) { @@ -75,6 +79,12 @@ function createGraph(positionPreviewVersion = 0) { return graph as SceneGraph } +test('retained scene backing caps large viewport backing allocations', () => { + expect(sceneBackingScaleForViewport(400, 300, 1)).toBe(3) + expect(sceneBackingScaleForViewport(2560, 1600, 2)).toBe(1) + expect(sceneBackingScaleForViewport(2560, 1600, 1)).toBeLessThan(2) +}) + test('retained scene backing falls back when CanvasKit cannot create an offscreen surface', () => { const r = createRenderer(() => null) const canvas = createCanvas()