Skip to content

RenderContext: Refactor context key.#32546

Merged
sunag merged 1 commit intomrdoob:devfrom
Mugen87:dev3
Dec 14, 2025
Merged

RenderContext: Refactor context key.#32546
sunag merged 1 commit intomrdoob:devfrom
Mugen87:dev3

Conversation

@Mugen87
Copy link
Collaborator

@Mugen87 Mugen87 commented Dec 13, 2025

Related issue: #32544

Description

Next attempt to reduce the overall number of render context. This time the scene and camera are removed from the key. A new parameter callDepth is added that track the number of nested renderings.

@github-actions
Copy link

github-actions bot commented Dec 13, 2025

📦 Bundle size

Full ESM build, minified and gzipped.

Before After Diff
WebGL 355.12
84.44
355.12
84.44
+0 B
+0 B
WebGPU 616.54
171.12
616.57
171.1
+26 B
-17 B
WebGPU Nodes 615.14
170.85
615.17
170.84
+26 B
-15 B

🌳 Bundle size after tree-shaking

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

Before After Diff
WebGL 487.3
119.32
487.3
119.32
+0 B
+0 B
WebGPU 687.29
186.66
687.34
186.66
+45 B
-2 B
WebGPU Nodes 637.13
173.88
637.18
173.86
+45 B
-15 B

@Mugen87 Mugen87 marked this pull request as ready for review December 13, 2025 11:58
@mrdoob
Copy link
Owner

mrdoob commented Dec 13, 2025

Claude feedback (in case there's anything of interest) 👀

1. Memory Management Trade-off

Important consideration: The current code uses ChainMap which is built on WeakMap. This means render contexts tied to scene/camera objects are automatically garbage collected when those objects are disposed.

The PR switches to a plain object with string keys. This means contexts will accumulate until dispose() is explicitly called. For most apps this is fine (limited attachment state combinations), but worth noting.

2. Simplifications I'd Suggest

Remove getForClear entirely:

// It becomes just:
getForClear( renderTarget = null ) {
    return this.get( renderTarget );
}

Could inline this at call sites or keep it for semantic clarity, but it's essentially a pass-through now.

Consider making callDepth internal to RenderContexts:

Instead of passing callDepth from Renderer, RenderContexts could track it internally:

class RenderContexts {
    constructor() {
        this._renderContexts = {};
        this._callDepth = 0;
    }

    begin() { this._callDepth++; }
    end() { this._callDepth--; }
    
    get( renderTarget = null, mrt = null ) {
        // uses this._callDepth internally
    }
}

This encapsulates the depth tracking better. The Renderer would just call begin()/end() around render operations.

3. Minor Simplifications

Consistent separator in key:

// PR uses mixed separators (: and -)
`${count}:${format}:${type}:${samples}:${depthBuffer}:${stencilBuffer}-${mrtState}-${callDepth}`

// Could be cleaner with consistent format:
`${attachmentState}|${mrtState}|${callDepth}`

Cache attachment state on RenderTarget: The attachment state string is recomputed on every get() call. If render targets had a cached attachmentState getter, this would be slightly more efficient.

4. Question About the Design

Do we actually need different RenderContext objects for different callDepth values with the same attachment state?

If the context is just being reconfigured before each use (which it appears to be), the same context could potentially be reused. The call depth might only matter for ensuring we don't have concurrent access to the same context during nested renders. Worth verifying.

Overall

The PR is a nice simplification. My main suggestion would be encapsulating the callDepth tracking within RenderContexts rather than exposing it as a parameter - it's an implementation detail that Renderer shouldn't need to manage explicitly.

@Mugen87
Copy link
Collaborator Author

Mugen87 commented Dec 13, 2025

@sunag I think this is the more correct approach for handling render contexts. There is now no scene and camera involved, only the render target and MRT settings define the context key. The new _callDepth is required though since certain native objects like render passes and encoders are managed in the render() scope right now. Hence, we can't share the render context across nested renders.

This change will still lead to less overall render contexts and thus render objects in multi-scene/cam scenarios. In webgpu_lines_fat for example, the number of render contexts is reduced from 4 to 2. A reduction like this cuts the number of render objects by half.

@Mugen87 Mugen87 force-pushed the dev3 branch 2 times, most recently from 8bee9b0 to d26b5ec Compare December 13, 2025 13:56
@Mugen87 Mugen87 added this to the r183 milestone Dec 14, 2025
@sunag
Copy link
Collaborator

sunag commented Dec 14, 2025

The new _callDepth is required though since certain native objects like render passes and encoders are managed in the render() scope right now. Hence, we can't share the render context across nested renders.

That's what I imagined when I saw it. It looks interesting.

I'm still a little concerned about relying on sharing the RenderContext based on the sequence of render calls in the primary key, let's give it a try.

@sunag sunag merged commit 20d7ef8 into mrdoob:dev Dec 14, 2025
10 checks passed
@sunag
Copy link
Collaborator

sunag commented Dec 14, 2025

I also have doubts about compileAsync() is still a good strategy; it seems to work only in simple cases. Most projects will use the new post-processing system, and compileAsync() not handle the entire pipeline flow.

I think we could implement something closer to what's used in games today, like asynchronous object compilation during the rendering process. It's common to see this happen in games; in fact, it's almost like what happens on an HTML page where elements load asynchronously and individually and it doesn't wait for everything to load before it starts displaying and some additional event functions should replace the current compileAsync..

@Mugen87
Copy link
Collaborator Author

Mugen87 commented Dec 14, 2025

I'm still a little concerned about relying on sharing the RenderContext based on the sequence of render calls in the primary key

Indeed. In the future, we should try to figure out how we can use e.g. native render passes and encoders in nested render calls as well. This will require some refactoring in Renderer as well as in both backends. An explicit callDepth property should still be the cleaner approach than relying on scene/camera which somewhat masked this limitation.

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.

3 participants