Skip to content

Commit

Permalink
fix: avoid norm16 for textures with linear filtering
Browse files Browse the repository at this point in the history
This only happens on certain devices due to a driver bug, see
KhronosGroup/WebGL#3706
  • Loading branch information
slak44 committed Dec 20, 2024
1 parent 89758ed commit dd7e000
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 15 deletions.
39 changes: 24 additions & 15 deletions Sources/Rendering/OpenGL/Texture/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -160,6 +162,17 @@ function vtkOpenGLTexture(publicAPI, model) {
}
};

const getNorm16Ext = () => {
if (
!supportsNorm16Linear() &&
(model.minificationFilter === Filter.LINEAR ||
model.magnificationFilter === Filter.LINEAR)
) {
return undefined;
}
return model.oglNorm16Ext;
};

//----------------------------------------------------------------------------
publicAPI.destroyTexture = () => {
// deactivate it first
Expand Down Expand Up @@ -391,7 +404,7 @@ function vtkOpenGLTexture(publicAPI, model) {
result = model._openGLRenderWindow.getDefaultTextureInternalFormat(
vtktype,
numComps,
model.oglNorm16Ext,
getNorm16Ext(),
publicAPI.useHalfFloat()
);
if (result) {
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
) {
Expand Down Expand Up @@ -925,7 +938,7 @@ function vtkOpenGLTexture(publicAPI, model) {
numComps *
model._openGLRenderWindow.getDefaultTextureByteSize(
dataType,
model.oglNorm16Ext,
getNorm16Ext(),
publicAPI.useHalfFloat()
);
publicAPI.deactivate();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1138,7 +1151,7 @@ function vtkOpenGLTexture(publicAPI, model) {
model.components *
model._openGLRenderWindow.getDefaultTextureByteSize(
dataType,
model.oglNorm16Ext,
getNorm16Ext(),
publicAPI.useHalfFloat()
);

Expand Down Expand Up @@ -1248,7 +1261,7 @@ function vtkOpenGLTexture(publicAPI, model) {
model.components *
model._openGLRenderWindow.getDefaultTextureByteSize(
VtkDataTypes.UNSIGNED_CHAR,
model.oglNorm16Ext,
getNorm16Ext(),
publicAPI.useHalfFloat()
);

Expand Down Expand Up @@ -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
}
Expand All @@ -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
) {
Expand Down Expand Up @@ -1569,7 +1578,7 @@ function vtkOpenGLTexture(publicAPI, model) {
model.components *
model._openGLRenderWindow.getDefaultTextureByteSize(
dataTypeToUse,
model.oglNorm16Ext,
getNorm16Ext(),
publicAPI.useHalfFloat()
);

Expand Down
129 changes: 129 additions & 0 deletions Sources/Rendering/OpenGL/Texture/supportsNorm16Linear.js
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit dd7e000

Please sign in to comment.