From 10e8ae40b8bdac2421a7673c1f557f88ecb766a8 Mon Sep 17 00:00:00 2001 From: Sanchit2662 Date: Thu, 15 Jan 2026 16:53:28 +0530 Subject: [PATCH 1/3] 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 { From 72db966d488edc9a69413b816279c5267a9ef93a Mon Sep 17 00:00:00 2001 From: Sanchit2662 Date: Sun, 25 Jan 2026 05:25:42 +0530 Subject: [PATCH 2/3] Fix WebGL cleanup for createGraphics contexts Signed-off-by: Sanchit2662 --- src/core/p5.Graphics.js | 6 ++++++ src/webgl/p5.RendererGL.js | 14 ++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index 07ddb54301..9a28a1d560 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -380,6 +380,12 @@ p5.Graphics = class extends p5.Element { * */ remove() { + // Clean up WebGL resources if the renderer has a remove method + // (WebGL renderers need to free GPU resources like shaders and textures) + if (this._renderer && typeof this._renderer.remove === 'function') { + this._renderer.remove(); + } + if (this.elt.parentNode) { this.elt.parentNode.removeChild(this.elt); } diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index e25640aa39..b5ce362fa1 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -682,17 +682,23 @@ p5.RendererGL = class RendererGL extends p5.Renderer { this._curShader = undefined; // Register cleanup hook to free WebGL resources when sketch is removed - this._pInst.registerMethod('remove', this._cleanupWebGLResources.bind(this)); + // Only register if this is the main p5 instance (not a p5.Graphics) + // For p5.Graphics, cleanup is called directly from p5.Graphics.remove() + const isPGraphics = this._pInst instanceof p5.Graphics; + if (!isPGraphics && this._pInst && typeof this._pInst.registerMethod === 'function') { + this._pInst.registerMethod('remove', this.remove.bind(this)); + } } /** * Frees all WebGL resources (shaders, textures, buffers) associated with - * this renderer. Called automatically when the p5 instance is removed. + * this renderer. Called automatically when the p5 instance is removed, + * or when a p5.Graphics object is removed. * - * @method _cleanupWebGLResources + * @method remove * @private */ - _cleanupWebGLResources() { + remove() { // Dispose all cached shaders const shadersToDispose = [ this._defaultLightShader, From 953eeab027e8977eb8430b446a515657eea20c8c Mon Sep 17 00:00:00 2001 From: Sanchit2662 Date: Sun, 25 Jan 2026 21:35:30 +0530 Subject: [PATCH 3/3] Rename dispose() to remove() for API consistency Signed-off-by: Sanchit2662 --- src/webgl/p5.RendererGL.js | 28 ++++++++++++++-------------- src/webgl/p5.Shader.js | 6 +++--- src/webgl/p5.Texture.js | 4 ++-- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index b5ce362fa1..27583e036a 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -699,8 +699,8 @@ p5.RendererGL = class RendererGL extends p5.Renderer { * @private */ remove() { - // Dispose all cached shaders - const shadersToDispose = [ + // Remove all cached shaders + const shadersToRemove = [ this._defaultLightShader, this._defaultImmediateModeShader, this._defaultNormalShader, @@ -715,25 +715,25 @@ p5.RendererGL = class RendererGL extends p5.Renderer { this.filterShader ]; - // Also dispose filter shaders + // Also add filter shaders if (this.defaultFilterShaders) { for (const key in this.defaultFilterShaders) { - shadersToDispose.push(this.defaultFilterShaders[key]); + shadersToRemove.push(this.defaultFilterShaders[key]); } } - // Dispose each shader - for (const shader of shadersToDispose) { - if (shader && typeof shader.dispose === 'function') { - shader.dispose(); + // Remove each shader + for (const shader of shadersToRemove) { + if (shader && typeof shader.remove === 'function') { + shader.remove(); } } - // Dispose all cached textures + // Remove all cached textures if (this.textures) { for (const texture of this.textures.values()) { - if (texture && typeof texture.dispose === 'function') { - texture.dispose(); + if (texture && typeof texture.remove === 'function') { + texture.remove(); } } this.textures.clear(); @@ -768,10 +768,10 @@ p5.RendererGL = class RendererGL extends p5.Renderer { this.specularTextures.clear(); } - // Dispose empty texture singleton + // Remove empty texture singleton if (this._emptyTexture) { - if (typeof this._emptyTexture.dispose === 'function') { - this._emptyTexture.dispose(); + if (typeof this._emptyTexture.remove === 'function') { + this._emptyTexture.remove(); } this._emptyTexture = null; } diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index f37400ac9c..7a5b38fe4e 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -1500,12 +1500,12 @@ p5.Shader = class { * 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 + * @method remove * @private */ - dispose() { + remove() { if (this._glProgram === 0) { - return; // Already disposed or never initialized + return; // Already removed or never initialized } const gl = this._renderer.GL; diff --git a/src/webgl/p5.Texture.js b/src/webgl/p5.Texture.js index 613bc83c68..f8fc5473aa 100644 --- a/src/webgl/p5.Texture.js +++ b/src/webgl/p5.Texture.js @@ -460,10 +460,10 @@ p5.Texture = class 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 + * @method remove * @private */ - dispose() { + remove() { // FramebufferTextures are managed by their parent Framebuffer if (this.isFramebufferTexture || this.glTex === undefined) { return;