@luma.gl/core
Version:
The luma.gl core Device API
240 lines (198 loc) • 7.72 kB
text/typescript
// luma.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import type {Log} from '@probe.gl/log';
import type {DeviceProps} from './device';
import {Device} from './device';
import {Adapter} from './adapter';
import {StatsManager, lumaStats} from '../utils/stats-manager';
import {log} from '../utils/log';
declare global {
// eslint-disable-next-line no-var
var luma: Luma;
}
const STARTUP_MESSAGE = 'set luma.log.level=1 (or higher) to trace rendering';
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 = {
/** Selects the type of device. `best-available` uses webgpu if available, then webgl. */
type?: 'webgl' | 'webgpu' | 'null' | 'unknown' | 'best-available';
/** List of adapters. Will also search any pre-registered adapters */
adapters?: Adapter[];
/**
* Whether to wait for page to be loaded so that CanvasContext's can access the DOM.
* The browser only supports one 'load' event listener so it may be necessary for the application to set this to false to avoid conflicts.
*/
waitForPageLoad?: boolean;
} & DeviceProps;
/** Properties for attaching an existing WebGL context or WebGPU device to a new luma Device */
export type AttachDeviceProps = {
/** List of adapters. Will also search any pre-registered adapters */
adapters?: Adapter[];
} & DeviceProps;
/**
* Entry point to the luma.gl GPU abstraction
* Register WebGPU and/or WebGL adapters (controls application bundle size)
* Run-time selection of the first available Device
*/
export class Luma {
static defaultProps: Required<CreateDeviceProps> = {
...Device.defaultProps,
type: 'best-available',
adapters: undefined!,
waitForPageLoad: true
};
/** Global stats for all devices */
readonly stats: StatsManager = lumaStats;
/**
* Global log
*
* Assign luma.log.level in console to control logging: \
* 0: none, 1: minimal, 2: verbose, 3: attribute/uniforms, 4: gl logs
* luma.log.break[], set to gl funcs, luma.log.profile[] set to model names`;
*/
readonly log: Log = log;
/** Version of luma.gl */
readonly VERSION: string =
// Version detection using build plugin
// @ts-expect-error no-undef
typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'running from source';
spector: unknown;
protected preregisteredAdapters = new Map<string, Adapter>();
constructor() {
if (globalThis.luma) {
if (globalThis.luma.VERSION !== this.VERSION) {
log.error(`Found luma.gl ${globalThis.luma.VERSION} while initialzing ${this.VERSION}`)();
log.error(`'yarn why @luma.gl/core' can help identify the source of the conflict`)();
throw new Error(`luma.gl - multiple versions detected: see console log`);
}
log.error('This version of luma.gl has already been initialized')();
}
log.log(1, `${this.VERSION} - ${STARTUP_MESSAGE}`)();
globalThis.luma = this;
}
/** Creates a device. Asynchronously. */
async createDevice(props_: CreateDeviceProps = {}): Promise<Device> {
const props: Required<CreateDeviceProps> = {...Luma.defaultProps, ...props_};
const adapter = this.selectAdapter(props.type, props.adapters);
if (!adapter) {
throw new Error(ERROR_MESSAGE);
}
// Wait for page to load so that CanvasContext's can access the DOM.
if (props.waitForPageLoad) {
await adapter.pageLoaded;
}
return await adapter.create(props);
}
/**
* Attach to an existing GPU API handle (WebGL2RenderingContext or GPUDevice).
* @param handle Externally created WebGL context or WebGPU device
*/
async attachDevice(handle: unknown, props: AttachDeviceProps): Promise<Device> {
const type = this._getTypeFromHandle(handle, props.adapters);
const adapter = type && this.selectAdapter(type, props.adapters);
if (!adapter) {
throw new Error(ERROR_MESSAGE);
}
return await adapter?.attach?.(handle, props);
}
/**
* Global adapter registration.
* @deprecated Use props.adapters instead
*/
registerAdapters(adapters: Adapter[]): void {
for (const deviceClass of adapters) {
this.preregisteredAdapters.set(deviceClass.type, deviceClass);
}
}
/** Get type strings for supported Devices */
getSupportedAdapters(adapters: Adapter[] = []): string[] {
const adapterMap = this._getAdapterMap(adapters);
return Array.from(adapterMap)
.map(([, adapter]) => adapter)
.filter(adapter => adapter.isSupported?.())
.map(adapter => adapter.type);
}
/** Get type strings for best available Device */
getBestAvailableAdapterType(adapters: Adapter[] = []): 'webgpu' | 'webgl' | 'null' | null {
const KNOWN_ADAPTERS: ('webgpu' | 'webgl' | 'null')[] = ['webgpu', 'webgl', 'null'];
const adapterMap = this._getAdapterMap(adapters);
for (const type of KNOWN_ADAPTERS) {
if (adapterMap.get(type)?.isSupported?.()) {
return type;
}
}
return null;
}
/** Select adapter of type from registered adapters */
selectAdapter(type: string, adapters: Adapter[] = []): Adapter | null {
let selectedType: string | null = type;
if (type === 'best-available') {
selectedType = this.getBestAvailableAdapterType(adapters);
}
const adapterMap = this._getAdapterMap(adapters);
return (selectedType && adapterMap.get(selectedType)) || null;
}
/**
* Override `HTMLCanvasContext.getCanvas()` to always create WebGL2 contexts with additional WebGL1 compatibility.
* Useful when attaching luma to a context from an external library does not support creating WebGL2 contexts.
*/
enforceWebGL2(enforce: boolean = true, adapters: Adapter[] = []): void {
const adapterMap = this._getAdapterMap(adapters);
const webgl2Adapter = adapterMap.get('webgl');
if (!webgl2Adapter) {
log.warn('enforceWebGL2: webgl adapter not found')();
}
(webgl2Adapter as any)?.enforceWebGL2?.(enforce);
}
// DEPRECATED
/** @deprecated */
setDefaultDeviceProps(props: CreateDeviceProps): void {
Object.assign(Luma.defaultProps, props);
}
// HELPERS
/** Convert a list of adapters to a map */
protected _getAdapterMap(adapters: Adapter[] = []): Map<string, Adapter> {
const map = new Map(this.preregisteredAdapters);
for (const adapter of adapters) {
map.set(adapter.type, adapter);
}
return map;
}
/** Get type of a handle (for attachDevice) */
protected _getTypeFromHandle(
handle: unknown,
adapters: Adapter[] = []
): 'webgpu' | 'webgl' | 'null' | null {
// TODO - delegate handle identification to adapters
// WebGL
if (handle instanceof WebGL2RenderingContext) {
return 'webgl';
}
if (typeof GPUDevice !== 'undefined' && handle instanceof GPUDevice) {
return 'webgpu';
}
// TODO - WebGPU does not yet seem to have a stable in-browser API, so we "sniff" for members instead
if ((handle as any)?.queue) {
return 'webgpu';
}
// null
if (handle === null) {
return 'null';
}
if (handle instanceof WebGLRenderingContext) {
log.warn('WebGL1 is not supported', handle)();
} else {
log.warn('Unknown handle type', handle)();
}
return null;
}
}
/**
* Entry point to the luma.gl GPU abstraction
* Register WebGPU and/or WebGL adapters (controls application bundle size)
* Run-time selection of the first available Device
*/
// biome-ignore lint/suspicious/noRedeclare: the exported singleton intentionally mirrors the global debug handle.
export const luma = new Luma();