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 {