You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Context state machine on Stage: running|lost|restoring|failed|destroyed, with a generation counter so stale async work is discarded. All GPU submission is suspended while not running (updateFrame/renderFrame guards, upload loss detection, useProgram guard).
preventDefault() on webglcontextlost (otherwise no restore event); one recovery frame outside the normal loop before emitting contextRestored. contextRestoreFailed is non-terminal on timeout (stays lost, default 10s via contextRestoreTimeout, 0 disables) and terminal on re-init / recovery-frame failure (failed).
Texture restores flow through the existing per-frame upload budget (TextureThrottler.processSome drains the restore queue first); cheap canvas-generated sources reload immediately, image sources stream in throttled. In-flight decodes from before the loss are discarded, never marked failed. Previously evicted textures reload on demand; destroy while lost drops all queued restores.
Shader programs recompile in place (stable references); executor VBOs, state-manager caches, compressed-extension handles and the render-texture pool are rebuilt; render-to-texture elements re-render via a forced type-3 update (a type-1 update would keep binding dead result textures).
Canvas/2D renderer explicitly out of scope (no listeners attached in c2d mode).
FWIW, L2 has no handleLoopError/emitSafe equivalent: recovery-frame render errors claimed by the stage are reported as contextRestoreFailed and swallowed (the event is the report); anything else rethrows to preserve fail-fast behavior.
Same-source texture reloads now also force a render update — without it, fresh pixels never paint in a static scene (renderFrame only renders when dirty).
Ports lightning-js/renderer#930 semantics to the Lightning 2
architecture: context state machine on Stage, suspend-all-GPU-work
while not running, throttled texture restores through the existing
per-frame upload budget, in-place shader recompiles, generation
counters for stale async work, and a single recovery frame outside
the normal loop before emitting contextRestored. Canvas2D is
explicitly out of scope.
- Keep canvas context listeners armed until destroy so a loss while
stopped still hits preventDefault(); track explicit stop/resume
intent so recovery never restarts a deliberately stopped loop.
- Recheck context state after texture uploads and after the scene
update inside renderFrame; reset _updatingFrame on early exit.
- Skip texture uploads when the context is already lost and route
upload failures through recovery only on loss (rethrow otherwise).
- Recreate CircularPushShader values textures on a new context
generation instead of binding dead handles.
- Dirty the dummy parent on restore so the recovery frame renders
even when nothing else does.
- Check the generation before untracking loading sources so stale
callbacks cannot evict replacement loads.
- Standardize license headers on new test/example files.
resume() can bypass recovery completion and suppress contextRestored
src/platforms/browser/WebPlatform.mjs:108
[P2] Calling resume() (with or without a preceding stop()) after browser restoration but before the recovery animation frame bypasses recovery completion. The stage is already running, so _beginLoop() cancels the recovery frame and starts the normal loop, which never calls onRecoveryFrameCompleted(). This leaves pendingRestoreEvent true and prevents contextRestored from being emitted. Schedule the pending recovery frame before starting the normal loop.
Stage destruction crashes when cancelling preloads after worker teardown
src/tree/TextureManager.mjs:83
[P2] With useImageWorker enabled, destroying a stage with a pending worker-backed preload throws here. Stage.destroy() destroys the platform first, deleting ImageWorker._worker and _items. This new cancellation then reaches ImageWorker.cancel(), which dereferences _worker.postMessage(), aborting the remaining teardown. Background preloads need not belong to an active element, so detaching the application does not necessarily cancel them first. Cancel pending sources before platform teardown, or make worker cancellation safe after destruction.
Recovery frames queue quads whose sources have no native texture yet
while image restores decode. PixelateShader/BoxBlurShader read texture
dimensions per quad, which dereferenced the null handle and failed
recovery terminally. Skip such quads the way never-loaded textures are
skipped, and harden getTextureWidth/Height with a source-dimensions
fallback. Adds a PixelateShader element to the example so the browser
spec covers the path.
[P2] Context listeners are attached only by startLoop(), but Stage.init() skips that call when autostart is false. This supported manual-rendering mode therefore has no listener to call preventDefault() on context loss or handle restoration, leaving it unrecoverable. Attach listeners after context initialization independently of starting the automatic loop, and preserve manual-loop intent when recovery completes.
Recovery completion is lost when restart cancels the recovery frame
src/platforms/browser/WebPlatform.mjs:108
[P2] Calling stop()/resume() after the restore event but before its recovery animation frame cancels that frame. The stage is already running, so startLoop() enters the normal loop, which never calls onRecoveryFrameCompleted(). Consequently, pendingRestoreEvent remains set and contextRestored is never emitted. Schedule the recovery frame whenever completion is still pending before starting the normal loop.
Pending image cancellation crashes after platform teardown
src/tree/TextureManager.mjs:83
[P2] With useImageWorker enabled, destroying a stage with an image load still pending can throw here. Stage.destroy() destroys the platform first, which deletes ImageWorker's _worker and _items. This new cancellation then calls ImageWorker.cancel(), which dereferences _worker.postMessage, aborting the remaining teardown. Cancel pending loads before destroying the platform, or make worker cancellation safe after teardown.
Async uploads proceed while context recovery is suspended
src/tree/TextureManager.mjs:246
[P2] An async text restore can complete after reinitialization or the recovery frame moves the stage to failed. Its callback generation still matches, and TextTexture uses throttle: false, so it reaches this uploader directly. Because the browser context is healthy, GPU allocation and uploads still proceed despite stage suspension. Requeue uploads when _isContextSuspended() is true, without reporting another context loss.
Stale source callbacks survive cancellation and generation invalidation
src/tree/TextureSource.mjs:270
[P2] A source canceled before context loss is removed from _loadingSources, so invalidation never increments its source generation. Async text draws have no abort callback. If the source starts loading again after restoration, its old callback can pass this check, untrack the replacement load, or report a stale txError. Capture and compare the manager-wide generation too, before changing shared tracking.
- Attach context listeners in manual-rendering mode, and keep it
manual after recovery unless the loop was explicitly running.
- If stop/resume cancels a pending recovery frame, run that frame
before restarting the normal loop so contextRestored still emits.
- Cancel pending texture loads before platform teardown while keeping
the original GPU cleanup order; make worker cancellation teardown-safe.
- Requeue async texture completions instead of uploading while the
stage is suspended or failed.
- Compare manager and source generations so callbacks from sources
canceled before loss cannot affect replacement loads.
Follow-up to Copilot's 10:58 re-review: all five previously-missed lifecycle findings are fixed on the current head (47d715b): (1) context listeners are armed even with autostart:false and manual mode stays manual; (2) resume reruns a canceled pending recovery frame before the normal loop; (3) pending decodes are canceled before platform/worker teardown, with worker cancellation now teardown-safe and GPU cleanup order preserved; (4) async texture completions requeue instead of uploading when state is suspended/failed; (5) callbacks compare both source and manager generations, including sources canceled before loss. Added regression tests for each path. Validation: 91 Vitest tests passed, tsc clean, Playwright context-loss spec passed. The repeated FossID match is still the same Apache-2.0 @lightningjs/core lineage.
Invalidate render-to-texture sources before recovery rendering
src/tree/TextureManager.mjs:174
[P2] Render-to-texture result sources are not in _uploadedTextureSources: ElementTexturizer._getTextureSource() creates them directly, and replaceNativeTexture() bypasses uploads. This branch therefore leaves their pre-loss handles intact. If a consumer is rendered before its producer during recovery, it queues the dead handle; the new null-texture guard cannot reject it. Invalidate these sources through their owning texturizers before recovery rendering, or track them separately. Add coverage with a consumer rendered before its producer.
Clear previous load errors when starting texture retries
src/tree/TextureSource.mjs:263
[P2] Clear the previous load error when starting a retry. _loadError survives until a successful upload, so context loss during a pending retry cancels and queues it, but both restoration paths then discard it because source.loadError is still set. The active texture remains unloaded with no pending work. Reset the error for the new attempt and add a regression covering loss during a retry.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Port of lightning-js/renderer#930 (WebGL context-loss recovery) to the Lightning 2 architecture.
Stage:running|lost|restoring|failed|destroyed, with a generation counter so stale async work is discarded. All GPU submission is suspended while notrunning(updateFrame/renderFrameguards, upload loss detection,useProgramguard).preventDefault()onwebglcontextlost(otherwise no restore event); one recovery frame outside the normal loop before emittingcontextRestored.contextRestoreFailedis non-terminal on timeout (stayslost, default 10s viacontextRestoreTimeout,0disables) and terminal on re-init / recovery-frame failure (failed).TextureThrottler.processSomedrains the restore queue first); cheap canvas-generated sources reload immediately, image sources stream in throttled. In-flight decodes from before the loss are discarded, never marked failed. Previously evicted textures reload on demand; destroy while lost drops all queued restores.c2dmode).FWIW, L2 has no
handleLoopError/emitSafeequivalent: recovery-frame render errors claimed by the stage are reported ascontextRestoreFailedand swallowed (the event is the report); anything else rethrows to preserve fail-fast behavior.Same-source texture reloads now also force a render update — without it, fresh pixels never paint in a static scene (
renderFrameonly renders when dirty).