diff --git a/Sources/Rendering/OpenGL/Helper/index.js b/Sources/Rendering/OpenGL/Helper/index.js index f025fdddb58..96dc8fccc26 100644 --- a/Sources/Rendering/OpenGL/Helper/index.js +++ b/Sources/Rendering/OpenGL/Helper/index.js @@ -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); diff --git a/Sources/Rendering/OpenGL/Helper/test/testNullProgramRecovery.js b/Sources/Rendering/OpenGL/Helper/test/testNullProgramRecovery.js new file mode 100644 index 00000000000..19824ad02f8 --- /dev/null +++ b/Sources/Rendering/OpenGL/Helper/test/testNullProgramRecovery.js @@ -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); +}); diff --git a/Sources/Rendering/OpenGL/ShaderProgram/index.js b/Sources/Rendering/OpenGL/ShaderProgram/index.js index 83d27aa9539..607aa9e5047 100644 --- a/Sources/Rendering/OpenGL/ShaderProgram/index.js +++ b/Sources/Rendering/OpenGL/ShaderProgram/index.js @@ -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; } diff --git a/Sources/Rendering/OpenGL/ShaderProgram/test/testFailedLink.js b/Sources/Rendering/OpenGL/ShaderProgram/test/testFailedLink.js new file mode 100644 index 00000000000..c0aac03d8ee --- /dev/null +++ b/Sources/Rendering/OpenGL/ShaderProgram/test/testFailedLink.js @@ -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(); +});