Skip to content
Open
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
6 changes: 6 additions & 0 deletions Sources/Rendering/OpenGL/Helper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ function vtkOpenGLHelper(publicAPI, model) {

publicAPI.setOpenGLRenderWindow = (win) => {
model.context = win.getContext();
if (!model.program) {
// A mapper clears the program on shader compile/bind failure; restore an
// empty one (handle 0) so getNeedToRebuildShaders re-arms instead of
// dereferencing null on the next pass.
model.program = vtkShaderProgram.newInstance();
}
model.program.setContext(model.context);
model.VAO.setOpenGLRenderWindow(win);
model.CABO.setOpenGLRenderWindow(win);
Expand Down
35 changes: 35 additions & 0 deletions Sources/Rendering/OpenGL/Helper/test/testNullProgramRecovery.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { it, expect } from 'vitest';

import vtkHelper from 'vtk.js/Sources/Rendering/OpenGL/Helper';

it('restores an empty program after a mapper clears it on shader failure', () => {
const helper = vtkHelper.newInstance();
const gl = {};
const win = { getContext: () => gl };

helper.setOpenGLRenderWindow(win);
// updateShaders clears the program when a shader fails to compile or bind
helper.setProgram(null);

// the next render pass must not dereference the null program
expect(() => helper.setOpenGLRenderWindow(win)).not.toThrow();

// an empty program (handle 0) re-arms getNeedToRebuildShaders so the
// mapper retries the shader instead of leaving the viewport dead
const program = helper.getProgram();
expect(program.getHandle()).toBe(0);
expect(program.getCompiled()).toBe(false);
expect(program.getVertexShader().getContext()).toBe(gl);
});

it('keeps the existing program when it was not cleared', () => {
const helper = vtkHelper.newInstance();
const win = { getContext: () => ({}) };
const program = { setContext() {} };

helper.setProgram(program);
helper.setOpenGLRenderWindow(win);

// healthy frames must not discard a compiled program every pass
expect(helper.getProgram()).toBe(program);
});
4 changes: 4 additions & 0 deletions Sources/Rendering/OpenGL/ShaderProgram/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,11 @@ function vtkShaderProgram(publicAPI, model) {
if (!isCompiled) {
const lastError = model.context.getProgramInfoLog(model.handle);
vtkErrorMacro(`Error linking shader ${lastError}`);
// Deleting the program detaches its shaders
model.context.deleteProgram(model.handle);
model.handle = 0;
model.vertexShaderHandle = 0;
model.fragmentShaderHandle = 0;
return false;
}

Expand Down
45 changes: 45 additions & 0 deletions Sources/Rendering/OpenGL/ShaderProgram/test/testFailedLink.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { it, expect, vi } from 'vitest';

import vtkShaderProgram from 'vtk.js/Sources/Rendering/OpenGL/ShaderProgram';

function createContext() {
let nextHandle = 1;
return {
VERTEX_SHADER: 'VERTEX_SHADER',
FRAGMENT_SHADER: 'FRAGMENT_SHADER',
createShader: () => nextHandle++,
shaderSource() {},
compileShader() {},
getShaderParameter: () => true,
deleteShader() {},
createProgram: () => nextHandle++,
attachShader() {},
detachShader: vi.fn(),
linkProgram() {},
getProgramParameter: vi.fn(() => true),
getProgramInfoLog: () => 'link failed',
deleteProgram: vi.fn(),
};
}

it('deletes the program when its link fails so a retry starts clean', () => {
const gl = createContext();
const program = vtkShaderProgram.newInstance();
program.setContext(gl);
program.getVertexShader().setSource('void main() {}');
program.getFragmentShader().setSource('void main() {}');

vi.spyOn(console, 'error').mockImplementation(() => {});
gl.getProgramParameter.mockReturnValueOnce(false);
expect(program.compileShader()).toBe(0);
const failedHandle = gl.deleteProgram.mock.calls[0]?.[0];
expect(failedHandle).toBeTruthy();
expect(program.getHandle()).toBe(0);

// the retry creates a new program; detaching the old shaders from it is
// INVALID_OPERATION, so the stale attachment handles must be cleared
expect(program.compileShader()).toBe(1);
expect(program.getHandle()).not.toBe(failedHandle);
expect(gl.detachShader).not.toHaveBeenCalled();
vi.restoreAllMocks();
});
Loading