From 36c8e12ff2af8c1fa56ac11161e469338a8972c0 Mon Sep 17 00:00:00 2001 From: sunag Date: Sat, 20 Jun 2026 05:40:00 -0300 Subject: [PATCH 1/3] WebGPURenderer: Scope sampler caching to bindings to prevent sampler collision on shared (#33847) --- src/renderers/common/Backend.js | 5 ++--- src/renderers/common/Bindings.js | 4 ++-- src/renderers/common/Textures.js | 7 +++---- src/renderers/webgl-fallback/WebGLBackend.js | 5 ++--- src/renderers/webgpu/WebGPUBackend.js | 7 +++---- .../webgpu/utils/WebGPUBindingUtils.js | 4 ++-- .../webgpu/utils/WebGPUTextureUtils.js | 21 ++++++++++--------- 7 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/renderers/common/Backend.js b/src/renderers/common/Backend.js index 1e0e661dcbe2e1..3bf5b8e2ffa87b 100644 --- a/src/renderers/common/Backend.js +++ b/src/renderers/common/Backend.js @@ -284,11 +284,10 @@ class Backend { * Updates a GPU sampler for the given texture. * * @abstract - * @param {Texture} texture - The texture to update the sampler for. - * @param {TextureNode} textureNode - The texture node to update the sampler with. + * @param {Sampler} binding - The sampler binding to update. * @return {string} The current sampler key. */ - updateSampler( /*texture, textureNode*/ ) { } + updateSampler( /*binding*/ ) { } /** * Creates a default texture for the given texture that can be used diff --git a/src/renderers/common/Bindings.js b/src/renderers/common/Bindings.js index d6c16863e372fa..e5430d96b73e54 100644 --- a/src/renderers/common/Bindings.js +++ b/src/renderers/common/Bindings.js @@ -210,7 +210,7 @@ class Bindings extends DataMap { } else if ( binding.isSampler ) { - this.textures.updateSampler( binding.texture, binding.textureNode ); + this.textures.updateSampler( binding ); } else if ( binding.isStorageBuffer ) { @@ -422,7 +422,7 @@ class Bindings extends DataMap { if ( updated ) { - const samplerKey = this.textures.updateSampler( binding.texture, binding.textureNode ); + const samplerKey = this.textures.updateSampler( binding ); if ( binding.samplerKey !== samplerKey ) { diff --git a/src/renderers/common/Textures.js b/src/renderers/common/Textures.js index 4c6479055dd3fe..12c35deb26c1a3 100644 --- a/src/renderers/common/Textures.js +++ b/src/renderers/common/Textures.js @@ -426,13 +426,12 @@ class Textures extends DataMap { * In WebGPU, samplers are objects like textures and it's possible to share * them when the texture parameters match. * - * @param {Texture} texture - The texture to update the sampler for. - * @param {TextureNode} textureNode - The texture node to update the sampler with. + * @param {Sampler} binding - The sampler binding to update. * @return {string} The current sampler key. */ - updateSampler( texture, textureNode ) { + updateSampler( binding ) { - return this.backend.updateSampler( texture, textureNode ); + return this.backend.updateSampler( binding ); } diff --git a/src/renderers/webgl-fallback/WebGLBackend.js b/src/renderers/webgl-fallback/WebGLBackend.js index bd8bd9d075bca7..68c2c6942b23a5 100644 --- a/src/renderers/webgl-fallback/WebGLBackend.js +++ b/src/renderers/webgl-fallback/WebGLBackend.js @@ -1429,11 +1429,10 @@ class WebGLBackend extends Backend { /** * This method does nothing since WebGL 2 has no concept of samplers. * - * @param {Texture} texture - The texture to update the sampler for. - * @param {TextureNode} textureNode - The texture node to update the sampler with. + * @param {Sampler} binding - The sampler binding to update. * @return {string} The current sampler key. */ - updateSampler( /*texture, textureNode*/ ) { + updateSampler( /*binding*/ ) { return ''; diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index 91ef251aaa118b..36f17aad17b087 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -2147,13 +2147,12 @@ class WebGPUBackend extends Backend { /** * Updates a GPU sampler for the given texture. * - * @param {Texture} texture - The texture to update the sampler for. - * @param {TextureNode} textureNode - The texture node to update the sampler with. + * @param {Sampler} binding - The sampler binding to update. * @return {string} The current sampler key. */ - updateSampler( texture, textureNode ) { + updateSampler( binding ) { - return this.textureUtils.updateSampler( texture, textureNode ); + return this.textureUtils.updateSampler( binding ); } diff --git a/src/renderers/webgpu/utils/WebGPUBindingUtils.js b/src/renderers/webgpu/utils/WebGPUBindingUtils.js index 696fbc7e5a906f..da4bd473bd8df0 100644 --- a/src/renderers/webgpu/utils/WebGPUBindingUtils.js +++ b/src/renderers/webgpu/utils/WebGPUBindingUtils.js @@ -392,9 +392,9 @@ class WebGPUBindingUtils { } else if ( binding.isSampler ) { - const textureGPU = backend.get( binding.texture ); + const bindingData = backend.get( binding ); - _bindGroupDescriptor.entries.push( { binding: bindingPoint, resource: textureGPU.sampler } ); + _bindGroupDescriptor.entries.push( { binding: bindingPoint, resource: bindingData.sampler } ); } diff --git a/src/renderers/webgpu/utils/WebGPUTextureUtils.js b/src/renderers/webgpu/utils/WebGPUTextureUtils.js index a0c470c50708ac..59f929095278a2 100644 --- a/src/renderers/webgpu/utils/WebGPUTextureUtils.js +++ b/src/renderers/webgpu/utils/WebGPUTextureUtils.js @@ -148,13 +148,14 @@ class WebGPUTextureUtils { /** * Creates a GPU sampler for the given texture. * - * @param {Texture} texture - The texture to create the sampler for. - * @param {TextureNode} textureNode - The texture node to update the sampler with. + * @param {Sampler} binding - The sampler binding to update. * @return {string} The current sampler key. */ - updateSampler( texture, textureNode ) { + updateSampler( binding ) { const backend = this.backend; + const texture = binding.texture; + const textureNode = binding.textureNode; const samplerKey = texture.minFilter + '-' + texture.magFilter + '-' + texture.wrapS + '-' + texture.wrapT + '-' + ( texture.wrapR || '0' ) + '-' + @@ -205,20 +206,20 @@ class WebGPUTextureUtils { } - const textureData = backend.get( texture ); + const bindingData = backend.get( binding ); - if ( textureData.sampler !== samplerData.sampler ) { + if ( bindingData.sampler !== samplerData.sampler ) { // check if previous sampler is unused so it can be deleted - if ( textureData.sampler !== undefined ) { + if ( bindingData.sampler !== undefined ) { - const oldSamplerData = this._samplerCache.get( textureData.samplerKey ); + const oldSamplerData = this._samplerCache.get( bindingData.samplerKey ); oldSamplerData.usedTimes --; if ( oldSamplerData.usedTimes === 0 ) { - this._samplerCache.delete( textureData.samplerKey ); + this._samplerCache.delete( bindingData.samplerKey ); } @@ -226,8 +227,8 @@ class WebGPUTextureUtils { // update to new sampler data - textureData.samplerKey = samplerKey; - textureData.sampler = samplerData.sampler; + bindingData.samplerKey = samplerKey; + bindingData.sampler = samplerData.sampler; samplerData.usedTimes ++; From 213697e3d5207c3a968ce6aa500cb625cd5300ec Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Sat, 20 Jun 2026 11:15:49 +0200 Subject: [PATCH 2/3] PMREMNode: Fix dispose of internal PMREMs. (#33849) --- src/nodes/pmrem/PMREMNode.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/nodes/pmrem/PMREMNode.js b/src/nodes/pmrem/PMREMNode.js index 44b35fb104079a..fc65e547aab918 100644 --- a/src/nodes/pmrem/PMREMNode.js +++ b/src/nodes/pmrem/PMREMNode.js @@ -81,6 +81,31 @@ function _getPMREMFromTexture( texture, renderer, generator ) { cacheTexture.pmremVersion = texture.pmremVersion; + // add dispose event listener for new PMREMs + + if ( cache.has( texture ) === false ) { + + const onDispose = () => { + + texture.removeEventListener( 'dispose', onDispose ); + + const pmrem = cache.get( texture ); + + if ( pmrem !== undefined ) { + + pmrem.dispose(); + cache.delete( texture ); + + } + + }; + + texture.addEventListener( 'dispose', onDispose ); + + } + + // + cache.set( texture, cacheTexture ); } From ad321f20e17b9af5489ded625154db5b5ff92d1b Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Sat, 20 Jun 2026 11:59:47 +0200 Subject: [PATCH 3/3] WebGPURenderer: Improve Sampler disposal. (#33850) --- src/renderers/common/Backend.js | 8 +++ src/renderers/common/Bindings.js | 6 +++ src/renderers/common/Textures.js | 6 +++ src/renderers/webgpu/WebGPUBackend.js | 11 ++++ .../webgpu/utils/WebGPUTextureUtils.js | 53 ++++++++++++++----- 5 files changed, 71 insertions(+), 13 deletions(-) diff --git a/src/renderers/common/Backend.js b/src/renderers/common/Backend.js index 3bf5b8e2ffa87b..867f222e3e55ac 100644 --- a/src/renderers/common/Backend.js +++ b/src/renderers/common/Backend.js @@ -289,6 +289,14 @@ class Backend { */ updateSampler( /*binding*/ ) { } + /** + * Frees the GPU sampler for the given sampler binding. + * + * @abstract + * @param {Sampler} binding - The sampler binding to free. + */ + destroySampler( /*binding*/ ) { } + /** * Creates a default texture for the given texture that can be used * as a placeholder until the actual texture is ready for usage. diff --git a/src/renderers/common/Bindings.js b/src/renderers/common/Bindings.js index e5430d96b73e54..5f3d213a016b37 100644 --- a/src/renderers/common/Bindings.js +++ b/src/renderers/common/Bindings.js @@ -267,6 +267,12 @@ class Bindings extends DataMap { } else if ( binding.isSampler ) { + if ( binding.isSampledTexture !== true ) { + + this.backend.destroySampler( binding ); + + } + binding.release(); } diff --git a/src/renderers/common/Textures.js b/src/renderers/common/Textures.js index 12c35deb26c1a3..f42b90eac69ca0 100644 --- a/src/renderers/common/Textures.js +++ b/src/renderers/common/Textures.js @@ -624,6 +624,12 @@ class Textures extends DataMap { if ( binding.isSampler && binding.texture === texture ) { + if ( binding.isSampledTexture !== true ) { + + this.backend.destroySampler( binding ); + + } + binding.reset(); binding.release(); diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index 36f17aad17b087..06e203693ed9b1 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -2156,6 +2156,17 @@ class WebGPUBackend extends Backend { } + /** + * Frees the GPU sampler for the given sampler binding. + * + * @param {Sampler} binding - The sampler binding to free. + */ + destroySampler( binding ) { + + this.textureUtils.destroySampler( binding ); + + } + /** * Creates a default texture for the given texture that can be used * as a placeholder until the actual texture is ready for usage. diff --git a/src/renderers/webgpu/utils/WebGPUTextureUtils.js b/src/renderers/webgpu/utils/WebGPUTextureUtils.js index 59f929095278a2..ff8ee2dbace281 100644 --- a/src/renderers/webgpu/utils/WebGPUTextureUtils.js +++ b/src/renderers/webgpu/utils/WebGPUTextureUtils.js @@ -210,20 +210,9 @@ class WebGPUTextureUtils { if ( bindingData.sampler !== samplerData.sampler ) { - // check if previous sampler is unused so it can be deleted + // release the previous sampler (if any) so it can be deleted when unused - if ( bindingData.sampler !== undefined ) { - - const oldSamplerData = this._samplerCache.get( bindingData.samplerKey ); - oldSamplerData.usedTimes --; - - if ( oldSamplerData.usedTimes === 0 ) { - - this._samplerCache.delete( bindingData.samplerKey ); - - } - - } + this._releaseSampler( bindingData ); // update to new sampler data @@ -238,6 +227,44 @@ class WebGPUTextureUtils { } + /** + * Frees the GPU sampler referenced by the given sampler binding. + * + * @param {Sampler} binding - The sampler binding to free. + */ + destroySampler( binding ) { + + this._releaseSampler( this.backend.get( binding ) ); + + } + + /** + * Releases the pooled sampler referenced by the given binding data and + * removes it from the cache when no binding references it anymore. + * + * @private + * @param {Object} bindingData - The binding data holding the sampler reference. + */ + _releaseSampler( bindingData ) { + + if ( bindingData.sampler !== undefined ) { + + const samplerData = this._samplerCache.get( bindingData.samplerKey ); + samplerData.usedTimes --; + + if ( samplerData.usedTimes === 0 ) { + + this._samplerCache.delete( bindingData.samplerKey ); + + } + + bindingData.sampler = undefined; + bindingData.samplerKey = undefined; + + } + + } + /** * Creates a default texture for the given texture that can be used * as a placeholder until the actual texture is ready for usage.