perf(canvas): cap retained backing size

This commit is contained in:
Danila Poyarkov 2026-05-23 00:30:30 +03:00
parent 054199ac15
commit 7b62c8acca
2 changed files with 36 additions and 15 deletions

View file

@ -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
)
}

View file

@ -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()