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 cac5528a62..27583e036a 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -680,6 +680,133 @@ p5.RendererGL = class RendererGL extends p5.Renderer { this.fontInfos = {}; this._curShader = undefined; + + // Register cleanup hook to free WebGL resources when sketch is removed + // 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, + * or when a p5.Graphics object is removed. + * + * @method remove + * @private + */ + remove() { + // Remove all cached shaders + const shadersToRemove = [ + 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 add filter shaders + if (this.defaultFilterShaders) { + for (const key in this.defaultFilterShaders) { + shadersToRemove.push(this.defaultFilterShaders[key]); + } + } + + // Remove each shader + for (const shader of shadersToRemove) { + if (shader && typeof shader.remove === 'function') { + shader.remove(); + } + } + + // Remove all cached textures + if (this.textures) { + for (const texture of this.textures.values()) { + if (texture && typeof texture.remove === 'function') { + texture.remove(); + } + } + 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(); + } + + // Remove empty texture singleton + if (this._emptyTexture) { + if (typeof this._emptyTexture.remove === 'function') { + this._emptyTexture.remove(); + } + 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..7a5b38fe4e 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 remove + * @private + */ + remove() { + if (this._glProgram === 0) { + return; // Already removed 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..f8fc5473aa 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 remove + * @private + */ + remove() { + // 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 {