diff --git a/modules/webgl/src/adapter/converters/device-parameters.ts b/modules/webgl/src/adapter/converters/device-parameters.ts index d95307e5e9..ffc2519a17 100644 --- a/modules/webgl/src/adapter/converters/device-parameters.ts +++ b/modules/webgl/src/adapter/converters/device-parameters.ts @@ -14,7 +14,6 @@ import type { GLProvokingVertex, GLStencilOp } from '@luma.gl/constants'; -import {pushContextState, popContextState} from '../../context/state-tracker/track-context-state'; import {setGLParameters} from '../../context/parameters/unified-parameter-api'; import {WebGLDevice} from '../webgl-device'; @@ -41,13 +40,13 @@ export function withDeviceAndGLParameters( // Wrap in a try-catch to ensure that parameters are restored on exceptions const webglDevice = device as WebGLDevice; - pushContextState(webglDevice.gl); + webglDevice.pushState(); try { setDeviceParameters(device, parameters); setGLParameters(webglDevice.gl, glParameters); return func(device); } finally { - popContextState(webglDevice.gl); + webglDevice.popState(); } } @@ -72,12 +71,12 @@ export function withGLParameters( // Wrap in a try-catch to ensure that parameters are restored on exceptions const webglDevice = device as WebGLDevice; - pushContextState(webglDevice.gl); + webglDevice.pushState(); try { setGLParameters(webglDevice.gl, parameters); return func(device); } finally { - popContextState(webglDevice.gl); + webglDevice.popState(); } } @@ -99,15 +98,14 @@ export function withDeviceParameters( return func(device); } - // Wrap in a try-catch to ensure that parameters are restored on exceptions - // @ts-expect-error - pushContextState(device.gl); + // Wrap in a try-catch to ensure that parameters are restored on exceptions' + const webglDevice = device as WebGLDevice; + webglDevice.pushState(); try { setDeviceParameters(device, parameters); return func(device); } finally { - // @ts-expect-error - popContextState(device.gl); + webglDevice.popState(); } } diff --git a/modules/webgl/src/adapter/resources/webgl-render-pass.ts b/modules/webgl/src/adapter/resources/webgl-render-pass.ts index aaaa817b2b..33216ba3ac 100644 --- a/modules/webgl/src/adapter/resources/webgl-render-pass.ts +++ b/modules/webgl/src/adapter/resources/webgl-render-pass.ts @@ -8,7 +8,6 @@ import {WebGLDevice} from '../webgl-device'; import {GL, GLParameters} from '@luma.gl/constants'; import {withGLParameters} from '../../context/state-tracker/with-parameters'; import {setGLParameters} from '../../context/parameters/unified-parameter-api'; -import {pushContextState, popContextState} from '../../context/state-tracker/track-context-state'; import {WEBGLQuerySet} from './webgl-query-set'; // Should collapse during minification @@ -30,7 +29,7 @@ export class WEBGLRenderPass extends RenderPass { this.device = device; // TODO - do parameters (scissorRect) affect the clear operation? - pushContextState(this.device.gl); + this.device.pushState(); this.setParameters(this.props.parameters); // Hack - for now WebGL draws in "immediate mode" (instead of queueing the operations)... @@ -38,7 +37,7 @@ export class WEBGLRenderPass extends RenderPass { } end(): void { - popContextState(this.device.gl); + this.device.popState(); // should add commands to CommandEncoder. } diff --git a/modules/webgl/src/adapter/webgl-device.ts b/modules/webgl/src/adapter/webgl-device.ts index 49d755a635..02010b6ba3 100644 --- a/modules/webgl/src/adapter/webgl-device.ts +++ b/modules/webgl/src/adapter/webgl-device.ts @@ -7,11 +7,7 @@ import type {DeviceProps, DeviceInfo, CanvasContextProps, TextureFormat} from '@ import type {Buffer, Texture, Framebuffer, VertexArray, VertexArrayProps} from '@luma.gl/core'; import {Device, CanvasContext, log} from '@luma.gl/core'; import type {GLExtensions} from '@luma.gl/constants'; -import { - popContextState, - pushContextState, - trackContextState -} from '../context/state-tracker/track-context-state'; +import {WebGLStateTracker} from '../context/state-tracker/webgl-state-tracker'; import {createBrowserContext} from '../context/helpers/create-browser-context'; import {getDeviceInfo} from './device-helpers/webgl-device-info'; import {WebGLDeviceFeatures} from './device-helpers/webgl-device-features'; @@ -231,13 +227,10 @@ ${device.info.vendor}, ${device.info.renderer} for canvas: ${device.canvasContex this.canvasContext.resize(); // Install context state tracking - // @ts-expect-error - hidden parameters - const {enable = true, copyState = false} = props; - trackContextState(this.gl, { - enable, - copyState, + const glState = new WebGLStateTracker(this.gl, { log: (...args: any[]) => log.log(1, ...args)() }); + glState.trackState(this.gl, {copyState: false}); // DEBUG contexts: Add debug instrumentation to the context, force log level to at least 1 if (props.debug) { @@ -461,12 +454,14 @@ ${device.info.vendor}, ${device.info.renderer} for canvas: ${device.canvasContex /** Save current WebGL context state onto an internal stack */ pushState(): void { - pushContextState(this.gl); + const webglState = WebGLStateTracker.get(this.gl); + webglState.push(); } /** Restores previously saved context state */ popState(): void { - popContextState(this.gl); + const webglState = WebGLStateTracker.get(this.gl); + webglState.pop(); } /** diff --git a/modules/webgl/src/context/state-tracker/track-context-state.ts b/modules/webgl/src/context/state-tracker/webgl-state-tracker.ts similarity index 73% rename from modules/webgl/src/context/state-tracker/track-context-state.ts rename to modules/webgl/src/context/state-tracker/webgl-state-tracker.ts index 261eb760e1..8f418a4948 100644 --- a/modules/webgl/src/context/state-tracker/track-context-state.ts +++ b/modules/webgl/src/context/state-tracker/webgl-state-tracker.ts @@ -2,37 +2,43 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors -// Support for listening to context state changes and intercepting state queries -// NOTE: this system does not handle buffer bindings +import {setGLParameters, getGLParameters} from '../parameters/unified-parameter-api'; +import {deepArrayEqual} from './deep-array-equal'; import { GL_PARAMETER_DEFAULTS, GL_HOOKED_SETTERS, NON_CACHE_PARAMETERS } from '../parameters/webgl-parameter-tables'; -import {setGLParameters, getGLParameters} from '../parameters/unified-parameter-api'; -import {deepArrayEqual} from './deep-array-equal'; -// HELPER CLASS - GLState +// HELPER CLASS - WebGLStateTracker + +/** + * Support for listening to context state changes and intercepting state queries + * NOTE: this system does not handle buffer bindings + */ +export class WebGLStateTracker { + static get(gl: WebGL2RenderingContext): WebGLStateTracker { + // @ts-expect-error + return gl.state as WebGLStateTracker; + } -/* eslint-disable no-shadow */ -class GLState { gl: WebGL2RenderingContext; program: unknown = null; stateStack: object[] = []; enable = true; - cache: Record; + cache: Record = null!; log; + protected initialized = false; + constructor( gl: WebGL2RenderingContext, - { - copyState = false, // Copy cache from params (slow) or initialize from WebGL defaults (fast) - log = () => {} // Logging function, called when gl parameter change calls are actually issued - } = {} + props?: { + log; // Logging function, called when gl parameter change calls are actually issued + } ) { this.gl = gl; - this.cache = copyState ? getGLParameters(gl) : Object.assign({}, GL_PARAMETER_DEFAULTS); - this.log = log; + this.log = props?.log || (() => {}); this._updateCache = this._updateCache.bind(this); Object.seal(this); @@ -51,6 +57,38 @@ class GLState { this.stateStack.pop(); } + /** + * Initialize WebGL state caching on a context + * can be called multiple times to enable/disable + * + * @note After calling this function, context state will be cached + * .push() and .pop() will be available for saving, + * temporarily modifying, and then restoring state. + */ + trackState(gl: WebGL2RenderingContext, options?: {copyState?: boolean}): void { + this.cache = options.copyState ? getGLParameters(gl) : Object.assign({}, GL_PARAMETER_DEFAULTS); + + if (this.initialized) { + throw new Error('WebGLStateTracker'); + } + this.initialized = true; + + // @ts-expect-error + this.gl.state = this; + + installProgramSpy(gl); + + // intercept all setter functions in the table + for (const key in GL_HOOKED_SETTERS) { + const setter = GL_HOOKED_SETTERS[key]; + installSetterSpy(gl, key, setter); + } + + // intercept all getter functions in the table + installGetterOverride(gl, 'getParameter'); + installGetterOverride(gl, 'isEnabled'); + } + /** // interceptor for context set functions - update our cache and our stack // values (Object) - the key values for this setter @@ -89,83 +127,6 @@ class GLState { } } -function getContextState(gl: WebGL2RenderingContext): GLState { - // @ts-expect-error - return gl.state as GLState; -} - -// PUBLIC API - -/** - * Initialize WebGL state caching on a context - * can be called multiple times to enable/disable - * - * @note After calling this function, context state will be cached - * gl.state.push() and gl.state.pop() will be available for saving, - * temporarily modifying, and then restoring state. - */ -export function trackContextState( - gl: WebGL2RenderingContext, - options?: { - enable?: boolean; - copyState?: boolean; - log?: any; - } -): WebGL2RenderingContext { - const {enable = true, copyState} = options || {}; - // assert(copyState !== undefined); - // @ts-expect-error - if (!gl.state) { - // @ts-ignore - // const {polyfillContext} = global_; - // if (polyfillContext) { - // polyfillContext(gl); - // } - - // Create a state cache - // @ts-expect-error - gl.state = new GLState(gl, {copyState}); - - installProgramSpy(gl); - - // intercept all setter functions in the table - for (const key in GL_HOOKED_SETTERS) { - const setter = GL_HOOKED_SETTERS[key]; - installSetterSpy(gl, key, setter); - } - - // intercept all getter functions in the table - installGetterOverride(gl, 'getParameter'); - installGetterOverride(gl, 'isEnabled'); - } - - const glState = getContextState(gl); - glState.enable = enable; - - return gl; -} - -/** - * Saves current WebGL context state onto an internal per-context stack - */ -export function pushContextState(gl: WebGL2RenderingContext): void { - let glState = getContextState(gl); - if (!glState) { - trackContextState(gl, {copyState: false}); - glState = getContextState(gl); - } - glState.push(); -} - -/** - * Restores previously saved WebGL context state - */ -export function popContextState(gl: WebGL2RenderingContext): void { - const glState = getContextState(gl); - // assert(glState); - glState.pop(); -} - // HELPER FUNCTIONS - INSTALL GET/SET INTERCEPTORS (SPYS) ON THE CONTEXT /** @@ -185,7 +146,7 @@ function installGetterOverride(gl: WebGL2RenderingContext, functionName: string) return originalGetterFunc(pname); } - const glState = getContextState(gl); + const glState = WebGLStateTracker.get(gl); if (!(pname in glState.cache)) { // WebGL limits are not prepopulated in the cache, call the original getter when first queried. glState.cache[pname] = originalGetterFunc(pname); @@ -229,7 +190,7 @@ function installSetterSpy(gl: WebGL2RenderingContext, functionName: string, sett gl[functionName] = function set(...params) { // Update the value // Call the setter with the state cache and the params so that it can store the parameters - const glState = getContextState(gl); + const glState = WebGLStateTracker.get(gl); // eslint-disable-next-line @typescript-eslint/unbound-method const {valueChanged, oldValue} = setter(glState._updateCache, ...params); @@ -257,7 +218,7 @@ function installProgramSpy(gl: WebGL2RenderingContext): void { const originalUseProgram = gl.useProgram.bind(gl); gl.useProgram = function useProgramLuma(handle) { - const glState = getContextState(gl); + const glState = WebGLStateTracker.get(gl); if (glState.program !== handle) { originalUseProgram(handle); glState.program = handle; diff --git a/modules/webgl/src/context/state-tracker/with-parameters.ts b/modules/webgl/src/context/state-tracker/with-parameters.ts index 96cc426317..7b178da23a 100644 --- a/modules/webgl/src/context/state-tracker/with-parameters.ts +++ b/modules/webgl/src/context/state-tracker/with-parameters.ts @@ -3,7 +3,7 @@ // Copyright (c) vis.gl contributors import {GLParameters, setGLParameters} from '../parameters/unified-parameter-api'; -import {pushContextState, popContextState} from './track-context-state'; +import {WebGLStateTracker} from './webgl-state-tracker'; /** * Execute a function with a set of temporary WebGL parameter overrides @@ -25,7 +25,8 @@ export function withGLParameters( const {nocatch = true} = parameters; - pushContextState(gl); + const webglState = WebGLStateTracker.get(gl); + webglState.push(); setGLParameters(gl, parameters); // Setup is done, call the function @@ -34,13 +35,13 @@ export function withGLParameters( if (nocatch) { // Avoid try catch to minimize stack size impact for safe execution paths value = func(gl); - popContextState(gl); + webglState.pop(); } else { // Wrap in a try-catch to ensure that parameters are restored on exceptions try { value = func(gl); } finally { - popContextState(gl); + webglState.pop(); } } diff --git a/modules/webgl/src/index.ts b/modules/webgl/src/index.ts index d9f452f02f..419b88eb85 100644 --- a/modules/webgl/src/index.ts +++ b/modules/webgl/src/index.ts @@ -43,17 +43,12 @@ export {setDeviceParameters, withDeviceParameters} from './adapter/converters/de // HELPERS - EXPERIMENTAL export {getShaderLayout} from './adapter/helpers/get-shader-layout'; +export {WebGLStateTracker} from './context/state-tracker/webgl-state-tracker'; // TEST EXPORTS export {TEXTURE_FORMATS as _TEXTURE_FORMATS} from './adapter/converters/texture-formats'; // DEPRECATED TEST EXPORTS -// State tracking -export { - trackContextState, - pushContextState, - popContextState -} from './context/state-tracker/track-context-state'; export { resetGLParameters, diff --git a/modules/webgl/test/context/state-tracker/track-context-state.spec.ts b/modules/webgl/test/context/state-tracker/webgl-state-tracker.spec.ts similarity index 86% rename from modules/webgl/test/context/state-tracker/track-context-state.spec.ts rename to modules/webgl/test/context/state-tracker/webgl-state-tracker.spec.ts index 628a4aa465..7a9a543415 100644 --- a/modules/webgl/test/context/state-tracker/track-context-state.spec.ts +++ b/modules/webgl/test/context/state-tracker/webgl-state-tracker.spec.ts @@ -8,9 +8,7 @@ import {createTestDevice} from '@luma.gl/test-utils'; import type {WebGLDevice} from '@luma.gl/webgl'; import { - trackContextState, - pushContextState, - popContextState, + WebGLStateTracker, getGLParameters, setGLParameters, resetGLParameters, @@ -30,23 +28,21 @@ import {ENUM_STYLE_SETTINGS_SET1, ENUM_STYLE_SETTINGS_SET2} from './data/sample- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion const device = createTestDevice({debug: true}) as WebGLDevice; -test('WebGLState#imports', t => { - t.ok(typeof trackContextState === 'function', 'trackContextState imported OK'); - t.ok(typeof pushContextState === 'function', 'trackContextState imported OK'); - t.ok(typeof popContextState === 'function', 'trackContextState imported OK'); +test('WebGLStateTracker#imports', t => { + t.ok(typeof WebGLStateTracker === 'function', 'WebGLStateTracker imported OK'); t.end(); }); -test('WebGLState#trackContextState', t => { - const {gl} = device; - t.doesNotThrow( - () => trackContextState(gl, {copyState: false}), - 'trackContextState call succeeded' - ); - t.end(); -}); +// test.skip('WebGLStateTracker#trackContextState', t => { +// const {gl} = device; +// t.doesNotThrow( +// () => trackContextState(gl, {copyState: false}), +// 'trackContextState call succeeded' +// ); +// t.end(); +// }); -test('WebGLState#push & pop', t => { +test('WebGLStateTracker#push & pop', t => { const {gl} = device; resetGLParameters(gl); @@ -62,7 +58,7 @@ test('WebGLState#push & pop', t => { ); } - pushContextState(gl); + device.pushState(); // Set custom values and verify. setGLParameters(gl, ENUM_STYLE_SETTINGS_SET1); @@ -76,7 +72,7 @@ test('WebGLState#push & pop', t => { ); } - pushContextState(gl); + device.pushState(); // Set custom values and verify setGLParameters(gl, ENUM_STYLE_SETTINGS_SET2); @@ -90,7 +86,7 @@ test('WebGLState#push & pop', t => { } // Pop and verify values restore to previous state - popContextState(gl); + device.popState(); parameters = getGLParameters(gl); for (const key in ENUM_STYLE_SETTINGS_SET1) { @@ -102,7 +98,7 @@ test('WebGLState#push & pop', t => { ); } - popContextState(gl); + device.popState(); parameters = getGLParameters(gl); for (const key in GL_PARAMETER_DEFAULTS) { @@ -117,7 +113,7 @@ test('WebGLState#push & pop', t => { t.end(); }); -test('WebGLState#gl API', t => { +test('WebGLStateTracker#gl API', t => { const {gl} = device; resetGLParameters(gl); @@ -133,7 +129,7 @@ test('WebGLState#gl API', t => { ); } - pushContextState(gl); + device.pushState(); // TODO: test gl calls for compsite setters too (may be just call all gl calls). for (const key in ENUM_STYLE_SETTINGS_SET1) { @@ -159,7 +155,7 @@ test('WebGLState#gl API', t => { } } - popContextState(gl); + device.popState(); parameters = getGLParameters(gl); for (const key in GL_PARAMETER_DEFAULTS) { const value = parameters[key]; @@ -173,12 +169,12 @@ test('WebGLState#gl API', t => { t.end(); }); -test('WebGLState#intercept gl calls', t => { +test('WebGLStateTracker#intercept gl calls', t => { const {gl} = device; resetGLParameters(gl); - pushContextState(gl); + device.pushState(); const buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); @@ -199,7 +195,7 @@ test('WebGLState#intercept gl calls', t => { gl.stencilOp(gl.KEEP, gl.ZERO, gl.REPLACE); t.is(getGLParameters(gl, gl.STENCIL_PASS_DEPTH_FAIL), gl.ZERO, 'direct gl call is tracked'); - popContextState(gl); + device.popState(); const parameters = getGLParameters(gl); // Verify default values. @@ -216,7 +212,7 @@ test('WebGLState#intercept gl calls', t => { t.end(); }); -test('WebGLState#not cached parameters', t => { +test('WebGLStateTracker#not cached parameters', t => { const {gl} = device; resetGLParameters(gl); diff --git a/modules/webgl/test/index.ts b/modules/webgl/test/index.ts index a601b17895..56bb8aa485 100644 --- a/modules/webgl/test/index.ts +++ b/modules/webgl/test/index.ts @@ -18,7 +18,7 @@ import './adapter/helpers/webgl-topology-utils.spec'; // state-tracker import './context/state-tracker/deep-array-equal.spec'; import './context/state-tracker/set-parameters.spec'; -import './context/state-tracker/track-context-state.spec'; +import './context/state-tracker/webgl-state-tracker.spec'; import './context/state-tracker/context-state.spec'; // ADAPTER