// luma.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import { Device, CanvasContext, log, uid, assert } from '@luma.gl/core'; import { popContextState, pushContextState, trackContextState } from "../context/state-tracker/track-context-state.js"; import { createBrowserContext } from "../context/helpers/create-browser-context.js"; import { getDeviceInfo } from "./device-helpers/webgl-device-info.js"; import { WebGLDeviceFeatures } from "./device-helpers/webgl-device-features.js"; import { WebGLDeviceLimits } from "./device-helpers/webgl-device-limits.js"; import { WebGLCanvasContext } from "./webgl-canvas-context.js"; import { loadSpectorJS, initializeSpectorJS } from "../context/debug/spector.js"; import { loadWebGLDeveloperTools, makeDebugContext } from "../context/debug/webgl-developer-tools.js"; import { isTextureFormatSupported, isTextureFormatRenderable, isTextureFormatFilterable } from "./converters/texture-formats.js"; import { WEBGLBuffer } from "./resources/webgl-buffer.js"; import { WEBGLShader } from "./resources/webgl-shader.js"; import { WEBGLSampler } from "./resources/webgl-sampler.js"; import { WEBGLTexture } from "./resources/webgl-texture.js"; import { WEBGLFramebuffer } from "./resources/webgl-framebuffer.js"; import { WEBGLRenderPass } from "./resources/webgl-render-pass.js"; import { WEBGLRenderPipeline } from "./resources/webgl-render-pipeline.js"; import { WEBGLCommandEncoder } from "./resources/webgl-command-encoder.js"; import { WEBGLVertexArray } from "./resources/webgl-vertex-array.js"; import { WEBGLTransformFeedback } from "./resources/webgl-transform-feedback.js"; import { WEBGLQuerySet } from "./resources/webgl-query-set.js"; import { readPixelsToArray, readPixelsToBuffer } from "../classic/copy-and-blit.js"; import { setGLParameters, getGLParameters, resetGLParameters } from "../context/parameters/unified-parameter-api.js"; import { withGLParameters } from "../context/state-tracker/with-parameters.js"; import { clear } from "../classic/clear.js"; import { getWebGLExtension } from "../context/helpers/webgl-extensions.js"; const LOG_LEVEL = 1; /** WebGPU style Device API for a WebGL context */ export class WebGLDevice extends Device { // // Public `Device` API // /** type of this device */ static type = 'webgl'; /** type of this device */ type = 'webgl'; /** The underlying WebGL context */ handle; features; limits; info; canvasContext; lost; _resolveContextLost; // // Static methods, expected to be present by `luma.createDevice()` // /** Check if WebGL 2 is available */ static isSupported() { return typeof WebGL2RenderingContext !== 'undefined'; } /** * Get a device instance from a GL context * Creates and instruments the device if not already created * @param gl * @returns */ static attach(gl) { if (gl instanceof WebGLDevice) { return gl; } // @ts-expect-error if (gl?.device instanceof Device) { // @ts-expect-error return gl.device; } if (!isWebGL(gl)) { throw new Error('Invalid WebGL2RenderingContext'); } return new WebGLDevice({ gl: gl }); } static async create(props = {}) { log.groupCollapsed(LOG_LEVEL, 'WebGLDevice created')(); const promises = []; // Load webgl and spector debug scripts from CDN if requested if (props.debug) { promises.push(loadWebGLDeveloperTools()); } if (props.spector) { promises.push(loadSpectorJS()); } // 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. if (typeof props.canvas === 'string') { promises.push(CanvasContext.pageLoaded); } // Wait for all the loads to settle before creating the context. // The Device.create() functions are async, so in contrast to the constructor, we can `await` here. const results = await Promise.allSettled(promises); for (const result of results) { if (result.status === 'rejected') { log.error(`Failed to initialize debug libraries ${result.reason}`)(); } } log.probe(LOG_LEVEL + 1, 'DOM is loaded')(); // @ts-expect-error if (props.gl?.device) { log.warn('reattaching existing device')(); return WebGLDevice.attach(props.gl); } const device = new WebGLDevice(props); // Log some debug info about the newly created context const message = `\ Created ${device.type}${device.debug ? ' debug' : ''} context: \ ${device.info.vendor}, ${device.info.renderer} for canvas: ${device.canvasContext.id}`; log.probe(LOG_LEVEL, message)(); log.table(LOG_LEVEL, device.info)(); log.groupEnd(LOG_LEVEL)(); return device; } // // Public API // constructor(props) { super({ ...props, id: props.id || uid('webgl-device') }); // If attaching to an already attached context, return the attached device // @ts-expect-error device is attached to context const device = props.gl?.device; if (device) { throw new Error(`WebGL context already attached to device ${device.id}`); } // Create and instrument context const canvas = props.gl?.canvas || props.canvas; this.canvasContext = new WebGLCanvasContext(this, { ...props, canvas }); this.lost = new Promise(resolve => { this._resolveContextLost = resolve; }); let gl = props.gl || null; gl ||= createBrowserContext(this.canvasContext.canvas, { ...props, onContextLost: (event) => this._resolveContextLost?.({ reason: 'destroyed', message: 'Entered sleep mode, or too many apps or browser tabs are using the GPU.' }) }); if (!gl) { throw new Error('WebGL context creation failed'); } this.handle = gl; this.gl = gl; this.gl.device = this; // Update GL context: Link webgl context back to device this.gl._version = 2; // Update GL context: Store WebGL version field on gl context (HACK to identify debug contexts) if (props.spector) { this.spectorJS = initializeSpectorJS({ ...this.props, canvas: this.handle.canvas }); } // luma Device fields this.info = getDeviceInfo(this.gl, this._extensions); this.limits = new WebGLDeviceLimits(this.gl); this.features = new WebGLDeviceFeatures(this.gl, this._extensions, this.props.disabledFeatures); if (this.props.initalizeFeatures) { this.features.initializeFeatures(); } this.canvasContext.resize(); // Install context state tracking // @ts-expect-error - hidden parameters const { enable = true, copyState = false } = props; trackContextState(this.gl, { enable, copyState, log: (...args) => log.log(1, ...args)() }); // DEBUG contexts: Add debug instrumentation to the context, force log level to at least 1 if (props.debug) { this.gl = makeDebugContext(this.gl, { ...props, throwOnError: true }); this.debug = true; log.level = Math.max(log.level, 1); log.warn('WebGL debug mode activated. Performance reduced.')(); } } /** * Destroys the context * @note Has no effect for WebGL browser contexts, there is no browser API for destroying contexts */ destroy() { } get isLost() { return this.gl.isContextLost(); } getSize() { return [this.gl.drawingBufferWidth, this.gl.drawingBufferHeight]; } isTextureFormatSupported(format) { return isTextureFormatSupported(this.gl, format, this._extensions); } isTextureFormatFilterable(format) { return isTextureFormatFilterable(this.gl, format, this._extensions); } isTextureFormatRenderable(format) { return isTextureFormatRenderable(this.gl, format, this._extensions); } // IMPLEMENTATION OF ABSTRACT DEVICE createCanvasContext(props) { throw new Error('WebGL only supports a single canvas'); } createBuffer(props) { const newProps = this._getBufferProps(props); return new WEBGLBuffer(this, newProps); } _createTexture(props) { return new WEBGLTexture(this, props); } createExternalTexture(props) { throw new Error('createExternalTexture() not implemented'); // return new Program(props); } createSampler(props) { return new WEBGLSampler(this, props); } createShader(props) { return new WEBGLShader(this, props); } createFramebuffer(props) { return new WEBGLFramebuffer(this, props); } createVertexArray(props) { return new WEBGLVertexArray(this, props); } createTransformFeedback(props) { return new WEBGLTransformFeedback(this, props); } createQuerySet(props) { return new WEBGLQuerySet(this, props); } createRenderPipeline(props) { return new WEBGLRenderPipeline(this, props); } beginRenderPass(props) { return new WEBGLRenderPass(this, props); } createComputePipeline(props) { throw new Error('ComputePipeline not supported in WebGL'); } beginComputePass(props) { throw new Error('ComputePass not supported in WebGL'); } renderPass = null; createCommandEncoder(props) { return new WEBGLCommandEncoder(this, props); } /** * Offscreen Canvas Support: Commit the frame * https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/commit * Chrome's offscreen canvas does not require gl.commit */ submit() { this.renderPass?.end(); this.renderPass = null; // this.canvasContext.commit(); } // // TEMPORARY HACKS - will be removed in v9.1 // /** @deprecated - should use command encoder */ readPixelsToArrayWebGL(source, options) { return readPixelsToArray(source, options); } /** @deprecated - should use command encoder */ readPixelsToBufferWebGL(source, options) { return readPixelsToBuffer(source, options); } setParametersWebGL(parameters) { setGLParameters(this.gl, parameters); } getParametersWebGL(parameters) { return getGLParameters(this.gl, parameters); } withParametersWebGL(parameters, func) { return withGLParameters(this.gl, parameters, func); } clearWebGL(options) { clear(this, options); } resetWebGL() { log.warn('WebGLDevice.resetWebGL is deprecated, use only for debugging')(); resetGLParameters(this.gl); } // // WebGL-only API (not part of `Device` API) // /** WebGL2 context. */ gl; debug = false; /** State used by luma.gl classes: TODO - move to canvasContext*/ _canvasSizeInfo = { clientWidth: 0, clientHeight: 0, devicePixelRatio: 1 }; /** State used by luma.gl classes - TODO - not used? */ _extensions = {}; _polyfilled = false; /** Instance of Spector.js (if initialized) */ spectorJS; /** * Triggers device (or WebGL context) loss. * @note primarily intended for testing how application reacts to device loss */ loseDevice() { let deviceLossTriggered = false; const extensions = this.getExtension('WEBGL_lose_context'); const ext = extensions.WEBGL_lose_context; if (ext) { deviceLossTriggered = true; ext.loseContext(); // ext.loseContext should trigger context loss callback but the platform may not do this, so do it explicitly } this._resolveContextLost?.({ reason: 'destroyed', message: 'Application triggered context loss' }); return deviceLossTriggered; } /** Save current WebGL context state onto an internal stack */ pushState() { pushContextState(this.gl); } /** Restores previously saved context state */ popState() { popContextState(this.gl); } /** * Storing data on a special field on WebGLObjects makes that data visible in SPECTOR chrome debug extension * luma.gl ids and props can be inspected */ setSpectorMetadata(handle, props) { // @ts-expect-error // eslint-disable-next-line camelcase handle.__SPECTOR_Metadata = props; } /** * Returns the GL. constant that corresponds to a numeric value of a GL constant * Be aware that there are some duplicates especially for constants that are 0, * so this isn't guaranteed to return the right key in all cases. */ getGLKey(value, gl) { // @ts-ignore expect-error depends on settings gl = gl || this.gl2 || this.gl; const number = Number(value); for (const key in gl) { // @ts-ignore expect-error depends on settings if (gl[key] === number) { return `GL.${key}`; } } // No constant found. Stringify the value and return it. return String(value); } /** Store constants */ _constants; /** * Set a constant value for a location. Disabled attributes at that location will read from this value * @note WebGL constants are stored globally on the WebGL context, not the VertexArray * so they need to be updated before every render * @todo - remember/cache values to avoid setting them unnecessarily? */ setConstantAttributeWebGL(location, constant) { const maxVertexAttributes = this.limits.maxVertexAttributes; this._constants = this._constants || new Array(maxVertexAttributes).fill(null); const currentConstant = this._constants[location]; if (currentConstant && compareConstantArrayValues(currentConstant, constant)) { log.info(1, `setConstantAttributeWebGL(${location}) could have been skipped, value unchanged`)(); } this._constants[location] = constant; switch (constant.constructor) { case Float32Array: setConstantFloatArray(this, location, constant); break; case Int32Array: setConstantIntArray(this, location, constant); break; case Uint32Array: setConstantUintArray(this, location, constant); break; default: assert(false); } } /** Ensure extensions are only requested once */ getExtension(name) { getWebGLExtension(this.gl, name, this._extensions); return this._extensions; } } /** Check if supplied parameter is a WebGL2RenderingContext */ function isWebGL(gl) { if (typeof WebGL2RenderingContext !== 'undefined' && gl instanceof WebGL2RenderingContext) { return true; } // Look for debug contexts, headless gl etc return Boolean(gl && Number.isFinite(gl._version)); } /** Set constant float array attribute */ function setConstantFloatArray(device, location, array) { switch (array.length) { case 1: device.gl.vertexAttrib1fv(location, array); break; case 2: device.gl.vertexAttrib2fv(location, array); break; case 3: device.gl.vertexAttrib3fv(location, array); break; case 4: device.gl.vertexAttrib4fv(location, array); break; default: assert(false); } } /** Set constant signed int array attribute */ function setConstantIntArray(device, location, array) { device.gl.vertexAttribI4iv(location, array); // TODO - not clear if we need to use the special forms, more testing needed // switch (array.length) { // case 1: // gl.vertexAttribI1iv(location, array); // break; // case 2: // gl.vertexAttribI2iv(location, array); // break; // case 3: // gl.vertexAttribI3iv(location, array); // break; // case 4: // break; // default: // assert(false); // } } /** Set constant unsigned int array attribute */ function setConstantUintArray(device, location, array) { device.gl.vertexAttribI4uiv(location, array); // TODO - not clear if we need to use the special forms, more testing needed // switch (array.length) { // case 1: // gl.vertexAttribI1uiv(location, array); // break; // case 2: // gl.vertexAttribI2uiv(location, array); // break; // case 3: // gl.vertexAttribI3uiv(location, array); // break; // case 4: // gl.vertexAttribI4uiv(location, array); // break; // default: // assert(false); // } } /** * Compares contents of two typed arrays * @todo max length? */ function compareConstantArrayValues(v1, v2) { if (!v1 || !v2 || v1.length !== v2.length || v1.constructor !== v2.constructor) { return false; } for (let i = 0; i < v1.length; ++i) { if (v1[i] !== v2[i]) { return false; } } return true; }