From b5eb69a8bcf8225c653877d2c499ad2471d5f2a0 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Fri, 5 Apr 2024 11:33:12 -0700 Subject: [PATCH] add(core) enforceWebGL2 (#2067) --- .ocularrc.js | 2 +- docs/api-reference/core/luma.md | 74 ++++++++++++++++++++------ modules/core/src/adapter/luma.ts | 23 ++++++++ modules/core/test/adapter/luma.spec.ts | 48 +++++++++++++++++ 4 files changed, 130 insertions(+), 17 deletions(-) diff --git a/.ocularrc.js b/.ocularrc.js index 4e9adaf1a6..b03bb1ff00 100644 --- a/.ocularrc.js +++ b/.ocularrc.js @@ -13,7 +13,7 @@ const config = { lint: { paths: ['modules', 'docs', 'test', 'examples'], - extensions: ['js', 'ts'] + extensions: ['js', 'ts', 'jsx', 'tsx'] }, typescript: { diff --git a/docs/api-reference/core/luma.md b/docs/api-reference/core/luma.md index e8cc92b4b3..7128062d4f 100644 --- a/docs/api-reference/core/luma.md +++ b/docs/api-reference/core/luma.md @@ -5,28 +5,37 @@ The [`luma`](/docs/api-reference/core/luma) namespace provides luma.gl applicati with the ability to register GPU Device backends and create `Device` class instances using the registered backends. -The returned [`Device`](/docs/api-reference/core/device) instances provides luma.gl applications -with further access to the GPU. +The returned [`Device`](/docs/api-reference/core/device) instances provides +luma.gl applications with a complete GPU API. ## Device Registration +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 imported from a corresponding +GPU API backend module (`@luma.gl/webgl` and/or `@luma.gl/webgpu`) and then registered with luma.gl. + +## Usage + +To register a device backend, import the corresponding device backend module and then call `luma.registerDevices()` + ```typescript import {luma} from '@luma.gl/core'; import {WebGLDevice} from '@luma.gl/webgl'; -import {WebGPUDevice} from '@luma.gl/webgl'; -luma.registerDevices([WebGLDevice, WebGPUDevice]); +luma.registerDevices([WebGLDevice]); ``` 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. - -## Usage +```typescript +import {luma} from '@luma.gl/core'; +import {WebGLDevice} from '@luma.gl/webgl'; +import {WebGPUDevice} from '@luma.gl/webgl'; +luma.registerDevices([WebGLDevice, WebGPUDevice]); +``` -Create a WebGL2 context, auto creating a canvas +Register the WebGL backend, then create a WebGL2 context, auto creating a canvas ```typescript import {luma} from '@luma.gl/core'; @@ -92,12 +101,15 @@ const webgpuDevice = luma.createDevice({ ### `luma.registerDevices()` ```typescript -luma.registerDevices(devices: Device[]): void; +luma.registerDevices(devices: (typeof 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. +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. ### `luma.createDevice()` @@ -105,16 +117,46 @@ Enables separation of the code that registers backends from the code that create luma.createDevice({type, ...DeviceProps}); ``` -To enable of this, the application create a `Device` using the `'best-available'` adapter. +To create a Device instance, the application calls `luma.createDevice()`. + +- `type`: `'webgl' \| 'webgpu' \| 'best-available'` +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. +Note: A device type is available if: +1. The backend module has been registered +2. The browser supports that GPU API + ### `luma.attachDevice()` ```ts -luma.attachDevice(handle: WebGLRenderingContext | GPUDevice, devices: unknown[]) +luma.attachDevice(handle: WebGL2RenderingContext | GPUDevice, devices: unknown[]); +``` + +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()`. + + +### `luma.enforceWebGL2()` + +```ts +luma.enforceWebGL2(enforce: boolean = true); ``` +Overrides `HTMLCanvasElement.prototype.getContext()` to return WebGL2 contexts even when WebGL1 context are requested. Reversible with `luma.enforceWebGL2(false);` + +Since luma.gl only supports WebGL2 contexts (`WebGL2RenderingContext`), it is not possible to call`luma.attachDevice()` on a WebGL1 context (`WebGLRenderingContext`). + +This becomes a problem when using luma.gl with a WebGL library that always creates WebGL1 contexts (such as Mapbox GL JS v1). +Calling `luma.enforceWebGL2()` before initializing the external library makes that library create a WebGL2 context, that luma.gl can then attach a Device to. + +:::caution +Since WebGL2 is a essentially a superset of WebGL1, a library written for WebGL 1 will often still work with a WebGL 2 context. However there may be issues if the external library relies on WebGL1 extensions that are not available in WebGL2. To make a WebGL 2 context support WebGL1-only extensions, those extensions would also need to be emulated on top of the WebGL 2 API, and this is not currently done. +::: + ## 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/modules/core/src/adapter/luma.ts b/modules/core/src/adapter/luma.ts index 03c94f758b..4a58497f42 100644 --- a/modules/core/src/adapter/luma.ts +++ b/modules/core/src/adapter/luma.ts @@ -149,6 +149,29 @@ export class luma { 'No matching device found. Ensure `@luma.gl/webgl` and/or `@luma.gl/webgpu` modules are imported.' ); } + + static enforceWebGL2(enforce: boolean = true): void { + const prototype = HTMLCanvasElement.prototype as any; + if (!enforce && prototype.originalGetContext) { + // Reset the original getContext function + prototype.getContext = prototype.originalGetContext; + prototype.originalGetContext = undefined; + return; + } + + // Store the original getContext function + prototype.originalGetContext = prototype.getContext; + + // Override the getContext function + 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); + } + // For any other type, return the original context + return this.originalGetContext(contextId, options); + }; + } } /** Convert a list of devices to a map */ diff --git a/modules/core/test/adapter/luma.spec.ts b/modules/core/test/adapter/luma.spec.ts index 72f42bcb55..8588d4b8a5 100644 --- a/modules/core/test/adapter/luma.spec.ts +++ b/modules/core/test/adapter/luma.spec.ts @@ -30,3 +30,51 @@ test('luma#registerDevices', async t => { t.equal(device.info.renderer, 'none', 'info.renderer ok'); t.end(); }); + +// To suppress @typescript-eslint/unbound-method +interface TestHTMLCanvasElement { + getContext: (contextId: any, options?: unknown) => string; + originalGetContext?: (contextId: any, options?: unknown) => unknown; +} + +test('luma#enforceWebGL2', async t => { + const prototype = HTMLCanvasElement.prototype as unknown as TestHTMLCanvasElement; + + // Setup mock getContext + const originalGetContext = prototype.getContext; + prototype.getContext = function (contextId: any, options?: unknown) { + return contextId; + }; + // Revert mock test completes. + t.teardown(() => { + prototype.getContext = originalGetContext; + }); + + t.equal(prototype.getContext('webgl'), 'webgl', 'getContext webgl ok'); + t.equal( + prototype.getContext('experimental-webgl'), + 'experimental-webgl', + 'getContext experimental-webgl ok' + ); + t.equal(prototype.getContext('webgl2'), 'webgl2', 'getContext webgl2 ok'); + + luma.enforceWebGL2(); + + t.true(prototype.originalGetContext, 'originalGetContext ok'); + t.equal(prototype.getContext('webgl'), 'webgl2', 'getContext enforce webgl2 ok'); + t.equal(prototype.getContext('experimental-webgl'), 'webgl2', 'getContext enforce webgl2 ok'); + t.equal(prototype.getContext('webgl2'), 'webgl2', 'getContext webgl2 ok'); + + luma.enforceWebGL2(false); + + t.false(prototype.originalGetContext, 'originalGetContext ok'); + t.equal(prototype.getContext('webgl'), 'webgl', 'getContext revert webgl ok'); + t.equal( + prototype.getContext('experimental-webgl'), + 'experimental-webgl', + 'getContext revert experimental-webgl ok' + ); + t.equal(prototype.getContext('webgl2'), 'webgl2', 'getContext webgl2 ok'); + + t.end(); +});