Skip to content

Conversation

@querielo
Copy link
Contributor

@querielo querielo commented Nov 27, 2025

Summary

Prevent memory leaks caused by ShadowNode instances and their associated resources by:

  • Ensuring LightsNode disposes obsolete ShadowNode instances when lights change and when the node itself is disposed.
  • Resetting internal caches so RenderObjects and their references to meshes can be garbage collected.

The PR partially resolves the issue. At least for now, the GC successfully deallocates meshes when I recreate the light sources.


Background / Problem

A ShadowNode can have associated runtime resources (shadow materials, textures, RenderObject's created for different light nodes)

If we do not dispose ShadowNode instances:

  • Their associated shadow materials are never disposed.
  • This implies their render objects are also never disposed.
  • These render objects keep references to all meshes they were created for.
  • Even if the renderer no longer uses these nodes in renderer._nodes, those meshes remain indirectly reachable via nodes.nodeBuilderCache.

So, as long as a ShadowNode sticks around, the JS GC cannot reclaim:

  • Shadow materials
  • Render objects
  • Meshes referenced by those render objects (geometry, materials, textures)

This makes long-running apps slowly accumulate “dead” meshes, materials, geometries in memory.


There is a bigger problem

WebGPURenderer: Right now it’s too hard to clean memory of meshes, even if the user explicitly disposes geometry, materials and textures and remove references.

UPD: here is an issue about it: WebGPURenderer: Memory Leaks

The reason is:

  • A mesh “does not know” with which materials it was rendered (scene.overrideMaterial)
  • Each PassNode / override material can create one or more RenderObjects behind the scenes.
  • Those RenderObjects can only be destroyed when the corresponding material is disposed.
  • As a result, it’s not enough to dispose just the resources (geometry, materials, textures).
    You must also dispose all nodes that use overrideMaterial (including ShadowNode and PP nodes), or RenderObjects will stay alive and keep meshes strongly referenced.

There is still a problem that a user have to recreate PP and light sources to clean references on removed meshes so GC can deallocate removed meshes.

@sunag @Mugen87 Should I create an issue?

@querielo querielo changed the title LST: prevent memory leaks caused by LightNodes LST: prevent memory leaks caused by LightsNode Nov 27, 2025
@github-actions
Copy link

github-actions bot commented Nov 27, 2025

📦 Bundle size

Full ESM build, minified and gzipped.

Before After Diff
WebGL 350.43
83.06
350.43
83.06
+0 B
+0 B
WebGPU 615.47
170.89
616.03
171
+560 B
+109 B
WebGPU Nodes 614.08
170.63
614.64
170.74
+560 B
+111 B

🌳 Bundle size after tree-shaking

Minimal build including a renderer, camera, empty scene, and dependencies.

Before After Diff
WebGL 482.47
117.86
482.47
117.86
+0 B
+0 B
WebGPU 686.81
186.65
687.42
186.76
+615 B
+108 B
WebGPU Nodes 636.65
173.84
637.26
173.94
+615 B
+108 B

@querielo querielo changed the title LST: prevent memory leaks caused by LightsNode TSL: prevent memory leaks caused by LightsNode Nov 27, 2025
@sunag
Copy link
Collaborator

sunag commented Nov 28, 2025

@sunag @Mugen87 Should I create an issue?

If there is a simple way to reproduce this problem, it will certainly help us identify the best way to fix it.

@querielo querielo marked this pull request as draft November 28, 2025 09:09
@querielo querielo marked this pull request as ready for review November 28, 2025 12:20
Copilot AI review requested due to automatic review settings November 28, 2025 12:20
Copilot finished reviewing on behalf of querielo November 28, 2025 12:25
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses memory leaks in the WebGPU renderer related to LightNode instances and their associated shadow resources. The changes ensure proper disposal of shadow materials, textures, and render objects when lights are disposed or modified, preventing memory accumulation in long-running applications.

Key Changes:

  • Added disposal mechanism for shadow materials through disposeShadowMaterial function
  • Implemented event-driven cleanup where AnalyticLightNode responds to shadow disposal events
  • Extended LightShadow to dispatch disposal events via EventDispatcher

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/nodes/lighting/ShadowFilterNode.js Added disposeShadowMaterial function to properly dispose shadow materials and remove them from the material library
src/nodes/lighting/ShadowNode.js Integrated disposeShadowMaterial call in the _reset() method to clean up shadow materials during node disposal
src/nodes/lighting/AnalyticLightNode.js Added event listener for shadow disposal and disposeShadow() method to clean up shadow-related nodes and restore color nodes
src/lights/LightShadow.js Extended EventDispatcher to enable disposal event dispatching, allowing dependent nodes to respond to shadow cleanup
examples/webgpu_test_memory.html Added memory test example demonstrating proper disposal patterns for meshes, lights, and post-processing effects
examples/screenshots/webgpu_test_memory.jpg Screenshot for the new memory test example
examples/files.json Registered the new memory test example in the examples list
test/e2e/puppeteer.js Added the new example to the test exception list for investigation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.

@querielo
Copy link
Contributor Author

querielo commented Nov 28, 2025

@sunag I’ve just updated the PR.

The next video was recorded inside the dev branch. As you can see, there’s no way for the GC to collect the unused memory after enabling and disabling shadows, then recreating the shadow light.

Video (dev branch):

output2.mp4

Here is the behavior in the PR branch:

output1.mp4

I'll create an issue for this. It would be great to have a way to notify Renderer that a mesh is no longer needed, so it can remove all RenderObject instances associated with that mesh.

@querielo querielo changed the title TSL: prevent memory leaks caused by LightsNode TSL: ShadowNode: prevent memory. Nov 28, 2025
@querielo querielo changed the title TSL: ShadowNode: prevent memory. TSL: ShadowNode: prevent memory leaks Nov 28, 2025
@querielo querielo changed the title TSL: ShadowNode: prevent memory leaks TSL: ShadowNode: prevent memory leaks. Add webgpu_test_memory Nov 28, 2025
@cmhhelgeson
Copy link
Contributor

@querielo
Copy link
Contributor Author

@cmhhelgeson The PR does not include the engine build.

So, we can treat your link as the dev branch, and this link as the PR version (it is a branch with the built engine):
https://raw.githack.com/querielo/three.js/kirill/lighting-memory-leak-build-engine/examples/webgpu_test_memory.html

@cmhhelgeson
Copy link
Contributor

@cmhhelgeson The PR does not include the engine build.

So, we can treat your link as the dev branch, and this link as the PR version (it is a branch with the built engine): https://raw.githack.com/querielo/three.js/kirill/lighting-memory-leak-build-engine/examples/webgpu_test_memory.html

Apologies, thank you.

@sunag sunag added this to the r183 milestone Nov 28, 2025
@querielo querielo requested a review from Mugen87 November 30, 2025 01:29
Mugen87
Mugen87 previously approved these changes Dec 1, 2025
@Mugen87 Mugen87 dismissed their stale review December 2, 2025 10:48

Changes to LightShadow required.

@Mugen87 Mugen87 modified the milestones: r183, r182 Dec 2, 2025
@Mugen87 Mugen87 merged commit 3b687a2 into mrdoob:dev Dec 2, 2025
10 checks passed
@sunag
Copy link
Collaborator

sunag commented Dec 2, 2025

I think TSL will need its own logic for dispose, similar to what we implemented for events #31514.

I'm thinking of implementing something like OnDispose that will be triggered when all related materials are disposed of.

const myFn = Fn( () => {

	OnDispose( () => {

		// dispose of resources here

	} );

	// ...

} )();

This should simplify the process for the user when creating the disposal logic, since we tried to make the system as modular as possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants