Skip to content

Tracker: Device+Canvas lifecycle #2743

Description

@ibgreen-openai

Why

deck.gl has accumulated a long stream of canvas, device, resize, attach, detach, async initialization, and teardown fixes. We should address the underlying ownership and lifecycle contracts in luma.gl instead of stacking integration-specific fixes in deck.gl.

One round of bigger root-level work is in #2742.

Ownership model

Concern Owner Contract
luma CanvasContext / PresentationContext wrapper Device Wrappers created through a device are managed children and are destroyed on final device teardown unless destroyed explicitly first.
DOM canvas Caller or integrating framework luma never removes caller-supplied canvases.
Native WebGL/WebGPU handle Creating or attaching caller destroy() releases owned handles; detach() removes luma ownership and returns the reusable backend handle.
Canvas sizing Standalone luma context or external host Standalone contexts may resize their canvas; attached/shared contexts must respect the external owner’s sizing authority.

Evidence from deck.gl

Family Representative evidence What it tells us Root luma.gl action
Wrapper and observer teardown deck.gl#10355, deck.gl#10356, deck.gl#10266, deck.gl#10078, deck.gl#9817, deck.gl#9853 Dropped wrappers and late observer callbacks can retain canvases or access partially destroyed devices. Device-owned wrappers, idempotent teardown, observer shutdown, and reverse-order destruction.
Async finalize and device reuse races deck.gl#9379, deck.gl#9421, deck.gl#8463 React StrictMode and rapid create/finalize/create cycles can complete device creation after the original owner was finalized. _reuseDevices avoided crashes but previously prevented real teardown. Reference-counted reusable devices, plus follow-up cancellation or late-result disposal for async creation.
Attach, detach, and external ownership deck.gl#9950, deck.gl#10059, deck.gl#10179, deck.gl#6298, deck.gl#6340, deck.gl#4948, deck.gl#4949, deck.gl#2461, deck.gl#2465 Shared contexts can retain stale device metadata, context-loss listeners, resize behavior, or callbacks after an overlay is removed. Formal attach() / detach() lifecycle, native event-listener teardown, and explicit external-canvas ownership rules.
Live-canvas sizing and DPR synchronization deck.gl#9326, deck.gl#9803, deck.gl#9886, deck.gl#9887, deck.gl#10228, deck.gl#10281, deck.gl#10173, deck.gl#10370 Deck has repeatedly added, removed, and restored drawing-buffer synchronization because attached-canvas sizing authority is unclear. Define one resize authority per canvas and expose sufficient CanvasContext state/hooks so deck does not duplicate luma sizing logic.
Historical resource and DOM cleanup deck.gl#3039, deck.gl#3842, deck.gl#9264, deck.gl#2715, deck.gl#3019, deck.gl#8773 Cleanup bugs recur whenever creation, destruction, DOM ownership, and GPU ownership are not symmetric. Keep wrapper ownership explicit and avoid broad implicit ownership of caller DOM or unrelated GPU resources.

Phase 1 — Device-owned canvas wrappers

  • Land #2742.
  • Register every default, additional, and presentation context created through a Device.
  • Destroy remaining wrappers in reverse creation order during final device teardown.
  • Ensure explicit wrapper destruction unregisters it, stops observers/listeners/timers, releases backend-specific framebuffer wrappers, and remains idempotent.
  • Replace _reuseDevices no-op teardown with reference-counted final teardown.
  • Add terminal Device.detach() semantics for exclusively owned devices.

Phase 2 — Native WebGL event-listener lifecycle

  • Retain and remove webglcontextlost and webglcontextrestored listeners during final destroy() and terminal detach().
  • Preserve listeners during intermediate _reuseDevices releases.
  • Ensure later attach/recreate cycles install only one active listener pair.
  • Add browser tests for destroy, detach, reuse, and reattach.

Phase 3 — Async creation and finalize safety

  • Define cancellation or late-result disposal for async device creation.
  • Prevent devices that finish after owner finalization from starting observers, inserting canvases, or remaining attached.
  • Cover React StrictMode create/finalize/create timing and rapid overlay replacement.
  • Coordinate deck.gl follow-up so internally acquired devices are always released.

Phase 4 — Attached and externally owned canvas sizing

  • Define resize contracts for standalone, attached/shared-GL, overlaid, and external-framebuffer contexts.
  • Ensure attached contexts do not mutate externally owned canvases unless explicitly requested.
  • Avoid parallel resize paths fighting over autoResize, useDevicePixels, DPR, and drawing-buffer dimensions.
  • Provide enough CanvasContext state/hooks for deck.gl viewport sizing, picking, and redraw invalidation.
  • Cover DPR changes, fractional zoom, fullscreen transitions, and basemap overlays.

Phase 5 — Documentation and downstream adoption

  • Document wrapper, DOM canvas, backend-handle, and resize ownership separately.
  • Document destroy(), early wrapper destruction, _reuseDevices, attach(), and terminal detach().
  • Clarify that destroyed WebGPU wrappers are not reusable, while the canvas/native context can be wrapped again after unconfiguration.
  • Update deck.gl to call device.destroy() for internally acquired devices instead of directly destroying _canvasContext.
  • Preserve caller ownership for externally supplied devices, GL contexts, canvases, and framebuffers.

Non-goals

  • Device teardown does not remove caller-supplied DOM canvases.
  • Device ownership does not automatically include every GPU resource created by user code.
  • luma cannot clean up objects whose references are dropped without calling destroy().
  • Rendering/FBO invalidation bugs remain separate unless they expose a missing lifecycle or sizing contract.

Success criteria

  • Final device release leaves no active CanvasContext observers, timers, DPR listeners, position polling, native context event listeners, or stale context metadata.
  • _reuseDevices acquisitions require matching releases; intermediate releases preserve usability and final release performs real teardown.
  • detach() returns a reusable backend handle without leaving luma-owned wrappers or listeners behind.
  • Late async initialization cannot resurrect a finalized owner.
  • External canvases are not removed or unexpectedly resized.
  • deck.gl can rely on documented luma lifecycle and sizing contracts instead of per-overlay cleanup and resize patches.

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions