Skip to content

fix(shader): emit contextLost when shader creation fails on a lost GL context#110

Merged
chiefcll merged 1 commit into
mainfrom
fix/contextlost-on-shader-create-failure
Jun 22, 2026
Merged

fix(shader): emit contextLost when shader creation fails on a lost GL context#110
chiefcll merged 1 commit into
mainfrom
fix/contextlost-on-shader-create-failure

Conversation

@chiefcll

Copy link
Copy Markdown
Contributor

Problem

Production telemetry on modern Chrome/Edge surfaced:

Unable to create the shader: VERTEX_SHADER. WebGlContext Error: 37442

37442 = 0x9242 = CONTEXT_LOST_WEBGL. The GL context was lost, so gl.createShader() returned null and WebGlShaderProgram threw. Two facts make this nasty:

  • Shader programs compile lazily, once per distinct shader cache key, and the cache is never cleared. So this isn't startup-only — it fires the first time any not-yet-compiled shader variant is reached, which can be deep into a session.
  • Shader creation runs on the consumer's reactive stack (convertToShaderrenderer.createShader()), which can execute before the async webglcontextlost DOM event is processed. The throw then propagates out of the public API with no contextLost signal, so consumers have nothing to react to and the exception tears through their reactive layer.

Fix

Detect the lost context at the point of failure and route it through the existing contextLost channel, splitting responsibility cleanly:

  • WebGlContextWrapper — add an isContextLost() passthrough. getError() can't be reused for detection here: its CONTEXT_LOST_WEBGL flag is cleared on first read (already consumed building the error message). isContextLost() is stateful and reliable.
  • WebGlRenderer.createShaderProgram (backend-specific) — wrap in try/catch; on a genuinely lost context, trip stage.setContextLost() (idempotent; emits contextLost), then rethrow. Closes the race with the async webglcontextlost event.
  • CoreShaderManager.createShader (backend-agnostic) — on a lost context, fail soft by returning the default shader node so the throw doesn't propagate through the consumer's reactive layer (recovery is an app reload, per BROWSERS.md). A genuine GLSL compile error (context not lost) still rethrows loudly so real shader bugs aren't masked.

The try/catch sits on the cache-miss path only (once per shader variant), not a hot path.

Behavior note

On context loss this fails soft rather than rethrowing, since the context is unrecoverable in-place and the render loop has already stopped. If a rethrow (e.g. to hit a SolidJS <ErrorBoundary>) is preferred, the CoreShaderManager branch is a one-line flip.

Consumers still need a contextLost listener to actually recover (prompt/auto reload) — the engine deliberately does not rebuild GPU resources in place.

Tests

  • CoreShaderManager.contextLoss.test.ts — fail-soft returns default node on loss; rethrows on genuine compile error; failures aren't cached.
  • WebGlRenderer.contextLoss.test.ts — reproduces the exact Unable to create the shader: VERTEX_SHADER. WebGlContext Error: 37442 throw; verifies setContextLost() fires only on a lost context.

Full suite: 251 passing. Build + lint clean.

🤖 Generated with Claude Code

… context

A lost GL context makes gl.createShader() return null and getError() report
CONTEXT_LOST_WEBGL (0x9242 / 37442), surfacing as an "Unable to create the
shader" / "Vertex shader creation failed" throw. Shader programs compile lazily
on first use of each distinct shader key, so this can fire mid-session — and it
runs on the consumer's reactive stack, which can beat the async
`webglcontextlost` DOM event. The throw then propagates out of
renderer.createShader() with no `contextLost` signal, so consumers can't react.

- WebGlContextWrapper: add an isContextLost() passthrough. getError() can't be
  reused for detection here — its CONTEXT_LOST_WEBGL flag is cleared on first
  read (already consumed building the error message).
- WebGlRenderer.createShaderProgram: detect a lost context on failure and trip
  stage.setContextLost() (idempotent; emits `contextLost`), then rethrow. Closes
  the race with the async webglcontextlost event.
- CoreShaderManager.createShader: on a lost context, fail soft by returning the
  default shader node so the throw doesn't tear through the consumer's reactive
  layer (recovery is an app reload). A genuine GLSL compile error (context not
  lost) still rethrows loudly.

Adds unit tests covering both the lost-context (fail-soft + event) and
healthy-context (rethrow) paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@chiefcll chiefcll merged commit 38582b7 into main Jun 22, 2026
1 check passed
@chiefcll chiefcll deleted the fix/contextlost-on-shader-create-failure branch June 22, 2026 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant