From 10e8ae40b8bdac2421a7673c1f557f88ecb766a8 Mon Sep 17 00:00:00 2001 From: Sanchit2662 Date: Thu, 15 Jan 2026 16:53:28 +0530 Subject: [PATCH] Fix WebGL shader and texture memory leak on sketch removal Add dispose() methods to p5.Shader and p5.Texture classes and register cleanup hook in p5.RendererGL to free GPU resources when remove() is called. Signed-off-by: Sanchit2662 --- src/webgl/p5.RendererGL.js | 121 +++++++++++++++++++++++++++++++++++++ src/webgl/p5.Shader.js | 47 ++++++++++++++ src/webgl/p5.Texture.js | 20 ++++++ 3 files changed, 188 insertions(+) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index cac5528a62..e25640aa39 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -680,6 +680,127 @@ p5.RendererGL = class RendererGL extends p5.Renderer { this.fontInfos = {}; this._curShader = undefined; + + // Register cleanup hook to free WebGL resources when sketch is removed + this._pInst.registerMethod('remove', this._cleanupWebGLResources.bind(this)); + } + + /** + * Frees all WebGL resources (shaders, textures, buffers) associated with + * this renderer. Called automatically when the p5 instance is removed. + * + * @method _cleanupWebGLResources + * @private + */ + _cleanupWebGLResources() { + // Dispose all cached shaders + const shadersToDispose = [ + this._defaultLightShader, + this._defaultImmediateModeShader, + this._defaultNormalShader, + this._defaultColorShader, + this._defaultPointShader, + this.userFillShader, + this.userStrokeShader, + this.userPointShader, + this._curShader, + this.specularShader, + this.diffusedShader, + this.filterShader + ]; + + // Also dispose filter shaders + if (this.defaultFilterShaders) { + for (const key in this.defaultFilterShaders) { + shadersToDispose.push(this.defaultFilterShaders[key]); + } + } + + // Dispose each shader + for (const shader of shadersToDispose) { + if (shader && typeof shader.dispose === 'function') { + shader.dispose(); + } + } + + // Dispose all cached textures + if (this.textures) { + for (const texture of this.textures.values()) { + if (texture && typeof texture.dispose === 'function') { + texture.dispose(); + } + } + this.textures.clear(); + } + + // Remove all framebuffers (they have their own remove() method) + if (this.framebuffers) { + for (const fb of this.framebuffers) { + if (fb && typeof fb.remove === 'function') { + fb.remove(); + } + } + this.framebuffers.clear(); + } + + // Clean up diffused and specular texture caches (these store framebuffers) + if (this.diffusedTextures) { + for (const fb of this.diffusedTextures.values()) { + if (fb && typeof fb.remove === 'function') { + fb.remove(); + } + } + this.diffusedTextures.clear(); + } + + if (this.specularTextures) { + for (const fb of this.specularTextures.values()) { + if (fb && typeof fb.remove === 'function') { + fb.remove(); + } + } + this.specularTextures.clear(); + } + + // Dispose empty texture singleton + if (this._emptyTexture) { + if (typeof this._emptyTexture.dispose === 'function') { + this._emptyTexture.dispose(); + } + this._emptyTexture = null; + } + + // Free all retained mode geometry buffers + if (this.retainedMode && this.retainedMode.geometry) { + for (const gId in this.retainedMode.geometry) { + this._freeBuffers(gId); + } + } + + // Clean up filter layers + if (this.filterLayer && typeof this.filterLayer.remove === 'function') { + this.filterLayer.remove(); + this.filterLayer = undefined; + } + if (this.filterLayerTemp && typeof this.filterLayerTemp.remove === 'function') { + this.filterLayerTemp.remove(); + this.filterLayerTemp = undefined; + } + + // Clear shader references + this._defaultLightShader = undefined; + this._defaultImmediateModeShader = undefined; + this._defaultNormalShader = undefined; + this._defaultColorShader = undefined; + this._defaultPointShader = undefined; + this.userFillShader = undefined; + this.userStrokeShader = undefined; + this.userPointShader = undefined; + this._curShader = undefined; + this.specularShader = undefined; + this.diffusedShader = undefined; + this.filterShader = undefined; + this.defaultFilterShaders = {}; } /** diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index a82f112361..f37400ac9c 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -1492,6 +1492,53 @@ p5.Shader = class { } } } + + /** + * Frees the GPU resources associated with this shader. + * + * This method deletes the vertex shader, fragment shader, and shader program + * from GPU memory. Call this when you no longer need the shader to prevent + * memory leaks, especially when creating and destroying multiple p5 instances. + * + * @method dispose + * @private + */ + dispose() { + if (this._glProgram === 0) { + return; // Already disposed or never initialized + } + + const gl = this._renderer.GL; + + // Unbind if currently bound + if (this._bound) { + this.unbindShader(); + } + + // Detach shaders from program before deletion + if (this._vertShader !== -1) { + gl.detachShader(this._glProgram, this._vertShader); + gl.deleteShader(this._vertShader); + this._vertShader = -1; + } + + if (this._fragShader !== -1) { + gl.detachShader(this._glProgram, this._fragShader); + gl.deleteShader(this._fragShader); + this._fragShader = -1; + } + + // Delete the program + gl.deleteProgram(this._glProgram); + this._glProgram = 0; + + // Clear cached data + this._loadedAttributes = false; + this._loadedUniforms = false; + this.attributes = {}; + this.uniforms = {}; + this.samplers = []; + } }; export default p5.Shader; diff --git a/src/webgl/p5.Texture.js b/src/webgl/p5.Texture.js index 04b59b487d..613bc83c68 100644 --- a/src/webgl/p5.Texture.js +++ b/src/webgl/p5.Texture.js @@ -453,6 +453,26 @@ p5.Texture = class Texture { gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, this.glWrapT); this.unbindTexture(); } + + /** + * Frees the GPU resources associated with this texture. + * + * This method deletes the WebGL texture from GPU memory. Call this when + * you no longer need the texture to prevent memory leaks. + * + * @method dispose + * @private + */ + dispose() { + // FramebufferTextures are managed by their parent Framebuffer + if (this.isFramebufferTexture || this.glTex === undefined) { + return; + } + + const gl = this._renderer.GL; + gl.deleteTexture(this.glTex); + this.glTex = undefined; + } }; export class MipmapTexture extends p5.Texture {