Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/api-reference/core/adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ create(props: DeviceProps): Promise<Device>;

### `attach()`

Attaches a device to a GPU device handle from this backend.
Attaches a device to a GPU device handle from this backend. The returned device borrows the handle.
Each successful attach call owns one logical device reference; release it with `device.destroy()`
or, when exclusively owned, `device.detach()`.

```ts
attach?(handle: unknown): Promise<Device>;
attach(handle: unknown, props?: DeviceProps): Promise<Device>;
```
10 changes: 10 additions & 0 deletions docs/api-reference/core/canvas-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ const canvasContext1 = device.createCanvasContext(...);
const canvasContext2 = device.createCanvasContext(...);
```

`CanvasContext` wrappers created through a device are managed children of that device. See the
[`Device` lifecycle](./device.md#lifecycle) for ownership, reuse, attach, and detach behavior. Call
`canvasContext.destroy()` to release one wrapper early, or call `device.destroy()` to release all
remaining context wrappers during final device teardown. Destroying a wrapper stops its DOM
observers and releases backend-specific resources, but it never removes the underlying canvas.

On WebGPU, destroying a luma.gl `CanvasContext` unconfigures its native `GPUCanvasContext`.
The destroyed luma.gl wrapper cannot be reused, but the same canvas can later be wrapped and
configured by another device.

WebGPU supports multiple `CanvasContext`s. A WebGL `Device` always has exactly one `CanvasContext` that must be created when the device is created, and a WebGL device can only render into that single canvas. This is a fundamental limitation of the WebGL API.

Because of this, the `Device` class provides a `DeviceProps.createCanvasContext` property that creates a default `CanvasContext`:
Expand Down
47 changes: 43 additions & 4 deletions docs/api-reference/core/device.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ const device = new luma.createDevice({type: 'webgl2', ...});
Attaching a `Device` to an externally created `WebGL2RenderingContext`.

```typescript
import {Device} from '@luma.gl/core';
import {luma} from '@luma.gl/core';
import {Model} from '@luma.gl/engine';
import {webgl2Adapter} from '@luma.gl/webgl';

const gl = canvas.getContext('webgl2', ...);
const device = Device.attach(gl);
const device = await luma.attachDevice(gl, {adapters: [webgl2Adapter]});

const model = new Model(device, options);
```
Expand All @@ -52,6 +53,26 @@ const {message} = await device.lost;
console.error(message);
```

## Lifecycle

A `Device` owns the luma.gl wrappers created through it, not the underlying DOM canvases.
Created devices own their backend handle; attached devices borrow an externally created handle.

| Operation | Wrapper behavior | Backend handle behavior |
| --- | --- | --- |
| `canvasContext.destroy()` | Destroys one canvas or presentation wrapper early. | Preserves the device and handle. |
| `device.destroy()` | Releases one logical device reference; the final release destroys all remaining wrappers. | Destroys an owned WebGPU handle, but preserves borrowed handles and WebGL contexts. |
| `device.detach()` | Requires exclusive ownership, destroys all remaining wrappers, and permanently invalidates the luma device. | Preserves and returns the backend handle. |

Each successful `createDevice()` or `attachDevice()` acquisition owns one logical reference.
Release it with `device.destroy()`, or use `device.detach()` instead when the device has one
exclusive owner. Reusable WebGL devices and repeated attachment can return the same `Device`
instance while retaining another logical reference; wrappers remain live until the final matching
release. `device.detach()` rejects shared devices so one owner cannot invalidate another.

Destroying or detaching a device never removes an `HTMLCanvasElement` or `OffscreenCanvas`.
A detached handle may be attached again later, but its old luma context wrappers cannot be reused.

## Types

### `DeviceProps`
Expand Down Expand Up @@ -274,10 +295,14 @@ Use the static `Device.create()` method to create classes.

### destroy()

Releases resources associated with this `Device`.
Releases one logical reference to this `Device`. The final release destroys every remaining
`CanvasContext` and `PresentationContext` wrapper created through it. Destroying a context
wrapper stops its canvas observers and releases backend-specific presentation resources, but does
not remove or destroy the underlying `HTMLCanvasElement` or `OffscreenCanvas`.

:::info
Calling `device.destroy()` releases GPU resources immediately on WebGPU. On WebGL it will not immediately release GPU resources.
Calling `device.destroy()` releases an owned WebGPU handle immediately after the final release.
Attached WebGPU handles are borrowed and are not destroyed. On WebGL it will not immediately release GPU resources.
The WebGL API does not provide a context destroy function,
instead relying on garbage collection to eventually release the resources.
:::
Expand All @@ -288,6 +313,17 @@ The application should not assume that destroying a device triggers a device los
or that the `lost` promise is resolved before any API errors are triggered by access to the destroyed device.
:::

### detach()

```typescript
detach(): unknown
```

Detaches luma.gl from this device and returns the backend handle without destroying it.
Detaching is terminal: it destroys all remaining context wrappers, clears backend association
metadata, and invalidates the luma `Device`. It requires the device to have exactly one logical
reference; call `destroy()` to release shared references first.

### createCanvasContext()

<p className="badges">
Expand All @@ -301,6 +337,9 @@ createCanvasContext(props?: CanvasContextProps): CanvasContext

Creates a new [`CanvasContext`](./canvas-context). WebGL devices can only render into the canvas they were created with.

The returned wrapper is managed by the device. The application may call `canvasContext.destroy()`
to release it early; otherwise `device.destroy()` releases it during final device teardown.

### createPresentationContext()

<p className="badges">
Expand Down
9 changes: 9 additions & 0 deletions docs/api-reference/core/luma.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,19 @@ luma.attachDevice(handle: WebGL2RenderingContext | GPUDevice | null, {adapters,
A luma.gl Device can be attached to an externally created `WebGL2RenderingContext` or `GPUDevice`.
This allows applications to use the luma.gl API to "interleave" rendering with other GPU libraries.

WebGL attachment is currently implemented. WebGPU handle attachment is reserved by this API but
is not yet implemented by `webgpuAdapter`.

- `handle` - The externally created `WebGL2RenderingContext` or `GPUDevice` that should be attached to a luma `Device`.
- `adapters` - list of `Device` backend classes. Can be omitted if `luma.registerAdapters()` has been called.
- `...deviceProps`: See [`DeviceProps`](./device.md#deviceprops) for device specific options.

The returned luma `Device` borrows the supplied handle. Each successful attachment owns one
logical device reference; release it with `device.destroy()`, or use `device.detach()` when the
device has one exclusive owner. Repeated attachments may return the same luma `Device` instance
with another retained reference. Detach destroys luma wrappers, preserves the external handle, and
returns it.

Note that while you cannot directly attach a luma.gl `Device` to a WebGL 1 `WebGLRenderingContext`, you may be able to work around it using `luma.enforceWebGL2()`.

### `luma.registerAdapters()`
Expand Down
3 changes: 3 additions & 0 deletions docs/api-reference/core/presentation-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ presentationContext.present();
- On WebGL, all `PresentationContext` instances on a device share that single default `CanvasContext`, so they must be used sequentially.
- On WebGPU, each `PresentationContext` owns its own destination `GPUCanvasContext`.
- `present()` is explicit. On WebGL it performs the copy to the destination canvas. On WebGPU it submits the frame.
- `PresentationContext` wrappers are managed by the device that created them. Applications may
call `presentationContext.destroy()` to release one early; otherwise `device.destroy()` releases
all remaining wrappers without removing their destination canvases.

## Backend Behavior

Expand Down
6 changes: 6 additions & 0 deletions docs/upgrade-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ luma.gl largely follows [SEMVER](https://semver.org) conventions. Breaking chang

**@luma.gl/core**
- WebGPU device creation now defaults to `DeviceProps.featureLevel: 'core'`. Applications that relied on luma.gl requesting every supported WebGPU feature and limit by default should pass `featureLevel: 'max'`.
- `Device.destroy()` now releases one logical device reference and, on final release, destroys all
`CanvasContext` and `PresentationContext` wrappers created through that device. Applications
that previously kept using a context wrapper after `device.destroy()` must keep the device alive
instead. Repeated reusable-device or `attachDevice()` acquisitions each require a matching
`destroy()` call; an exclusive owner can call `device.detach()` to tear down luma wrappers
while preserving and returning the backend handle.
- Render draw state is now owned by `RenderPass`. `RenderPipelineProps.bindings`,
`RenderPipelineProps.bindGroups`, `RenderPipeline.setBindings()`, and
`RenderPipeline.draw()` are deprecated compatibility APIs and will be
Expand Down
5 changes: 5 additions & 0 deletions docs/whats-new.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ Target Release Date: Q3, 2026
**@luma.gl/core**

- **HTML-in-Canvas feature detection** - `device.features.has('html-in-canvas')` reports whether the active browser and backend expose the experimental DOM-to-texture rasterization path.
- **[Device and canvas lifecycle](/docs/api-reference/core/device#lifecycle)** - Devices now manage
their `CanvasContext` and `PresentationContext` wrappers, so final teardown disconnects canvas
observers and releases backend presentation resources. Reused and repeatedly attached WebGL
devices retain logical references until the final release, and the new `device.detach()` API
preserves and returns an exclusively owned backend handle.

**@luma.gl/experimental**

Expand Down
5 changes: 4 additions & 1 deletion modules/core/src/adapter/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ export abstract class Adapter {
abstract isDeviceHandle(handle: unknown): boolean;
/** Create a new device for this backend */
abstract create(props: DeviceProps): Promise<Device>;
/** Attach a Device to a valid handle for this backend (GPUDevice, WebGL2RenderingContext etc) */
/**
* Attach a Device to a valid handle for this backend (GPUDevice, WebGL2RenderingContext etc).
* Each successful attachment owns one logical reference that must be released through the Device.
*/
abstract attach(handle: unknown, props: DeviceProps): Promise<Device>;

/**
Expand Down
1 change: 1 addition & 0 deletions modules/core/src/adapter/canvas-surface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export abstract class CanvasSurface {
if (!this.destroyed) {
this.destroyed = true;
this._stopObservers();
this.device?._unregisterCanvasSurface?.(this);
// @ts-expect-error Clear the device to make sure we don't access it after destruction.
this.device = null;
}
Expand Down
104 changes: 103 additions & 1 deletion modules/core/src/adapter/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
} from '../shadertypes/texture-types/texture-formats';
import type {CanvasContext, CanvasContextProps} from './canvas-context';
import type {PresentationContext, PresentationContextProps} from './presentation-context';
import type {CanvasSurface} from './canvas-surface';
import type {BufferProps} from './resources/buffer';
import {Buffer} from './resources/buffer';
import type {RenderPipeline, RenderPipelineProps} from './resources/render-pipeline';
Expand Down Expand Up @@ -444,6 +445,8 @@ export type DeviceProps = {

/** @deprecated Internal, Do not use directly! Use `luma.attachDevice()` to attach to pre-created contexts/devices. */
_handle?: unknown; // WebGL2RenderingContext | GPUDevice | null;
/** @internal Whether final device teardown owns destruction of the backend handle. */
_ownsHandle?: boolean;
};

type DeviceFactories = {
Expand Down Expand Up @@ -536,7 +539,8 @@ export abstract class Device {
},

// INTERNAL
_handle: undefined!
_handle: undefined!,
_ownsHandle: true
};

get [Symbol.toStringTag](): string {
Expand Down Expand Up @@ -572,6 +576,14 @@ export abstract class Device {

/** True if this device has been reused during device creation (app has multiple references) */
_reused: boolean = false;
/** Canvas and presentation wrappers created through this device. */
private _canvasSurfaces = new Set<CanvasSurface>();
/** Number of logical owners that still need to release this device. */
private _referenceCount = 1;
/** Whether the final device teardown has run. */
private _destroyed = false;
/** Whether final teardown should destroy the backend handle. */
protected readonly _ownsHandle: boolean;
/** Used by other luma.gl modules to store data on the device */
private _moduleData: Record<string, Record<string, unknown>> = {};

Expand All @@ -598,10 +610,100 @@ export abstract class Device {
constructor(props: DeviceProps) {
this.props = {...Device.defaultProps, ...props};
this.id = this.props.id || uid(this[Symbol.toStringTag].toLowerCase());
this._ownsHandle = this.props._ownsHandle;
}

/**
* Releases one logical reference to this device.
* Final release destroys managed canvas surfaces and owned backend handles.
*/
abstract destroy(): void;

/**
* Detaches luma.gl from this device and returns its backend handle without destroying it.
* @remarks Detach is terminal and requires exclusive ownership of the device reference.
*/
abstract detach(): unknown;

/**
* Registers a canvas-backed wrapper as a child of this device.
* @internal
*/
protected _registerCanvasSurface<T extends CanvasSurface>(canvasSurface: T): T {
if (this._destroyed) {
canvasSurface.destroy();
throw new Error('Device is destroyed');
}

this._canvasSurfaces.add(canvasSurface);
return canvasSurface;
}

/**
* Removes a canvas-backed wrapper that has been explicitly destroyed.
* @internal
*/
_unregisterCanvasSurface(canvasSurface: CanvasSurface): void {
this._canvasSurfaces.delete(canvasSurface);
}

/**
* Retains one logical reference to a shared device.
* @internal
*/
_retainDeviceReference(): void {
if (this._destroyed) {
throw new Error('Device is destroyed');
}

this._referenceCount++;
}

/**
* Releases one logical reference and returns true only for the final release.
* @internal
*/
protected _releaseDeviceReference(): boolean {
if (this._destroyed || this._referenceCount === 0) {
return false;
}

this._referenceCount--;
if (this._referenceCount > 0) {
return false;
}

this._destroyed = true;
return true;
}

/**
* Claims the exclusive reference required for terminal detach.
* @internal
*/
protected _detachDeviceReference(): void {
if (this._destroyed) {
throw new Error('Device is destroyed');
}
if (this._referenceCount !== 1) {
throw new Error('Device is shared');
}

this._referenceCount = 0;
this._destroyed = true;
}

/**
* Destroys canvas-backed wrappers in reverse creation order.
* @internal
*/
protected _destroyCanvasSurfaces(): void {
for (const canvasSurface of [...this._canvasSurfaces].reverse()) {
canvasSurface.destroy();
}
this._canvasSurfaces.clear();
}

// TODO - just expose the shadertypes decoders?

getVertexFormatInfo(format: VertexFormat): VertexFormatInfo {
Expand Down
Loading
Loading