fix(canvas): report retained backing failures

- Warn once with the rejected surface dimensions and original CanvasKit error
- Disable repeated cache allocation attempts until the renderer surface is replaced
- Keep direct scene rendering as the visible fallback
This commit is contained in:
Danila Poyarkov 2026-08-05 19:10:41 +03:00
parent c4df0c0257
commit afc5305e8c
3 changed files with 34 additions and 11 deletions

View file

@ -122,6 +122,7 @@ export class SkiaRenderer {
} | null = null
sceneBackingPreviewUntil = 0
sceneBackingNeedsCrispRender = false
sceneBackingAllocationFailed = false
sceneBackingBuild: {
surface: Surface
graph: SceneGraph
@ -451,6 +452,7 @@ export class SkiaRenderer {
replaceSurface(surface: Surface): void {
this.surface.delete()
this.surface = surface
this.sceneBackingAllocationFailed = false
this.invalidateScenePicture()
}

View file

@ -187,15 +187,22 @@ function sceneBackingGeometry(r: SkiaRenderer) {
}
function createSceneBackingSurface(r: SkiaRenderer, width: number, height: number): Surface | null {
try {
return r.surface.makeSurface({
if (r.sceneBackingAllocationFailed) return null
const info = {
width: Math.ceil(width * r.dpr),
height: Math.ceil(height * r.dpr),
colorType: r.ck.ColorType.RGBA_8888,
alphaType: r.ck.AlphaType.Premul,
colorSpace: r.ck.ColorSpace.SRGB
})
} catch {
}
try {
return r.surface.makeSurface(info)
} catch (error) {
r.sceneBackingAllocationFailed = true
console.warn(
`Disabling retained scene backing after CanvasKit failed to allocate ${info.width}×${info.height}`,
error
)
return null
}
}
@ -452,6 +459,7 @@ export function renderSceneBacking(
graph: SceneGraph,
sceneVersion: number
): boolean {
if (r.sceneBackingAllocationFailed) return false
const positionPreviewVersion = graph.positionPreviewVersion
const allowStaleZoom = now() < r.sceneBackingPreviewUntil
const hasCoverage = backingCoverageContainsLiveViewport(

View file

@ -1,4 +1,4 @@
import { expect, mock, test } from 'bun:test'
import { expect, mock, spyOn, test } from 'bun:test'
import type { Canvas, Image as CKImage, ImageInfo, Surface } from 'canvaskit-wasm'
@ -39,6 +39,7 @@ function createRenderer(surfaceFactory: (info: ImageInfo) => Surface | null) {
pageId: 'page',
sceneBacking: null,
sceneBackingBuild: null,
sceneBackingAllocationFailed: false,
sceneBackingNeedsCrispRender: false,
sceneBackingPreviewUntil: 0,
sceneBackingAverageRecordMs: 40,
@ -124,15 +125,27 @@ test('retained scene backing preserves the full margin when it fits the allocati
expect(requests[0]).toMatchObject({ width: 2400, height: 1800 })
})
test('retained scene backing falls back when CanvasKit throws during allocation', () => {
test('retained scene backing reports a throwing allocation and disables further attempts', () => {
const error = new TypeError("Cannot set properties of null (setting 'be')")
const r = createRenderer(() => {
throw new TypeError("Cannot set properties of null (setting 'be')")
throw error
})
const canvas = createCanvas()
const warn = spyOn(console, 'warn').mockImplementation(() => undefined)
expect(() => renderSceneBacking(r, canvas, createGraph(), 1)).not.toThrow()
expect(renderSceneBacking(r, canvas, createGraph(), 1)).toBe(false)
expect(r.sceneBackingAllocationFailed).toBe(true)
expect(r.surface.makeSurface).toHaveBeenCalledTimes(1)
expect(warn).toHaveBeenCalledTimes(1)
expect(warn).toHaveBeenCalledWith(
'Disabling retained scene backing after CanvasKit failed to allocate 300×300',
error
)
expect(r.sceneBacking).toBeNull()
expect(canvas.drawImageRectOptions).not.toHaveBeenCalled()
warn.mockRestore()
})
test('retained scene backing filters cross-zoom previews instead of falling back to live rendering', () => {