From 3b687a2a02e16f289de3a990996096c9d3d3a9cd Mon Sep 17 00:00:00 2001 From: Kirill Osipov Date: Tue, 2 Dec 2025 13:30:54 +0100 Subject: [PATCH] TSL: ShadowNode: prevent memory leaks. Add webgpu_test_memory (#32395) --- examples/files.json | 1 + examples/screenshots/webgpu_test_memory.jpg | Bin 0 -> 10831 bytes examples/webgpu_test_memory.html | 301 ++++++++++++++++++++ src/lights/DirectionalLight.js | 2 + src/lights/Light.js | 2 +- src/lights/PointLight.js | 2 + src/lights/SpotLight.js | 2 + src/nodes/lighting/AnalyticLightNode.js | 47 +++ src/nodes/lighting/ShadowFilterNode.js | 18 ++ src/nodes/lighting/ShadowNode.js | 4 +- test/e2e/puppeteer.js | 1 + 11 files changed, 378 insertions(+), 2 deletions(-) create mode 100644 examples/screenshots/webgpu_test_memory.jpg create mode 100644 examples/webgpu_test_memory.html diff --git a/examples/files.json b/examples/files.json index 065ccac7461dea..99d4bc835b326d 100644 --- a/examples/files.json +++ b/examples/files.json @@ -455,6 +455,7 @@ "webgpu_sprites", "webgpu_storage_buffer", "webgpu_struct_drawindirect", + "webgpu_test_memory", "webgpu_texturegrad", "webgpu_textures_2d-array", "webgpu_textures_2d-array_compressed", diff --git a/examples/screenshots/webgpu_test_memory.jpg b/examples/screenshots/webgpu_test_memory.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fbaa2cfd538661b43c73274be508816eb61cf56e GIT binary patch literal 10831 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∨OC}UDL&sOQD=J~5 zs?XX4R!CZ2PyIC9taS(3N)!8&vC%(}V&`FF8xu8II`&Ps3xR`;?P^r~GuJNBeo~H; zHzZ9;>~R}9??K;U91v0 zFrtpr%8w4ks#{f-lgr7JN^(Qoj9N20)}*J`O)Kn0<_6*n`~5Io^Y8zv>-V zqWgvU*^R{b4d;(P6{YNzpIc)FXee#W&xpf1NBtUZ>v)wfh^Q{x+*Y zm@*~Tsod8{`AURPSUB@_-f&CxOMJkjDCY7fyYJN2B(99zj5DIt75OwFB5hmRfZLx#tnv{$EH55Imh{uNsSZz^Y4R3 zq|$`o4ZgYeJ@>|`S6ah8)G?oANhqr^Sg$d8m0#5;!0 zFPYC4_tbv!nYnk0E*%pU(excllIbtO!EN?M1#E+Pu=kV+em|m9^B+L`3t)vHI(po+ zVL2YTqqmx$ezZYfL5{fgb4{jtE~KfhP8^+2rS_s1d!6TM!-PoCwT<|aE{zw)$Yujlsa$5V$6M1E{M z6nS2XkM)jycG zGYxn%H^9)vSCWah-=5JTX`Tsf|!> zz(&J9SI%XQKC@*KA>i1m*!^|3R*kllMBk$Nu@rs4c^0pKBH^x95ms{ow?8UycVJrm zTSL6;fP_WmVX^dO*j>gEWZg>3!?h=68$vryyD9g>+5OLSWSWO=g`9cZe5jFqs3$$~ zdV*!ul~W4V0#ATTV4W;dtJTAYmEWu#y&iJUM6{0-oiY)Ap>wytfAYqFOvuHJBa;GV z%IThsiH4bgBL9yBSn=y*YvpETr)MV6%_uqMNayjNl7}s5l$cw@82b5Zsaq9BZ7XVf z!-4_>z5FE50`2+cIj(}~ij-PP;=XPm;z^hy_c;&nAlxm$sx+XZkkYW(d4BYo@UGGy zsJ?u*O$CoUG%(e>_G2=g3;AL{u1lb(kE>qVmfVMg-5 zhkk;338gdjpoICj*n?5&YV~n3$+KSGpjLdr*UK%qdlrBjbm75Sg2 z0&rNeiRdE87@Eo6hhvjHjTpzV(97%NIO#w23s9OI{PL#e+j~cBHd;%--D@Z3jtO6; zSlEAq#__MTub09x#0zY&4#QM4;l{=G0%(^n>4&^R4EG+~7np=3e6`u~yRWhx*7G?k z8Hmw!fufZiV`vLAV)L1Hp)=rgOfYibIIk;}0Sx6PC$l?K4;PzfQeC_cO t8jMzr=;Oi_$kv$$`r&D9-{2;Ihe3Lk!Y`Lv0j|Y=1GN7Yw!zOE{5Oot#dZJy literal 0 HcmV?d00001 diff --git a/examples/webgpu_test_memory.html b/examples/webgpu_test_memory.html new file mode 100644 index 00000000000000..e604b93edae645 --- /dev/null +++ b/examples/webgpu_test_memory.html @@ -0,0 +1,301 @@ + + + + three.js webgpu - memory test I + + + + + + +
+ + +
+ three.jsWebGPU Memory Test I +
+ + + This example tests memory management with WebGPU renderer. +
Spheres are created, rendered, and disposed in each frame. +
+
+ + + + + + + diff --git a/src/lights/DirectionalLight.js b/src/lights/DirectionalLight.js index 6eb0da6b26c77c..ce9682fffec79b 100644 --- a/src/lights/DirectionalLight.js +++ b/src/lights/DirectionalLight.js @@ -80,6 +80,8 @@ class DirectionalLight extends Light { dispose() { + super.dispose(); + this.shadow.dispose(); } diff --git a/src/lights/Light.js b/src/lights/Light.js index 47b0b5004678f8..4d81b98fe10c5a 100644 --- a/src/lights/Light.js +++ b/src/lights/Light.js @@ -54,7 +54,7 @@ class Light extends Object3D { */ dispose() { - // Empty here in base class; some subclasses override. + this.dispatchEvent( { type: 'dispose' } ); } diff --git a/src/lights/PointLight.js b/src/lights/PointLight.js index 7e8255985cc769..d760abfa45f6ad 100644 --- a/src/lights/PointLight.js +++ b/src/lights/PointLight.js @@ -94,6 +94,8 @@ class PointLight extends Light { dispose() { + super.dispose(); + this.shadow.dispose(); } diff --git a/src/lights/SpotLight.js b/src/lights/SpotLight.js index eb93251abc651a..287437f654378a 100644 --- a/src/lights/SpotLight.js +++ b/src/lights/SpotLight.js @@ -147,6 +147,8 @@ class SpotLight extends Light { dispose() { + super.dispose(); + this.shadow.dispose(); } diff --git a/src/nodes/lighting/AnalyticLightNode.js b/src/nodes/lighting/AnalyticLightNode.js index 7fac36424e280a..46dc98d02560ef 100644 --- a/src/nodes/lighting/AnalyticLightNode.js +++ b/src/nodes/lighting/AnalyticLightNode.js @@ -96,6 +96,53 @@ class AnalyticLightNode extends LightingNode { */ this.updateType = NodeUpdateType.FRAME; + if ( light && light.shadow ) { + + this._shadowDisposeListener = () => { + + this.disposeShadow(); + + }; + + light.addEventListener( 'dispose', this._shadowDisposeListener ); + + } + + } + + dispose() { + + if ( this._shadowDisposeListener ) { + + this.light.removeEventListener( 'dispose', this._shadowDisposeListener ); + + } + + super.dispose(); + + } + + /** + * Frees internal resources related to shadows. + */ + disposeShadow() { + + if ( this.shadowNode !== null ) { + + this.shadowNode.dispose(); + this.shadowNode = null; + + } + + this.shadowColorNode = null; + + if ( this.baseColorNode !== null ) { + + this.colorNode = this.baseColorNode; + this.baseColorNode = null; + + } + } getHash() { diff --git a/src/nodes/lighting/ShadowFilterNode.js b/src/nodes/lighting/ShadowFilterNode.js index 37b477e0b47719..b41f42dc754dff 100644 --- a/src/nodes/lighting/ShadowFilterNode.js +++ b/src/nodes/lighting/ShadowFilterNode.js @@ -272,3 +272,21 @@ export const getShadowMaterial = ( light ) => { return material; }; + +/** + * Disposes the shadow material for the given light source. + * + * @param {Light} light - The light source. + */ +export const disposeShadowMaterial = ( light ) => { + + const material = shadowMaterialLib.get( light ); + + if ( material !== undefined ) { + + material.dispose(); + shadowMaterialLib.delete( light ); + + } + +}; diff --git a/src/nodes/lighting/ShadowNode.js b/src/nodes/lighting/ShadowNode.js index 03d4da5f4e7aaa..3ff442311ef53f 100644 --- a/src/nodes/lighting/ShadowNode.js +++ b/src/nodes/lighting/ShadowNode.js @@ -17,7 +17,7 @@ import { viewZToLogarithmicDepth } from '../display/ViewportDepthNode.js'; import { lightShadowMatrix } from '../accessors/Lights.js'; import { resetRendererAndSceneState, restoreRendererAndSceneState } from '../../renderers/common/RendererUtils.js'; import { getDataFromObject } from '../core/NodeUtils.js'; -import { getShadowMaterial, BasicShadowFilter, PCFShadowFilter, PCFSoftShadowFilter, VSMShadowFilter } from './ShadowFilterNode.js'; +import { getShadowMaterial, disposeShadowMaterial, BasicShadowFilter, PCFShadowFilter, PCFSoftShadowFilter, VSMShadowFilter } from './ShadowFilterNode.js'; import ChainMap from '../../renderers/common/ChainMap.js'; import { warn } from '../../utils.js'; import { textureSize } from '../accessors/TextureSizeNode.js'; @@ -748,6 +748,8 @@ class ShadowNode extends ShadowBaseNode { this._currentShadowType = null; + disposeShadowMaterial( this.light ); + if ( this.shadowMap ) { this.shadowMap.dispose(); diff --git a/test/e2e/puppeteer.js b/test/e2e/puppeteer.js index a909d55420b36b..356317afe559ca 100644 --- a/test/e2e/puppeteer.js +++ b/test/e2e/puppeteer.js @@ -35,6 +35,7 @@ const exceptionList = [ 'webgpu_postprocessing_sss', 'webgpu_postprocessing_traa', 'webgpu_reflection', + 'webgpu_test_memory', 'webgpu_texturegrad', 'webgpu_tsl_vfx_flames',