Skip to content

Commit

Permalink
chore(core): Convert luma object to singleton
Browse files Browse the repository at this point in the history
  • Loading branch information
ibgreen committed Apr 28, 2024
1 parent cb258af commit 573771d
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 204 deletions.
11 changes: 11 additions & 0 deletions docs/api-reference/core/luma.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ const webgpuDevice = luma.createDevice({
});
```

## Types

### `CreateDeviceProps`

```ts
type CreateDeviceProps = DeviceProps & {
/** Selects the type of device. `best-available` uses webgpu if available, then webgl. */
type?: 'webgl' | 'webgpu' | 'unknown' | 'best-available';
}
```
## Methods
### `luma.registerDevices()`
Expand Down
9 changes: 9 additions & 0 deletions modules/core/src/adapter/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,15 @@ export type DeviceProps = {
_factoryDestroyPolicy?: 'unused' | 'never';
};

/** Used by Device registration */
export interface DeviceConstructor {
// 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
204 changes: 96 additions & 108 deletions modules/core/src/adapter/luma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,153 +4,143 @@

import type {Log} from '@probe.gl/log';
import type {DeviceProps} from './device';
import {Device} from './device';
import {Device, DeviceConstructor} 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[];
devices?: DeviceConstructor[];
};

/** 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;
devices?: DeviceConstructor[];
};

/**
* 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, DeviceConstructor>();

registerDevices(deviceClasses: DeviceConstructor[]): 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: DeviceConstructor[] = []): 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: DeviceConstructor[] = []): '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
// eslint-disable-next-line GPUDevice type isn't generally available, we avoid enabling it in core

Check failure on line 111 in modules/core/src/adapter/luma.ts

View workflow job for this annotation

GitHub Actions / test (20)

Definition for rule 'GPUDevice type isn't generally available' was not found

Check failure on line 111 in modules/core/src/adapter/luma.ts

View workflow job for this annotation

GitHub Actions / test (20)

Definition for rule 'we avoid enabling it in core' was not found
if (typeof GPUDevice !== 'undefined' && props.handle instanceof GPUDevice) {

Check warning on line 112 in modules/core/src/adapter/luma.ts

View workflow job for this annotation

GitHub Actions / test (20)

'GPUDevice' is not defined
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 +156,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: DeviceConstructor[] = []): Map<string, DeviceConstructor> {
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();
14 changes: 14 additions & 0 deletions modules/core/test/adapter/luma.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ test('luma#registerDevices', async t => {
t.end();
});

test('luma#getSupportedDevices', async t => {
luma.registerDevices([NullDevice]);
const types = luma.getSupportedDeviceTypes();
t.ok(types.includes('unknown'), 'null device is supported');
});

test('luma#getBestAvailableDeviceType', async t => {
luma.registerDevices([NullDevice]);
// Somewhat dummy test, as tests rely on test utils registering webgl and webgpu devices
// But they might not be supported on all devices.
const types = luma.getBestAvailableDeviceType();
t.ok(typeof types === 'string', 'doesnt crash');
});

// To suppress @typescript-eslint/unbound-method
interface TestHTMLCanvasElement {
getContext: (contextId: any, options?: unknown) => string;
Expand Down
4 changes: 2 additions & 2 deletions modules/test-utils/src/create-test-device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export function createTestContext(opts: Record<string, any> = {}): WebGL2Renderi
export function createTestDevice(props: DeviceProps = {}): WebGLDevice | null {
try {
props = {...CONTEXT_DEFAULTS, ...props, debug: true};
// We dont use luma.createDevice since this tests current expect this context to be created synchronously
return new WebGLDevice(props);
// TODO - We dont use luma.createDevice since this function currently expect the context to be created synchronously
return WebGLDevice.createSync(props);
} catch (error) {
// eslint-disable-next-line no-console
console.error(`Failed to created device '${props.id}': ${(error as Error).message}`);
Expand Down
12 changes: 9 additions & 3 deletions modules/webgl/src/adapter/webgl-device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,14 @@ export class WebGLDevice extends Device {

log.probe(LOG_LEVEL + 1, 'DOM is loaded')();

const device = WebGLDevice.createSync(props);
log.groupEnd(LOG_LEVEL)();

return device;
}

/** Create or attach device synchronously. Not supported for all devices. */
static createSync(props: DeviceProps = {}): WebGLDevice {
// @ts-expect-error
if (props.gl?.device) {
log.warn('reattaching existing device')();
Expand All @@ -173,16 +181,14 @@ ${device.info.vendor}, ${device.info.renderer} for canvas: ${device.canvasContex
log.probe(LOG_LEVEL, message)();
log.table(LOG_LEVEL, device.info)();

log.groupEnd(LOG_LEVEL)();

return device;
}

//
// Public API
//

constructor(props: DeviceProps) {
protected constructor(props: DeviceProps) {
super({...props, id: props.id || 'webgl-device'});

// If attaching to an already attached context, return the attached device
Expand Down
8 changes: 4 additions & 4 deletions modules/webgpu/src/adapter/webgpu-device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export class WebGPUDevice extends Device {
log.probe(1, 'DOM is loaded')();
}

const device = new WebGPUDevice(gpuDevice, adapter, adapterInfo, props);
const device = new WebGPUDevice(props, gpuDevice, adapter, adapterInfo);

log.probe(
1,
Expand All @@ -139,11 +139,11 @@ export class WebGPUDevice extends Device {
return device;
}

constructor(
protected constructor(
props: DeviceProps,
device: GPUDevice,
adapter: GPUAdapter,
adapterInfo: GPUAdapterInfo,
props: DeviceProps
adapterInfo: GPUAdapterInfo
) {
super({...props, id: props.id || 'webgpu-device'});
this.handle = device;
Expand Down
Loading

0 comments on commit 573771d

Please sign in to comment.