From 055d0b2d22f16ea603440f733ae87dc675c3d16d Mon Sep 17 00:00:00 2001 From: Ib Green Date: Thu, 7 Mar 2024 14:38:48 -0500 Subject: [PATCH] feat(core): luma.attachDevice() --- docs/api-reference/core/luma.md | 82 ++++++++------ docs/whats-new.md | 4 + modules/core/src/adapter/luma.ts | 106 ++++++++++++++---- modules/core/test/adapter/luma.spec.ts | 15 +++ modules/core/test/index.ts | 1 + .../src/null-device/null-device-info.ts | 10 +- .../test-utils/src/null-device/null-device.ts | 4 + yarn.lock | 15 +-- 8 files changed, 160 insertions(+), 77 deletions(-) create mode 100644 modules/core/test/adapter/luma.spec.ts diff --git a/docs/api-reference/core/luma.md b/docs/api-reference/core/luma.md index e39995b29a..e8cc92b4b3 100644 --- a/docs/api-reference/core/luma.md +++ b/docs/api-reference/core/luma.md @@ -8,14 +8,7 @@ using the registered backends. The returned [`Device`](/docs/api-reference/core/device) instances provides luma.gl applications with further access to the GPU. -## luma.registerDevices - -```typescript -luma.registerDevices(devices: Device[]): void; -``` - -Registers one or more devices so that they can be used to create `Device` instances against -that GPU backend. +## Device Registration ```typescript import {luma} from '@luma.gl/core'; @@ -27,26 +20,10 @@ luma.registerDevices([WebGLDevice, WebGPUDevice]); It is possible to register more than one device to create an application that can work in both WebGL and WebGPU environments. -``` The `@luma.gl/core` module defines abstract API interfaces such as `Device`, `Buffer` etc and is not usable on its own. One or more GPU backend modules must be also be imported from a corresponding GPU API backend module (`@luma.gl/webgl` and/or `@luma.gl/webgpu`) and then registered with luma.gl. - -## luma.createDevice - -```typescript -luma.createDevice({type, ...DeviceProps}); -``` - -To enable of this, the application create a `Device` using the `'best-available'` adapter. - -luma.gl favors WebGPU over WebGL devices, whenever WebGPU is available. - -:::note -At least one backend must be imported and registered with `luma.registerDevices()` for `luma.createDevice()` calls to succeed. -::: - ## Usage Create a WebGL2 context, auto creating a canvas @@ -69,12 +46,9 @@ luma.registerDevices([WebGLDevice]); const webgpuDevice = luma.createDevice({type: 'webgl', canvas: ...}); ``` - - ## Registering Device Backends - -To create a WebGPU device: +Install device modules ```sh yarn add @luma.gl/core @@ -82,6 +56,8 @@ yarn add @luma.gl/webgl yarn add @luma.gl/webgpu ``` +To create a WebGPU device: + ```typescript import {luma} from '@luma.gl/core'; import {WebGPUDevice} from '@luma.gl/webgpu'; @@ -90,12 +66,7 @@ luma.registerDevices([WebGPUDevice]); const device = await luma.createDevice({type: 'webgpu', canvas: ...}); ``` - -```sh -yarn add @luma.gl/core -yarn add @luma.gl/webgl -yarn add @luma.gl/webgpu -``` +Pre-register devices ```typescript import {luma} from '@luma.gl/core'; @@ -103,6 +74,47 @@ import {WebGLDevice} from '@luma.gl/webgl'; import {WebGPUDevice} from '@luma.gl/webgpu'; luma.registerDevices([WebGLDevice, WebGPUDevice]); - const webgpuDevice = luma.createDevice({type: 'best-available', canvas: ...}); ``` + +Provide devices to createDevice + +```typescript +const webgpuDevice = luma.createDevice({ + type: 'best-available', + canvas: ..., + devices: [WebGLDevice, WebGPUDevice] +}); +``` + +## Methods + +### `luma.registerDevices()` + +```typescript +luma.registerDevices(devices: Device[]): void; +``` + +Registers one or more devices so that they can be used to create `Device` instances against +that GPU backend. They will be available to `luma.createDevice()` and `luma.attachDevice()` calls. +Enables separation of the code that registers backends from the code that creates devices. + +### `luma.createDevice()` + +```typescript +luma.createDevice({type, ...DeviceProps}); +``` + +To enable of this, the application create a `Device` using the `'best-available'` adapter. + +luma.gl favors WebGPU over WebGL devices, whenever WebGPU is available. + +### `luma.attachDevice()` + +```ts +luma.attachDevice(handle: WebGLRenderingContext | GPUDevice, devices: unknown[]) +``` + +## Remarks + +- At least one backend must be imported and registered with `luma.registerDevices()` for `luma.createDevice()` or `luma.attachDevice()` calls to succeed (unless `Device` implementations are supplied to those calls). diff --git a/docs/whats-new.md b/docs/whats-new.md index 10c8684ff9..7ef21f59ba 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -8,6 +8,10 @@ Target Date: Q2 2024 - Production quality (non-experimental) WebGPU backend. +**@luma.gl/core** + +- new [`luma.attachDevice()`](/docs/api-reference/core/luma#attachdevice) API - A `Device` can now be [attached to `WebGL2RenderingContext` or `GPUDevice`] without importing `WebGLDevice`. + ## Version 9.0 Target Date: Feb 2024 diff --git a/modules/core/src/adapter/luma.ts b/modules/core/src/adapter/luma.ts index bf927eb4c6..a38251c400 100644 --- a/modules/core/src/adapter/luma.ts +++ b/modules/core/src/adapter/luma.ts @@ -9,11 +9,20 @@ import {StatsManager} from '../utils/stats-manager'; import {lumaStats} from '../utils/stats-manager'; import {log} from '../utils/log'; -const deviceList = new Map(); +let deviceMap = new Map(); +/** Properties for creating a new device */ export type CreateDeviceProps = DeviceProps & { - /** Select type of device */ - type?: 'webgl' | 'webgpu' | 'best-available'; + /** Selects the type of device. `best-available` uses webgpu if available, then webgl. */ + type?: 'webgl' | 'webgpu' | 'unknown' | 'best-available'; + devices?: any[]; +}; + +/** 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[]; }; /** @@ -24,7 +33,8 @@ export type CreateDeviceProps = DeviceProps & { export class luma { static defaultProps: Required = { ...Device.defaultProps, - type: 'best-available' + type: 'best-available', + devices: undefined! }; /** Global stats for all devices */ @@ -34,20 +44,17 @@ export class luma { static log: Log = log; static registerDevices(deviceClasses: any[] /* : typeof Device */): void { - for (const deviceClass of deviceClasses) { - // assert(deviceClass.type && deviceClass.isSupported && deviceClass.create); - deviceList.set(deviceClass.type, deviceClass); - } + deviceMap = getDeviceMap(deviceClasses); } static getAvailableDevices(): string[] { // @ts-expect-error - return Array.from(deviceList).map(Device => Device.type); + return Array.from(deviceMap).map(Device => Device.type); } static getSupportedDevices(): string[] { return ( - Array.from(deviceList) + Array.from(deviceMap) // @ts-expect-error .filter(Device => Device.isSupported()) // @ts-expect-error @@ -59,6 +66,39 @@ export class luma { Object.assign(luma.defaultProps, props); } + /** Attach to an existing GPU API handle (WebGL2RenderingContext or GPUDevice). */ + static async attachDevice(props: AttachDeviceProps): Promise { + 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; + } + } + + // 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; + // } + // } + + // null + if (props.handle === null) { + const UnknownDevice = devices.get('unknown') as any; + if (UnknownDevice) { + return (await UnknownDevice.attach(null)) as Device; + } + } + + throw new Error( + 'Failed to attach device. Ensure `@luma.gl/webgl` and/or `@luma.gl/webgpu` modules are imported.' + ); + } + /** Creates a device. Asynchronously. */ static async createDevice(props: CreateDeviceProps = {}): Promise { props = {...luma.defaultProps, ...props}; @@ -66,28 +106,38 @@ export class luma { props.type = 'webgl'; } - let DeviceClass: any; + const devices = getDeviceMap(props.devices) || deviceMap; + switch (props.type) { case 'webgpu': - DeviceClass = deviceList.get('webgpu'); - if (DeviceClass) { - return await DeviceClass.create(props); + let WebGPUDevice = devices.get('webgpu') as any; + if (WebGPUDevice) { + return await WebGPUDevice.create(props); } break; + case 'webgl': - DeviceClass = deviceList.get('webgl'); - if (DeviceClass) { - return await DeviceClass.create(props); + let 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': - DeviceClass = deviceList.get('webgpu'); - if (DeviceClass && DeviceClass.isSupported()) { - return await DeviceClass.create(props); + WebGPUDevice = devices.get('webgpu') as any; + if (WebGPUDevice?.isSupported?.()) { + return await WebGPUDevice.create(props); } - DeviceClass = deviceList.get('webgl'); - if (DeviceClass && DeviceClass.isSupported()) { - return await DeviceClass.create(props); + WebGLDevice = devices.get('webgl'); + if (WebGLDevice?.isSupported?.()) { + return await WebGLDevice.create(props); } break; } @@ -96,3 +146,13 @@ export class luma { ); } } + +/** Convert a list of devices to a map */ +function getDeviceMap(deviceClasses: any[] /* : typeof Device */): Map { + const map = new Map(); + for (const deviceClass of deviceClasses) { + // assert(deviceClass.type && deviceClass.isSupported && deviceClass.create); + map.set(deviceClass.type, deviceClass); + } + return map; +} diff --git a/modules/core/test/adapter/luma.spec.ts b/modules/core/test/adapter/luma.spec.ts new file mode 100644 index 0000000000..f8ecfbae73 --- /dev/null +++ b/modules/core/test/adapter/luma.spec.ts @@ -0,0 +1,15 @@ +// luma.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import test from 'tape-promise/tape'; +import {NullDevice} from '@luma.gl/test-utils'; +import {luma} from '@luma.gl/core'; + +test('luma#attachDevice', async t => { + const device = await luma.attachDevice({handle: null, devices: [NullDevice]}); + t.equal(device.type, 'unknown', 'info.vendor ok'); + t.equal(device.info.vendor, 'no one', 'info.vendor ok'); + t.equal(device.info.renderer, 'none', 'info.renderer ok'); + t.end(); +}); diff --git a/modules/core/test/index.ts b/modules/core/test/index.ts index 561513222d..2680ba7a80 100644 --- a/modules/core/test/index.ts +++ b/modules/core/test/index.ts @@ -25,6 +25,7 @@ import './adapter/helpers/parse-shader-compiler-log.spec'; import './adapter/device.spec'; import './adapter/canvas-context.spec'; +import './adapter/luma.spec'; // Resources import './adapter/texture-formats.spec'; diff --git a/modules/test-utils/src/null-device/null-device-info.ts b/modules/test-utils/src/null-device/null-device-info.ts index 3d10a73704..ce54212877 100644 --- a/modules/test-utils/src/null-device/null-device-info.ts +++ b/modules/test-utils/src/null-device/null-device-info.ts @@ -4,14 +4,14 @@ import type {DeviceInfo} from '@luma.gl/core'; -export const NullDeviceInfo: DeviceInfo = { - type: 'webgl', +export const NullDeviceInfo = { + type: 'unknown', gpu: 'software', gpuType: 'unknown', gpuBackend: 'unknown', - vendor: '', + vendor: 'no one', renderer: 'none', version: '1.0', - shadingLanguage: 'glsl' as const, + shadingLanguage: 'glsl', shadingLanguageVersion: 300 -} as const; +} as const satisfies DeviceInfo; diff --git a/modules/test-utils/src/null-device/null-device.ts b/modules/test-utils/src/null-device/null-device.ts index 2594af25c5..db2cd7f305 100644 --- a/modules/test-utils/src/null-device/null-device.ts +++ b/modules/test-utils/src/null-device/null-device.ts @@ -59,6 +59,10 @@ export class NullDevice extends Device { readonly canvasContext: NullCanvasContext; readonly lost: Promise<{reason: 'destroyed'; message: string}>; + static attach(handle: null): NullDevice { + return new NullDevice({}); + } + static async create(props: DeviceProps = {}): Promise { // Wait for page to load: if canvas is a string we need to query the DOM for the canvas element. // We only wait when props.canvas is string to avoids setting the global page onload callback unless necessary. diff --git a/yarn.lock b/yarn.lock index 96c50c307a..5edd13cbae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3284,19 +3284,6 @@ __metadata: languageName: unknown linkType: soft -"@luma.gl/core-tests@workspace:modules/core-tests": - version: 0.0.0-use.local - resolution: "@luma.gl/core-tests@workspace:modules/core-tests" - dependencies: - "@luma.gl/core": "npm:9.0.0-beta.7" - "@luma.gl/engine": "npm:9.0.0-beta.7" - "@luma.gl/shadertools": "npm:9.0.0-beta.7" - "@luma.gl/test-utils": "npm:9.0.0-beta.7" - "@luma.gl/webgl": "npm:9.0.0-beta.7" - "@luma.gl/webgpu": "npm:9.0.0-beta.7" - languageName: unknown - linkType: soft - "@luma.gl/core@npm:9.0.0-beta.7, @luma.gl/core@workspace:modules/core": version: 0.0.0-use.local resolution: "@luma.gl/core@workspace:modules/core" @@ -3347,7 +3334,7 @@ __metadata: languageName: unknown linkType: soft -"@luma.gl/test-utils@npm:9.0.0-beta.7, @luma.gl/test-utils@workspace:modules/test-utils": +"@luma.gl/test-utils@workspace:modules/test-utils": version: 0.0.0-use.local resolution: "@luma.gl/test-utils@workspace:modules/test-utils" dependencies: