diff --git a/Sources/Rendering/OpenGL/RenderWindow/index.js b/Sources/Rendering/OpenGL/RenderWindow/index.js index 13e511c9e8c..f141603a6e8 100644 --- a/Sources/Rendering/OpenGL/RenderWindow/index.js +++ b/Sources/Rendering/OpenGL/RenderWindow/index.js @@ -86,6 +86,63 @@ function checkRenderTargetSupport(gl, internalFormat, format, type) { return status === gl.FRAMEBUFFER_COMPLETE; } +// ---------------------------------------------------------------------------- +// GL informations helpers +// ---------------------------------------------------------------------------- + +// Returns a factory of lazy getters for the parameters of a WebGL extension. +// The extension is only requested (once) when one of the getters is first +// called. This matters for extensions that are costly or noisy to request, +// e.g. Firefox logs a deprecation warning whenever WEBGL_debug_renderer_info +// is requested. +function createLazyExtensionParameterGetter(gl, extensionName) { + let extension; + return (parameterName) => () => { + if (extension === undefined) { + extension = gl.getExtension(extensionName); + } + return extension && gl.getParameter(extension[parameterName]); + }; +} + +// Defines an enumerable `object[name]` property that is only evaluated with +// `evaluate()` on first access, then cached as a regular writable value. +function defineLazyProperty(object, name, evaluate) { + Object.defineProperty(object, name, { + get() { + const value = evaluate(); + Object.defineProperty(object, name, { + value, + enumerable: true, + writable: true, + }); + return value; + }, + enumerable: true, + configurable: true, + }); +} + +// Converts a list of [label, key, value] entries into a { [key]: { label, +// value } } map. A function value is evaluated lazily, on first read of the +// entry value. Entries without a key are skipped. +function buildGLInformations(params) { + const result = {}; + while (params.length) { + const [label, key, value] = params.pop(); + if (key) { + if (typeof value === 'function') { + const entry = { label }; + defineLazyProperty(entry, 'value', value); + result[key] = entry; + } else { + result[key] = { label, value }; + } + } + } + return result; +} + // ---------------------------------------------------------------------------- // Monitor the usage of GL context across vtkOpenGLRenderWindow instances // ---------------------------------------------------------------------------- @@ -663,7 +720,14 @@ function vtkOpenGLRenderWindow(publicAPI, model) { // Float/halfFloat textures and multiple render targets are core in // WebGL2; only float color renderability still hangs off an extension. const glColorBufferFloat = gl.getExtension('EXT_color_buffer_float'); - const glDebugRendererInfo = gl.getExtension('WEBGL_debug_renderer_info'); + // Firefox deprecated WEBGL_debug_renderer_info (RENDERER/VENDOR already + // report the unmasked strings there) and logs a warning every time the + // extension is requested, so only request it when the unmasked values + // are actually read. + const getDebugRendererInfoParameter = createLazyExtensionParameterGetter( + gl, + 'WEBGL_debug_renderer_info' + ); const glAnisotropic = gl.getExtension('EXT_texture_filter_anisotropic') || gl.getExtension('WEBKIT_EXT_texture_filter_anisotropic'); @@ -998,27 +1062,18 @@ function vtkOpenGLRenderWindow(publicAPI, model) { [ 'Unmasked Renderer', 'UNMASKED_RENDERER', - glDebugRendererInfo && - gl.getParameter(glDebugRendererInfo.UNMASKED_RENDERER_WEBGL), + getDebugRendererInfoParameter('UNMASKED_RENDERER_WEBGL'), ], [ 'Unmasked Vendor', 'UNMASKED_VENDOR', - glDebugRendererInfo && - gl.getParameter(glDebugRendererInfo.UNMASKED_VENDOR_WEBGL), + getDebugRendererInfoParameter('UNMASKED_VENDOR_WEBGL'), ], ['WebGL Version', 'WEBGL_VERSION', 2], ]; - const result = {}; - while (params.length) { - const [label, key, value] = params.pop(); - if (key) { - result[key] = { label, value }; - } - } - model._glInformation = result; - return result; + model._glInformation = buildGLInformations(params); + return model._glInformation; }; publicAPI.traverseAllPasses = () => { diff --git a/Sources/Rendering/OpenGL/RenderWindow/test/testGLInformations.js b/Sources/Rendering/OpenGL/RenderWindow/test/testGLInformations.js new file mode 100644 index 00000000000..6d89d277187 --- /dev/null +++ b/Sources/Rendering/OpenGL/RenderWindow/test/testGLInformations.js @@ -0,0 +1,56 @@ +import { it, expect, vi } from 'vitest'; + +import vtkOpenGLRenderWindow from 'vtk.js/Sources/Rendering/OpenGL/RenderWindow'; + +it.skipIf(__VTK_TEST_NO_WEBGL__)( + 'only requests WEBGL_debug_renderer_info when unmasked values are read', + () => { + const getExtension = vi.spyOn( + WebGL2RenderingContext.prototype, + 'getExtension' + ); + const glWindow = vtkOpenGLRenderWindow.newInstance(); + + try { + const info = glWindow.getGLInformations(); + expect(info.RENDERER.value).toBeTruthy(); + expect(info.UNMASKED_RENDERER.label).toBe('Unmasked Renderer'); + // Firefox logs a deprecation warning whenever this extension is + // requested, so getting the regular GL informations must not do it. + expect(getExtension).not.toHaveBeenCalledWith( + 'WEBGL_debug_renderer_info' + ); + + const unmaskedRenderer = info.UNMASKED_RENDERER.value; + const unmaskedVendor = info.UNMASKED_VENDOR.value; + expect(getExtension).toHaveBeenCalledWith('WEBGL_debug_renderer_info'); + const calls = getExtension.mock.calls.filter( + ([name]) => name === 'WEBGL_debug_renderer_info' + ).length; + expect(calls).toBe(1); + + const debugInfo = glWindow + .get3DContext() + .getExtension('WEBGL_debug_renderer_info'); + if (debugInfo) { + const gl = glWindow.get3DContext(); + expect(unmaskedRenderer).toBe( + gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) + ); + expect(unmaskedVendor).toBe( + gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) + ); + } else { + expect(unmaskedRenderer).toBeFalsy(); + expect(unmaskedVendor).toBeFalsy(); + } + + // values are cached and enumerable like the other entries + expect(info.UNMASKED_RENDERER.value).toBe(unmaskedRenderer); + expect(Object.keys(info.UNMASKED_RENDERER)).toEqual(['label', 'value']); + } finally { + getExtension.mockRestore(); + glWindow.delete(); + } + } +);