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 94e45c2 commit d216f1b
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 83 deletions.
150 changes: 76 additions & 74 deletions modules/core/src/adapter/luma.ts
Expand Up @@ -12,19 +12,19 @@ import {log} from '../utils/log';
const ERROR_MESSAGE =
'No matching device found. Ensure `@luma.gl/webgl` and/or `@luma.gl/webgpu` modules are imported.';

const deviceMap = new Map<string, DeviceFactory>();

/** 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';
/** 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;
handle: unknown; // WebGL2RenderingContext | GPUDevice | null;
/** List of device types. Will also search any pre-registered device types */
devices?: DeviceFactory[];
};

Expand All @@ -33,43 +33,39 @@ export type AttachDeviceProps = DeviceProps & {
* 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;

protected deviceMap = new Map<string, DeviceFactory>();

static registerDevices(deviceClasses: DeviceFactory[]): void {
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);
}

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 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);
}

/** Get type strings for best available Device */
static getBestAvailableDeviceType(devices: DeviceFactory[] = []): 'webgpu' | 'webgl' | null {
const deviceMap = getDeviceMap(devices);
getBestAvailableDeviceType(devices: DeviceFactory[] = []): 'webgpu' | 'webgl' | null {
const deviceMap = this.getDeviceMap(devices);
if (deviceMap.get('webgpu')?.isSupported?.()) {
return 'webgpu';
}
Expand All @@ -79,61 +75,64 @@ export class luma {
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 Device = devices.get('webgl');
const device = Device?.attach?.(null);
if (device) {
return 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 Device = devices.get('unknown');
const device = Device?.attach?.(null);
if (device) {
return device;
}
const deviceMap = this.getDeviceMap(props.devices);

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

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);

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

let type: string = props.type || '';
if (type === 'best-available') {
type = luma.getBestAvailableDeviceType(props.devices) || type;
// 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 Device = devices.get(type);
const device = await Device?.create?.(props);
if (device) {
return device;
if (deviceType) {
const Device = deviceMap.get(deviceType);
const device = Device?.attach?.(props.handle);
if (device) {
return device;
}
}

throw new Error(ERROR_MESSAGE);
Expand All @@ -144,7 +143,7 @@ export class luma {
* Used when attaching luma to a context from an external library does not support creating WebGL2 contexts.
* (luma can only attach to WebGL2 contexts).
*/
static enforceWebGL2(enforce: boolean = true): void {
enforceWebGL2(enforce: boolean = true): void {
const prototype = HTMLCanvasElement.prototype as any;
if (!enforce && prototype.originalGetContext) {
// Reset the original getContext function
Expand All @@ -160,20 +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: DeviceFactory[] = []): Map<string, DeviceFactory> {
const map = new Map<string, DeviceFactory>(deviceMap);
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();
14 changes: 14 additions & 0 deletions modules/core/test/adapter/luma.spec.ts
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
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
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
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

0 comments on commit d216f1b

Please sign in to comment.