Skip to content

fix(Texture): Prevent GL_INVALID_OPERATION when updating texture data #3297

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Sources/Rendering/OpenGL/Texture/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,26 @@ export interface vtkOpenGLTexture extends vtkViewNode {
flip?: boolean;
}): boolean;

/**
* Updates existing 2D texture data without recreating the texture.
* This method uses texSubImage2D to efficiently update texture data when
* the texture dimensions and format haven't changed, avoiding GL_INVALID_OPERATION
* errors that occur when trying to recreate immutable WebGL2 textures.
* @param data The raw data for the texture update.
* @param dataType The data type of the texture data.
* @param flip Whether to flip the texture vertically. Defaults to false.
* @returns {boolean} True if the texture was successfully updated, false otherwise.
*/
update2DFromRaw({
data,
dataType,
flip,
}: {
data: any;
dataType: VtkDataTypes;
flip?: boolean;
}): boolean;

/**
* Creates a cube texture from raw data.
* @param width The width of each face of the cube texture.
Expand Down
86 changes: 80 additions & 6 deletions Sources/Rendering/OpenGL/Texture/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,10 @@ function vtkOpenGLTexture(publicAPI, model) {
const input = model.renderable.getInputData(0);
if (input && input.getPointData().getScalars()) {
const ext = input.getExtent();
const newWidth = ext[1] - ext[0] + 1;
const newHeight = ext[3] - ext[2] + 1;
const inScalars = input.getPointData().getScalars();
const newComponents = inScalars.getNumberOfComponents();

// do we have a cube map? Six inputs
const data = [];
Expand All @@ -156,6 +159,31 @@ function vtkOpenGLTexture(publicAPI, model) {
data.push(scalars);
}
}

// Check if we can update existing texture without recreation.
// Avoid GL_INVALID_OPERATION: glTexStorage2D: Texture is immutable.
const isSameGeometry =
model.width === newWidth &&
model.height === newHeight &&
model.components === newComponents;
const canUpdate =
model.handle &&
isSameGeometry &&
model.numberOfDimensions === 2 &&
data.length <= 1; // Not a cube map

if (canUpdate) {
const success = publicAPI.update2DFromRaw({
data: inScalars.getData(),
dataType: inScalars.getDataType(),
});
if (success) {
model.textureBuildTime.modified();
return;
}
}

// Full texture creation needed
if (
model.renderable.getInterpolate() &&
inScalars.getNumberOfComponents() === 4
Expand All @@ -165,17 +193,17 @@ function vtkOpenGLTexture(publicAPI, model) {
}
if (data.length % 6 === 0) {
publicAPI.createCubeFromRaw({
width: ext[1] - ext[0] + 1,
height: ext[3] - ext[2] + 1,
numComps: inScalars.getNumberOfComponents(),
width: newWidth,
height: newHeight,
numComps: newComponents,
dataType: inScalars.getDataType(),
data,
});
} else {
publicAPI.create2DFromRaw({
width: ext[1] - ext[0] + 1,
height: ext[3] - ext[2] + 1,
numComps: inScalars.getNumberOfComponents(),
width: newWidth,
height: newHeight,
numComps: newComponents,
dataType: inScalars.getDataType(),
data: inScalars.getData(),
});
Expand Down Expand Up @@ -1091,6 +1119,52 @@ function vtkOpenGLTexture(publicAPI, model) {
return true;
};

//----------------------------------------------------------------------------
// Update existing 2D texture data without recreating the texture
publicAPI.update2DFromRaw = ({
data = requiredParam('data'),
dataType = requiredParam('dataType'),
flip = false,
} = {}) => {
if (!model.handle) {
vtkErrorMacro('No texture to update');
return false;
}

model._openGLRenderWindow.activateTexture(publicAPI);
publicAPI.bind();

const dataArray = [data];
const pixData = publicAPI.updateArrayDataTypeForGL(dataType, dataArray);
const scaledData = scaleTextureToHighestPowerOfTwo(pixData);

model.context.pixelStorei(model.context.UNPACK_FLIP_Y_WEBGL, flip);
model.context.pixelStorei(model.context.UNPACK_ALIGNMENT, 1);

model.context.texSubImage2D(
model.target,
0, // level
0, // xoffset
0, // yoffset
model.width,
model.height,
model.format,
model.openGLDataType,
scaledData[0]
);

if (model.generateMipmap) {
model.context.generateMipmap(model.target);
}

if (flip) {
model.context.pixelStorei(model.context.UNPACK_FLIP_Y_WEBGL, false);
}

publicAPI.deactivate();
return true;
};

//----------------------------------------------------------------------------
publicAPI.createCubeFromRaw = ({
width = requiredParam('width'),
Expand Down
95 changes: 95 additions & 0 deletions Sources/Rendering/OpenGL/Texture/test/testTextureDataUpdate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/* eslint-disable no-await-in-loop */
import test from 'tape';
import testUtils from 'vtk.js/Sources/Testing/testUtils';
import vtkOpenGLRenderWindow from 'vtk.js/Sources/Rendering/OpenGL/RenderWindow';
import vtkRenderWindow from 'vtk.js/Sources/Rendering/Core/RenderWindow';
import vtkRenderer from 'vtk.js/Sources/Rendering/Core/Renderer';
import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor';
import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper';
import vtkPlaneSource from 'vtk.js/Sources/Filters/Sources/PlaneSource';
import vtkTexture from 'vtk.js/Sources/Rendering/Core/Texture';
import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData';
import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';

test.onlyIfWebGL(
'Test Texture Data Update Without GL_INVALID_OPERATION',
async (t) => {
const gc = testUtils.createGarbageCollector();

// Helper function to check for GL errors
// Dont want to see GL_INVALID_OPERATION: glTexStorage2D: Texture is immutable.
function checkGLError(gl) {
const error = gl.getError();
if (error !== gl.NO_ERROR) {
const errorMsg =
error === gl.INVALID_OPERATION
? 'GL_INVALID_OPERATION detected'
: `GL Error ${error} (0x${error.toString(16)})`;
t.fail(errorMsg);
t.end();
return true;
}
return false;
}

// Create render setup
const container = gc.registerDOMElement(document.createElement('div'));
document.querySelector('body').appendChild(container);

const renderWindow = gc.registerResource(vtkRenderWindow.newInstance());
const renderer = gc.registerResource(vtkRenderer.newInstance());
renderWindow.addRenderer(renderer);

const glwindow = gc.registerResource(vtkOpenGLRenderWindow.newInstance());
glwindow.setContainer(container);
renderWindow.addView(glwindow);
glwindow.setSize(64, 64);

// Create textured plane
const plane = gc.registerResource(vtkPlaneSource.newInstance());
const mapper = gc.registerResource(vtkMapper.newInstance());
mapper.setInputConnection(plane.getOutputPort());

const actor = gc.registerResource(vtkActor.newInstance());
actor.setMapper(mapper);
renderer.addActor(actor);

// Create texture with red data
const texture = gc.registerResource(vtkTexture.newInstance());
const imageData = gc.registerResource(vtkImageData.newInstance());
imageData.setDimensions(64, 64, 1);

const data = new Uint8Array(64 * 64 * 3).fill(255); // red
const scalars = gc.registerResource(
vtkDataArray.newInstance({
numberOfComponents: 3,
values: data,
})
);
imageData.getPointData().setScalars(scalars);
texture.setInputData(imageData);
actor.addTexture(texture);

renderWindow.render();

// Update texture to green
data.fill(0);
for (let i = 1; i < data.length; i += 3) {
data[i] = 255; // green channel
}
scalars.modified();
imageData.modified();
texture.modified();

renderWindow.render();

// Check for GL errors
if (checkGLError(glwindow.getContext())) {
gc.releaseResources();
return;
}

t.pass('No GL errors occurred during texture update');
gc.releaseResources();
}
);