From 3c0121233718efc3ab106132992bfde94f799641 Mon Sep 17 00:00:00 2001 From: Stefan Silviu Date: Thu, 19 Dec 2024 16:19:52 +0200 Subject: [PATCH] fix: avoid norm16 for textures with linear filtering This only happens on certain devices due to a driver bug, see https://github.com/KhronosGroup/WebGL/issues/3706 --- Sources/Rendering/OpenGL/Texture/index.js | 39 ++++-- .../OpenGL/Texture/supportsNorm16Linear.js | 129 ++++++++++++++++++ 2 files changed, 153 insertions(+), 15 deletions(-) create mode 100644 Sources/Rendering/OpenGL/Texture/supportsNorm16Linear.js diff --git a/Sources/Rendering/OpenGL/Texture/index.js b/Sources/Rendering/OpenGL/Texture/index.js index 6ab1b33a542..ce0b7d2bda1 100644 --- a/Sources/Rendering/OpenGL/Texture/index.js +++ b/Sources/Rendering/OpenGL/Texture/index.js @@ -7,6 +7,8 @@ import vtkViewNode from 'vtk.js/Sources/Rendering/SceneGraph/ViewNode'; import { registerOverride } from 'vtk.js/Sources/Rendering/OpenGL/ViewNodeFactory'; +import supportsNorm16Linear from './supportsNorm16Linear'; + const { Wrap, Filter } = Constants; const { VtkDataTypes } = vtkDataArray; const { vtkDebugMacro, vtkErrorMacro, vtkWarningMacro } = macro; @@ -160,6 +162,17 @@ function vtkOpenGLTexture(publicAPI, model) { } }; + const getNorm16Ext = () => { + if ( + (model.minificationFilter === Filter.LINEAR || + model.magnificationFilter === Filter.LINEAR) && + !supportsNorm16Linear() + ) { + return undefined; + } + return model.oglNorm16Ext; + }; + //---------------------------------------------------------------------------- publicAPI.destroyTexture = () => { // deactivate it first @@ -391,7 +404,7 @@ function vtkOpenGLTexture(publicAPI, model) { result = model._openGLRenderWindow.getDefaultTextureInternalFormat( vtktype, numComps, - model.oglNorm16Ext, + getNorm16Ext(), publicAPI.useHalfFloat() ); if (result) { @@ -478,9 +491,9 @@ function vtkOpenGLTexture(publicAPI, model) { return model.context.UNSIGNED_BYTE; // prefer norm16 since that is accurate compared to // half float which is not - case model.oglNorm16Ext && !useHalfFloat && VtkDataTypes.SHORT: + case getNorm16Ext() && !useHalfFloat && VtkDataTypes.SHORT: return model.context.SHORT; - case model.oglNorm16Ext && !useHalfFloat && VtkDataTypes.UNSIGNED_SHORT: + case getNorm16Ext() && !useHalfFloat && VtkDataTypes.UNSIGNED_SHORT: return model.context.UNSIGNED_SHORT; // use half float type case useHalfFloat && VtkDataTypes.SHORT: @@ -820,7 +833,7 @@ function vtkOpenGLTexture(publicAPI, model) { if ( webGLInfo.RENDERER.value.match(/WebKit/gi) && navigator.platform.match(/Mac/gi) && - model.oglNorm16Ext && + getNorm16Ext() && (dataType === VtkDataTypes.UNSIGNED_SHORT || dataType === VtkDataTypes.SHORT) ) { @@ -925,7 +938,7 @@ function vtkOpenGLTexture(publicAPI, model) { numComps * model._openGLRenderWindow.getDefaultTextureByteSize( dataType, - model.oglNorm16Ext, + getNorm16Ext(), publicAPI.useHalfFloat() ); publicAPI.deactivate(); @@ -1049,7 +1062,7 @@ function vtkOpenGLTexture(publicAPI, model) { numComps * model._openGLRenderWindow.getDefaultTextureByteSize( dataType, - model.oglNorm16Ext, + getNorm16Ext(), publicAPI.useHalfFloat() ); // generateMipmap must not be called here because we manually upload all levels @@ -1138,7 +1151,7 @@ function vtkOpenGLTexture(publicAPI, model) { model.components * model._openGLRenderWindow.getDefaultTextureByteSize( dataType, - model.oglNorm16Ext, + getNorm16Ext(), publicAPI.useHalfFloat() ); @@ -1248,7 +1261,7 @@ function vtkOpenGLTexture(publicAPI, model) { model.components * model._openGLRenderWindow.getDefaultTextureByteSize( VtkDataTypes.UNSIGNED_CHAR, - model.oglNorm16Ext, + getNorm16Ext(), publicAPI.useHalfFloat() ); @@ -1401,11 +1414,7 @@ function vtkOpenGLTexture(publicAPI, model) { } // Handle SHORT data type with EXT_texture_norm16 extension - if ( - model.oglNorm16Ext && - !useHalfFloat && - dataType === VtkDataTypes.SHORT - ) { + if (getNorm16Ext() && !useHalfFloat && dataType === VtkDataTypes.SHORT) { for (let c = 0; c < numComps; ++c) { model.volumeInfo.scale[c] = 32767.0; // Scale to [-1, 1] range } @@ -1414,7 +1423,7 @@ function vtkOpenGLTexture(publicAPI, model) { // Handle UNSIGNED_SHORT data type with EXT_texture_norm16 extension if ( - model.oglNorm16Ext && + getNorm16Ext() && !useHalfFloat && dataType === VtkDataTypes.UNSIGNED_SHORT ) { @@ -1569,7 +1578,7 @@ function vtkOpenGLTexture(publicAPI, model) { model.components * model._openGLRenderWindow.getDefaultTextureByteSize( dataTypeToUse, - model.oglNorm16Ext, + getNorm16Ext(), publicAPI.useHalfFloat() ); diff --git a/Sources/Rendering/OpenGL/Texture/supportsNorm16Linear.js b/Sources/Rendering/OpenGL/Texture/supportsNorm16Linear.js new file mode 100644 index 00000000000..dcdddf0cf74 --- /dev/null +++ b/Sources/Rendering/OpenGL/Texture/supportsNorm16Linear.js @@ -0,0 +1,129 @@ +/** + * Even when the EXT_texture_norm16 extension is present, linear filtering + * might not be supported for normalized fixed point textures. + * + * This is a driver bug. See https://github.com/KhronosGroup/WebGL/issues/3706 + * @return {boolean} + */ +function supportsNorm16Linear() { + try { + const canvasSize = 4; + const texWidth = 2; + const texHeight = 1; + const texData = new Int16Array([0, 2 ** 15 - 1]); + const pixelToCheck = [1, 1]; + + const canvas = document.createElement('canvas'); + canvas.width = canvasSize; + canvas.height = canvasSize; + const gl = canvas.getContext('webgl2'); + if (!gl) { + return false; + } + + const ext = gl.getExtension('EXT_texture_norm16'); + if (!ext) { + return false; + } + + const vs = `#version 300 es + void main() { + gl_PointSize = ${canvasSize.toFixed(1)}; + gl_Position = vec4(0, 0, 0, 1); + } + `; + const fs = `#version 300 es + precision highp float; + precision highp int; + precision highp sampler2D; + + uniform sampler2D u_image; + + out vec4 color; + + void main() { + vec4 intColor = texture(u_image, gl_PointCoord.xy); + color = vec4(vec3(intColor.rrr), 1); + } + `; + + const vertexShader = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vertexShader, vs); + gl.compileShader(vertexShader); + if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { + return false; + } + + const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fragmentShader, fs); + gl.compileShader(fragmentShader); + if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { + return false; + } + + const program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + return false; + } + + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + ext.R16_SNORM_EXT, + texWidth, + texHeight, + 0, + gl.RED, + gl.SHORT, + texData + ); + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.useProgram(program); + gl.drawArrays(gl.POINTS, 0, 1); + + const pixel = new Uint8Array(4); + gl.readPixels( + pixelToCheck[0], + pixelToCheck[1], + 1, + 1, + gl.RGBA, + gl.UNSIGNED_BYTE, + pixel + ); + const [r, g, b] = pixel; + + const webglLoseContext = gl.getExtension('WEBGL_lose_context'); + if (webglLoseContext) { + webglLoseContext.loseContext(); + } + + return r === g && g === b && r !== 0; + } catch (e) { + return false; + } +} + +/** + * @type {boolean | undefined} + */ +let supportsNorm16LinearCache; + +function supportsNorm16LinearCached() { + // Only create a canvas+texture+shaders the first time + if (supportsNorm16LinearCache === undefined) { + supportsNorm16LinearCache = supportsNorm16Linear(); + } + + return supportsNorm16LinearCache; +} + +export default supportsNorm16LinearCached;