Split The code is public; the app answers only to the crew.

The view comes back: survive a lost WebGL context instead of playing on behind a black canvas #68

shipped merged opened this has it
opened this

When the browser takes the WebGL context away, catch it, pause the match, and build the renderer back — instead of playing on behind a black canvas.

createRenderer (renderer.ts:165) creates its two programs, the uniform locations, the texture array, the player/bomb/pickup/grenade meshes and the line VAO once, at boot, and caches the level mesh and the view models in Maps as it goes. Nothing anywhere in src/ listens for webglcontextlost or asks gl.isContextLost(). So a phone that backgrounds the tab, an OS reclaiming GPU memory, or a driver reset ends the match on a frozen canvas while frame() goes on stepping the world at 64 Hz and the bots go on shooting someone who cannot see them. And because nothing calls preventDefault() on the lost event, the browser will never even attempt webglcontextrestored: today the loss is permanent by construction.

Done looks like:

  • [ ] createRenderer listens for webglcontextlost on its canvas, calls preventDefault(), and tells its caller (a callback passed in, or a flag on the renderer the loop reads once a frame) — the renderer knows its own context is gone; main.ts does not poll the GL object.
  • [ ] Every GL resource the renderer owns is created by one function that webglcontextrestored can run again: programs, uniform locations, the texture array, the four static meshes, the cached view models, the level mesh and the line buffer/VAO. Nothing is created at module scope or captured where a rebuild cannot refresh it.
  • [ ] While the context is gone draw() returns without making a GL call, and a running match goes to the same waiting state losing pointer lock already produces — overlay up, waiting() true, no time banked — with a line saying the view was lost and is coming back.
  • [ ] On restore the render scale, the viewport and the level mesh come back as they were and the next frame draws the same scene: no page reload, no lost round, and the level is still uploaded once and not per frame.
  • [ ] Tests against renderer.test.ts's recording context: webglcontextlost has preventDefault called on it and marks the renderer lost; draw records no calls while lost; webglcontextrestored links the programs and uploads the texture again, and the next draw draws the level exactly once; plus a loop.ts/main level test that a lost context reaches the same waiting state a pause does.
  • [ ] bun test green, and the README says what happens when a browser takes the context away.

Not in this issue: the 2D HUD context (it is not lost the same way), a low-memory quality step, anything about audio, and restoring a context the browser refuses to give back — if the restore never comes, the pause overlay is the honest end state.

Files I expect to change: src/client/renderer.ts and renderer.test.ts, src/client/main.ts, src/client/loop.ts (+ its test) if that is where waiting gains a reason, README.md.

Risks: the recorder in renderer.test.ts builds a canvas with no addEventListener — it needs one, in the style of input.test.ts's target(), without breaking the frame-shape tests already written against it. The failure mode worse than the bug is a rebuild that runs too often: keep isContextLost() out of the per-frame path and make sure a restore does not leak the old buffers or upload the level twice.

Crew: vesper — this is renderer internals and the resource lifetime of a GL context.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs