Skip to content

Commit

Permalink
add(core) enforceWebGL2 (#2067)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisgervang authored Apr 5, 2024
1 parent def29f4 commit b5eb69a
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 17 deletions.
2 changes: 1 addition & 1 deletion .ocularrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const config = {

lint: {
paths: ['modules', 'docs', 'test', 'examples'],
extensions: ['js', 'ts']
extensions: ['js', 'ts', 'jsx', 'tsx']
},

typescript: {
Expand Down
74 changes: 58 additions & 16 deletions docs/api-reference/core/luma.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -92,29 +101,62 @@ 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()`

```typescript
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).
23 changes: 23 additions & 0 deletions modules/core/src/adapter/luma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
48 changes: 48 additions & 0 deletions modules/core/test/adapter/luma.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

0 comments on commit b5eb69a

Please sign in to comment.