From da17a205f1d6892e595089132f11789699ced732 Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Thu, 25 Jun 2026 13:18:20 -0500 Subject: [PATCH] fix(webgl): release throwaway context in premultiply-alpha probe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `detectPremultiplyAlphaHonored` creates a temporary WebGL context to test whether `createImageBitmap(..., { premultiplyAlpha: 'premultiply' })` is honored, but it only deleted the texture/framebuffer — the context itself was left dangling until GC. This probe runs in a microtask *after* the main render context is created, so a leaked context here is the newest one on the page. On embedded TV browsers (Apollo/Sunrise, Chrome 38+) the live-context budget is tiny; the lingering probe pushes the page over the limit and the browser evicts the *oldest* context — the live render context — after which every `createTexture()` returns null and the engine spams "Could not create WebGL Texture" each frame. Release the probe context immediately via `WEBGL_lose_context` on every exit path instead of waiting for GC. The lose event fires only on the throwaway canvas, so it does not trip the main renderer's `webglcontextlost` handler. Only active when `premultiplyAlphaHonored: 'auto'` is set (default is `true`, no probe). Co-Authored-By: Claude Opus 4.8 --- src/core/lib/validateImageBitmap.test.ts | 15 +++++++++++++++ src/core/lib/validateImageBitmap.ts | 8 ++++++++ 2 files changed, 23 insertions(+) diff --git a/src/core/lib/validateImageBitmap.test.ts b/src/core/lib/validateImageBitmap.test.ts index bee51ba..bc014d5 100644 --- a/src/core/lib/validateImageBitmap.test.ts +++ b/src/core/lib/validateImageBitmap.test.ts @@ -44,6 +44,10 @@ function createFakeGl(readbackRed: number, framebufferComplete = true) { ), deleteFramebuffer: vi.fn(), deleteTexture: vi.fn(), + // The probe releases its throwaway context via WEBGL_lose_context once done. + getExtension: vi.fn((name: string) => + name === 'WEBGL_lose_context' ? { loseContext: vi.fn() } : null, + ), }; } @@ -116,4 +120,15 @@ describe('detectPremultiplyAlphaHonored', () => { false, ); }); + + it('releases the throwaway context so it does not leak a GL slot', async () => { + const lose = { loseContext: vi.fn() }; + const gl = createFakeGl(128); + gl.getExtension = vi.fn((name: string) => + name === 'WEBGL_lose_context' ? lose : null, + ); + await detectPremultiplyAlphaHonored(createPlatform(gl)); + expect(gl.getExtension).toHaveBeenCalledWith('WEBGL_lose_context'); + expect(lose.loseContext).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/core/lib/validateImageBitmap.ts b/src/core/lib/validateImageBitmap.ts index 8c4abcf..5fab8c6 100644 --- a/src/core/lib/validateImageBitmap.ts +++ b/src/core/lib/validateImageBitmap.ts @@ -172,5 +172,13 @@ export async function detectPremultiplyAlphaHonored( gl.deleteTexture(tex); bitmap.close?.(); + // Release this throwaway context immediately. Embedded TV browsers cap the + // number of live WebGL contexts very low; since this probe runs AFTER the + // main render context is created, a leaked context here is the newest one + // and its lingering presence can push the page over the limit, evicting the + // OLDEST context (the live render context) — which then fails every + // createTexture. Don't wait for GC to reclaim the canvas; drop it now. + gl.getExtension('WEBGL_lose_context')?.loseContext(); + return result; }