UNPKG

@luma.gl/gltools

Version:

WebGL2 API Polyfills for WebGL1 WebGLRenderingContext

221 lines (184 loc) 7.13 kB
// Support for listening to context state changes and intercepting state queries // NOTE: this system does not handle buffer bindings /** @typedef {import('./track-context-state')} types */ import {GL_PARAMETER_DEFAULTS, GL_HOOKED_SETTERS} from './webgl-parameter-tables'; import {setParameters, getParameters} from './unified-parameter-api'; import {assert} from '../utils/assert'; import {deepArrayEqual} from '../utils/utils'; // HELPER FUNCTIONS - INSTALL GET/SET INTERCEPTORS (SPYS) ON THE CONTEXT // Overrides a WebGLRenderingContext state "getter" function // to return values directly from cache function installGetterOverride(gl, functionName) { // Get the original function from the WebGLRenderingContext const originalGetterFunc = gl[functionName].bind(gl); // Wrap it with a spy so that we can update our state cache when it gets called gl[functionName] = function get(...params) { const pname = params[0]; // WebGL limits are not prepopulated in the cache, it's neither undefined in GL_PARAMETER_DEFAULTS // nor intercepted by GL_HOOKED_SETTERS. Query the original getter. if (!(pname in gl.state.cache)) { return originalGetterFunc(...params); } // Optionally call the original function to do a "hard" query from the WebGLRenderingContext return gl.state.enable ? // Call the getter the params so that it can e.g. serve from a cache gl.state.cache[pname] : // Optionally call the original function to do a "hard" query from the WebGLRenderingContext originalGetterFunc(...params); }; // Set the name of this anonymous function to help in debugging and profiling Object.defineProperty(gl[functionName], 'name', { value: `${functionName}-from-cache`, configurable: false }); } // Overrides a WebGLRenderingContext state "setter" function // to call a setter spy before the actual setter. Allows us to keep a cache // updated with a copy of the WebGL context state. function installSetterSpy(gl, functionName, setter) { // Get the original function from the WebGLRenderingContext const originalSetterFunc = gl[functionName].bind(gl); // Wrap it with a spy so that we can update our state cache when it gets called 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 {valueChanged, oldValue} = setter(gl.state._updateCache, ...params); // Call the original WebGLRenderingContext func to make sure the context actually gets updated if (valueChanged) { originalSetterFunc(...params); } // Note: if the original function fails to set the value, our state cache will be bad // No solution for this at the moment, but assuming that this is unlikely to be a real problem // We could call the setter after the originalSetterFunc. Concern is that this would // cause different behavior in debug mode, where originalSetterFunc can throw exceptions return oldValue; }; // Set the name of this anonymous function to help in debugging and profiling Object.defineProperty(gl[functionName], 'name', { value: `${functionName}-to-cache`, configurable: false }); } function installProgramSpy(gl) { const originalUseProgram = gl.useProgram.bind(gl); gl.useProgram = function useProgramLuma(handle) { if (gl.state.program !== handle) { originalUseProgram(handle); gl.state.program = handle; } }; } // HELPER CLASS - GLState /* eslint-disable no-shadow */ class GLState { constructor( gl, { 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 } = {} ) { this.gl = gl; this.program = null; this.stateStack = []; this.enable = true; this.cache = copyState ? getParameters(gl) : Object.assign({}, GL_PARAMETER_DEFAULTS); this.log = log; this._updateCache = this._updateCache.bind(this); Object.seal(this); } push(values = {}) { this.stateStack.push({}); } pop() { assert(this.stateStack.length > 0); // Use the saved values in the state stack to restore parameters const oldValues = this.stateStack[this.stateStack.length - 1]; setParameters(this.gl, oldValues); // Don't pop until we have reset parameters (to make sure other "stack frames" are not affected) this.stateStack.pop(); } // interceptor for context set functions - update our cache and our stack // values (Object) - the key values for this setter _updateCache(values) { let valueChanged = false; let oldValue; // = undefined const oldValues = this.stateStack.length > 0 && this.stateStack[this.stateStack.length - 1]; for (const key in values) { assert(key !== undefined); const value = values[key]; const cached = this.cache[key]; // Check that value hasn't already been shadowed if (!deepArrayEqual(value, cached)) { valueChanged = true; oldValue = cached; // First, save current value being shadowed // If a state stack frame is active, save the current parameter values for pop // but first check that value hasn't already been shadowed and saved if (oldValues && !(key in oldValues)) { oldValues[key] = cached; } // Save current value being shadowed this.cache[key] = value; } } return {valueChanged, oldValue}; } } // PUBLIC API /** * Initialize WebGL state caching on a context * @type {types['trackContextState']} */ // 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, options = {}) { const {enable = true, copyState} = options; assert(copyState !== undefined); // @ts-ignore if (!gl.state) { // @ts-ignore const {polyfillContext} = globalThis; if (polyfillContext) { polyfillContext(gl); } // Create a state cache // @ts-ignore 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'); } // @ts-ignore gl.state.enable = enable; return gl; } /** * Initialize WebGL state caching on a context * @type {types['pushContextState']} */ export function pushContextState(gl) { // @ts-ignore if (!gl.state) { trackContextState(gl, {copyState: false}); } // @ts-ignore gl.state.push(); } /** * Initialize WebGL state caching on a context * @type {types['popContextState']} */ export function popContextState(gl) { // @ts-ignore assert(gl.state); // @ts-ignore gl.state.pop(); }