Skip to content

Commit

Permalink
Merge 984a1a8 into cb258af
Browse files Browse the repository at this point in the history
  • Loading branch information
ibgreen committed Apr 28, 2024
2 parents cb258af + 984a1a8 commit 66e5cbd
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 216 deletions.
56 changes: 44 additions & 12 deletions docs/api-reference/core/luma.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,30 +96,46 @@ const webgpuDevice = luma.createDevice({
});
```

## Methods
## Types

### `luma.registerDevices()`
### `CreateDeviceProps`

```typescript
luma.registerDevices(devices: (typeof Device)[]): void;
Properties for creating a new device

```ts
type CreateDeviceProps = DeviceProps & {
/** Selects the type of device. `best-available` uses webgpu if available, then webgl. */
type?: 'webgl' | 'webgpu' | 'unknown' | 'best-available';
/** List of device types. Will also search any pre-registered device backends */
devices?: DeviceFactory[];
}
```
Registers one or more devices (device constructors) so that they can be used
to create `Device` instances against that GPU backend. The registered device types
will be available to `luma.createDevice()` and `luma.attachDevice()` calls.
### `AttachDeviceProps`
`luma.registerDevices()` enables separation of the application code that
registers GPU backends from the application code that creates devices.
Properties for attaching an existing WebGL context or WebGPU device to a new luma Device.
```ts
export type AttachDeviceProps = DeviceProps & {
/** Externally created WebGL context or WebGPU device */
handle: WebGL2RenderingContext | GPUDevice | null;
/** List of device types. Will also search any pre-registered device backends */
devices?: DeviceFactory[];
};
```

## Methods

### `luma.createDevice()`

```typescript
luma.createDevice({type, ...DeviceProps});
luma.createDevice({type, devices, ...deviceProps}: CreateDeviceProps);
```

To create a Device instance, the application calls `luma.createDevice()`.

- `type`: `'webgl' \| 'webgpu' \| 'best-available'`
- `devices`: list of `Device` backend classes. Can be omitted if `luma.registerDevices()` has been called.

Unless a device `type` is specified a `Device` will be created using the `'best-available'` adapter.
luma.gl favors WebGPU over WebGL devices, whenever WebGPU is available.
Expand All @@ -131,14 +147,30 @@ Note: A device type is available if:
### `luma.attachDevice()`

```ts
luma.attachDevice(handle: WebGL2RenderingContext | GPUDevice, devices: unknown[]);
luma.attachDevice({handle: WebGL2RenderingContext | GPUDevice, devices, ...}: AttachDeviceProps);
```

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.

If you need to attach a luma.gl `Device` to a WebGL 1 `WebGLRenderingContext`, see `luma.enforceWebGL2()`.
- `handle` - The externally created `WebGL2RenderingContext` or `GPUDevice` that should be attached to a luma `Device`.
- `devices` - list of `Device` backend classes. Can be omitted if `luma.registerDevices()` has been called.

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.registerDevices()`

```typescript
luma.registerDevices(devices?: (typeof Device)[]): void;
```

Registers one or more devices (device constructors) so that they can be used
to create `Device` instances against that GPU backend. The registered device types
will be available to `luma.createDevice()` and `luma.attachDevice()` calls.

`luma.registerDevices()` enables separation of the application code that
registers GPU backends from the application code that creates devices,
so that device types do not have to be provided at `Device` create or attach time.

### `luma.enforceWebGL2()`

Expand Down
11 changes: 11 additions & 0 deletions modules/core/src/adapter/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,17 @@ export type DeviceProps = {
_factoryDestroyPolicy?: 'unused' | 'never';
};

/**
* Create and attach devices for a specific backend. Currently static methods on each device
*/
export interface DeviceFactory {
// new (props: DeviceProps): Device; Constructor isn't used
type: string;
isSupported(): boolean;
create(props: DeviceProps): Promise<Device>;
attach?(handle: unknown): Device;
}

/**
* WebGPU Device/WebGL context abstraction
*/
Expand Down
207 changes: 99 additions & 108 deletions modules/core/src/adapter/luma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,153 +4,146 @@

import type {Log} from '@probe.gl/log';
import type {DeviceProps} from './device';
import {Device} from './device';
import {Device, DeviceFactory} from './device';
import {StatsManager} from '../utils/stats-manager';
import {lumaStats} from '../utils/stats-manager';
import {log} from '../utils/log';

const deviceMap = new Map<string, typeof Device>();
const ERROR_MESSAGE =
'No matching device found. Ensure `@luma.gl/webgl` and/or `@luma.gl/webgpu` modules are imported.';

/** Properties for creating a new device */
export type CreateDeviceProps = DeviceProps & {
/** Selects the type of device. `best-available` uses webgpu if available, then webgl. */
type?: 'webgl' | 'webgpu' | 'unknown' | 'best-available';
devices?: any[];
/** List of device types. Will also search any pre-registered device types */
devices?: DeviceFactory[];
};

/** Properties for attaching an existing WebGL context or WebGPU device to a new luma Device */
export type AttachDeviceProps = DeviceProps & {
/** Externally created WebGL context or WebGPU device */
handle: WebGL2RenderingContext; // | GPUDevice;
devices?: any[];
handle: unknown; // WebGL2RenderingContext | GPUDevice | null;
/** List of device types. Will also search any pre-registered device types */
devices?: DeviceFactory[];
};

/**
* Entry point to the luma.gl GPU abstraction
* Register WebGPU and/or WebGL devices (controls application bundle size)
* Run-time selection of the first available Device
*/
export class luma {
export class Luma {
static defaultProps: Required<CreateDeviceProps> = {
...Device.defaultProps,
type: 'best-available',
devices: undefined!
};

/** Global stats for all devices */
static stats: StatsManager = lumaStats;
readonly stats: StatsManager = lumaStats;

/** Global log */
static log: Log = log;
readonly log: Log = log;

static registerDevices(deviceClasses: any[] /* : typeof Device */): void {
protected deviceMap = new Map<string, DeviceFactory>();

registerDevices(deviceClasses: DeviceFactory[]): void {
for (const deviceClass of deviceClasses) {
deviceMap.set(deviceClass.type, deviceClass);
this.deviceMap.set(deviceClass.type, deviceClass);
}
}

static getAvailableDevices(): string[] {
// @ts-expect-error
return Array.from(deviceMap).map(Device => Device.type);
/** Get type strings for supported Devices */
getSupportedDeviceTypes(devices: DeviceFactory[] = []): string[] {
const deviceMap = this.getDeviceMap(devices);
return Array.from(deviceMap)
.map(([, Device]) => Device)
.filter(Device => Device.isSupported?.())
.map(Device => Device.type);
}

static getSupportedDevices(): string[] {
return (
Array.from(deviceMap)
// @ts-expect-error
.filter(Device => Device.isSupported())
// @ts-expect-error
.map(Device => Device.type)
);
/** Get type strings for best available Device */
getBestAvailableDeviceType(devices: DeviceFactory[] = []): 'webgpu' | 'webgl' | null {
const deviceMap = this.getDeviceMap(devices);
if (deviceMap.get('webgpu')?.isSupported?.()) {
return 'webgpu';
}
if (deviceMap.get('webgl')?.isSupported?.()) {
return 'webgl';
}
return null;
}

static setDefaultDeviceProps(props: CreateDeviceProps): void {
Object.assign(luma.defaultProps, props);
setDefaultDeviceProps(props: CreateDeviceProps): void {
Object.assign(Luma.defaultProps, props);
}

/** Attach to an existing GPU API handle (WebGL2RenderingContext or GPUDevice). */
static async attachDevice(props: AttachDeviceProps): Promise<Device> {
const devices = getDeviceMap(props.devices) || deviceMap;

// WebGL
if (props.handle instanceof WebGL2RenderingContext) {
const WebGLDevice = devices.get('webgl') as any;
if (WebGLDevice) {
return (await WebGLDevice.attach(props.handle)) as Device;
}
}
/** Creates a device. Asynchronously. */
async createDevice(props: CreateDeviceProps = {}): Promise<Device> {
props = {...Luma.defaultProps, ...props};

// TODO - WebGPU does not yet have a stable API
// if (props.handle instanceof GPUDevice) {
// const WebGPUDevice = devices.get('webgpu') as any;
// if (WebGPUDevice) {
// return (await WebGPUDevice.attach(props.handle)) as Device;
// }
// Should be handled by attach device
// if (props.gl) {
// props.type = 'webgl';
// }

// null
if (props.handle === null) {
const UnknownDevice = devices.get('unknown') as any;
if (UnknownDevice) {
return (await UnknownDevice.attach(null)) as Device;
}
const deviceMap = this.getDeviceMap(props.devices);

let type: string = props.type || '';
if (type === 'best-available') {
type = this.getBestAvailableDeviceType(props.devices) || type;
}

throw new Error(
'Failed to attach device. Ensure `@luma.gl/webgl` and/or `@luma.gl/webgpu` modules are imported.'
);
const Device = deviceMap.get(type);
if (Device) {
return await Device.create(props);
}

throw new Error(ERROR_MESSAGE);
}

/** Creates a device. Asynchronously. */
static async createDevice(props: CreateDeviceProps = {}): Promise<Device> {
props = {...luma.defaultProps, ...props};
if (props.gl) {
props.type = 'webgl';
/** Attach to an existing GPU API handle (WebGL2RenderingContext or GPUDevice). */
async attachDevice(props: AttachDeviceProps): Promise<Device> {
const deviceMap = this.getDeviceMap(props.devices);

let deviceType;

// WebGPU sniffing
// GPUDevice type isn't generally available, we avoid enabling it in core
// eslint-disable-next-line no-undef
if (typeof GPUDevice !== 'undefined' && props.handle instanceof GPUDevice) {
deviceType = 'webgpu';
}
// WebGL
else if (
typeof WebGL2RenderingContext !== 'undefined' &&
props.handle instanceof WebGL2RenderingContext
) {
deviceType = 'webgl';
}
// NullDevice
else if (props.handle === null) {
deviceType = 'unknown';
}

const devices = getDeviceMap(props.devices) || deviceMap;

let WebGPUDevice;
let WebGLDevice;
switch (props.type) {
case 'webgpu':
WebGPUDevice = devices.get('webgpu') as any;
if (WebGPUDevice) {
return await WebGPUDevice.create(props);
}
break;

case 'webgl':
WebGLDevice = devices.get('webgl') as any;
if (WebGLDevice) {
return await WebGLDevice.create(props);
}
break;

case 'unknown':
const UnknownDevice = devices.get('unknown') as any;
if (UnknownDevice) {
return await UnknownDevice.create(props);
}
break;

case 'best-available':
WebGPUDevice = devices.get('webgpu') as any;
if (WebGPUDevice?.isSupported?.()) {
return await WebGPUDevice.create(props);
}
WebGLDevice = devices.get('webgl') as any;
if (WebGLDevice?.isSupported?.()) {
return await WebGLDevice.create(props);
}
break;
if (deviceType) {
const Device = deviceMap.get(deviceType);
const device = Device?.attach?.(props.handle);
if (device) {
return device;
}
}
throw new Error(
'No matching device found. Ensure `@luma.gl/webgl` and/or `@luma.gl/webgpu` modules are imported.'
);

throw new Error(ERROR_MESSAGE);
}

static enforceWebGL2(enforce: boolean = true): void {
/**
* Override `HTMLCanvasContext.getCanvas()` to always create WebGL2 contexts.
* Used when attaching luma to a context from an external library does not support creating WebGL2 contexts.
* (luma can only attach to WebGL2 contexts).
*/
enforceWebGL2(enforce: boolean = true): void {
const prototype = HTMLCanvasElement.prototype as any;
if (!enforce && prototype.originalGetContext) {
// Reset the original getContext function
Expand All @@ -166,25 +159,23 @@ export class luma {
prototype.getContext = function (contextId: string, options?: WebGLContextAttributes) {
// Attempt to force WebGL2 for all WebGL1 contexts
if (contextId === 'webgl' || contextId === 'experimental-webgl') {
return this.originalGetContext('webgl2', options);
const context = this.originalGetContext('webgl2', options);
return context;
}
// For any other type, return the original context
return this.originalGetContext(contextId, options);
};
}
}

/** Convert a list of devices to a map */
function getDeviceMap(
deviceClasses?: any[] /* : typeof Device */
): Map<string, typeof Device> | null {
if (!deviceClasses || deviceClasses?.length === 0) {
return null;
}
const map = new Map<string, typeof Device>();
for (const deviceClass of deviceClasses) {
// assert(deviceClass.type && deviceClass.isSupported && deviceClass.create);
map.set(deviceClass.type, deviceClass);
/** Convert a list of devices to a map */
protected getDeviceMap(deviceClasses: DeviceFactory[] = []): Map<string, DeviceFactory> {
const map = new Map(this.deviceMap);
for (const deviceClass of deviceClasses) {
map.set(deviceClass.type, deviceClass);
}
return map;
}
return map;
}

/** Singleton */
export const luma = new Luma();

0 comments on commit 66e5cbd

Please sign in to comment.