Reuse GL context on panel resize

Panel resizing now creates a new surface from the existing
GrDirectContext instead of destroying and recreating the entire
renderer. All caches (paints, fonts, node pictures, scene picture,
filters) are preserved across resizes.
This commit is contained in:
Danila Poyarkov 2026-03-03 12:48:51 +03:00
parent 921f28e9f9
commit 93c549bad0
5 changed files with 53 additions and 14 deletions

View file

@ -49,6 +49,7 @@
- Per-node SkPicture cache for effect rendering — unchanged shadow/blur nodes replay from cache on scene redraws
- Drop shadows use `MaskFilter` direct draw instead of `saveLayer` offscreen buffers
- Cached `ImageFilter`, `MaskFilter`, reusable effect paint — zero per-frame WASM allocations for effects
- Reuse GL context on panel resize — swap surface without recreating renderer, preserving all caches
- Per-frame absolute position cache — avoids repeated parent-chain walks during rendering
- Optimize zoom/pan smoothness with `shallowReactive`, `useRafFn`, and input coalescing

View file

@ -328,6 +328,12 @@ export class SkiaRenderer {
this.invalidateAllPictures()
}
replaceSurface(surface: Surface): void {
this.surface.delete()
this.surface = surface
this.invalidateScenePicture()
}
invalidateScenePicture(): void {
this.scenePicture?.delete()
this.scenePicture = null

View file

@ -50,4 +50,3 @@ input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
display: none;
}

View file

@ -63,7 +63,9 @@ const cursor = computed(() => {
<div
class="absolute bottom-1/2 left-1/2 h-0.5 w-25 -translate-x-1/2 translate-y-10 overflow-hidden rounded-full bg-white/8"
>
<div class="h-full w-2/5 animate-[slide_1s_ease-in-out_infinite] rounded-full bg-white/25" />
<div
class="h-full w-2/5 animate-[slide_1s_ease-in-out_infinite] rounded-full bg-white/25"
/>
</div>
</div>
</Transition>

View file

@ -41,6 +41,7 @@ export function useCanvas(canvasRef: Ref<HTMLCanvasElement | null>, store: Edito
let renderer: SkiaRenderer | null = null
let ck: CanvasKit | null = null
let gpuCtx: WebGPUContext | null = null
let glContext: ReturnType<CanvasKit['MakeGrContext']> | null = null
let destroyed = false
let dirty = true
let lastRenderVersion = -1
@ -71,17 +72,34 @@ export function useCanvas(canvasRef: Ref<HTMLCanvasElement | null>, store: Edito
}
}
function sizeCanvas(canvas: HTMLCanvasElement) {
const dpr = window.devicePixelRatio || 1
canvas.width = canvas.clientWidth * dpr
canvas.height = canvas.clientHeight * dpr
}
function makeGLSurface(canvas: HTMLCanvasElement) {
if (!ck) return null
if (!glContext) {
const isTest = new URLSearchParams(window.location.search).has('test')
const glAttrs = isTest ? { preserveDrawingBuffer: 1 } : undefined
const handle = ck.GetWebGLContext(canvas, glAttrs)
if (!handle) return null
glContext = ck.MakeGrContext(handle)
}
if (!glContext) return null
return ck.MakeOnScreenGLSurface(glContext, canvas.width, canvas.height, ck.ColorSpace.SRGB)
}
function createSurface(canvas: HTMLCanvasElement) {
if (!ck) return
renderer?.destroy()
renderer = null
glContext?.delete()
glContext = null
const dpr = window.devicePixelRatio || 1
const w = canvas.clientWidth
const h = canvas.clientHeight
canvas.width = w * dpr
canvas.height = h * dpr
sizeCanvas(canvas)
let surface
if (getGpuBackend() === 'webgpu' && gpuCtx) {
@ -93,12 +111,7 @@ export function useCanvas(canvasRef: Ref<HTMLCanvasElement | null>, store: Edito
return
}
} else {
const isTest = new URLSearchParams(window.location.search).has('test')
surface = ck.MakeWebGLCanvasSurface(
canvas,
undefined,
isTest ? { preserveDrawingBuffer: 1 } : undefined
)
surface = makeGLSurface(canvas)
if (!surface) {
console.error('Failed to create WebGL surface')
return
@ -171,6 +184,7 @@ export function useCanvas(canvasRef: Ref<HTMLCanvasElement | null>, store: Edito
pause()
cancelAnimationFrame(resizeRaf)
renderer?.destroy()
glContext?.delete()
})
let resizeRaf = 0
@ -179,10 +193,27 @@ export function useCanvas(canvasRef: Ref<HTMLCanvasElement | null>, store: Edito
if (!canvas || !ck || resizeRaf) return
resizeRaf = requestAnimationFrame(() => {
resizeRaf = 0
createSurface(canvas)
resizeCanvas(canvas)
})
})
function resizeCanvas(canvas: HTMLCanvasElement) {
if (!ck || !renderer) {
createSurface(canvas)
return
}
sizeCanvas(canvas)
const surface = makeGLSurface(canvas)
if (!surface) {
createSurface(canvas)
return
}
renderer.replaceSurface(surface)
renderNow()
}
function hitTestSectionTitle(canvasX: number, canvasY: number) {
return renderer?.hitTestSectionTitle(store.graph, canvasX, canvasY) ?? null
}