Skip to content

Nodes: Introduce renderer.nodeBuilderStateProvider for ahead-of-time compiled shaders - #34070

Open
RenaudRohlinger wants to merge 1 commit into
mrdoob:devfrom
RenaudRohlinger:nodes-builder-state-provider
Open

Nodes: Introduce renderer.nodeBuilderStateProvider for ahead-of-time compiled shaders#34070
RenaudRohlinger wants to merge 1 commit into
mrdoob:devfrom
RenaudRohlinger:nodes-builder-state-provider

Conversation

@RenaudRohlinger

@RenaudRohlinger RenaudRohlinger commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Related issue: #31674

Follow-up to: #32984

Description

Building a TSL material or compute node runs nodeBuilder.build() on the render thread. #32984 made compileAsync() responsive by yielding during that work, but the complete TSL-to-native-shader build still runs, and first-render and compute paths remain synchronous.

Applications that own a fixed set of TSL materials or compute kernels can solve a narrower case ahead of time: capture the generated WGSL or GLSL and the small amount of binding metadata during development, store that artifact as JSON, then hydrate a fresh renderer-local builder state on a later page load. This skips work that has already been done. Today, that is impossible without patching three.webgpu.js, because there is no interception point before nodeBuilder.build().

We use this approach in production. On a fluid-simulation scene with 25 materials and kernels, first-load node-build time drops from 97 ms to 22 ms; the remaining three internal builds are intentionally not captured.

This draft adds one opt-in seam: renderer.nodeBuilderStateProvider, defaulting to null. On a cache miss, NodeManager asks getForRender( renderObject, NodeBuilderState ) or getForCompute( computeNode, NodeBuilderState ) for a prebuilt state. Passing the private constructor lets a provider hydrate persisted artifacts into a fresh state without exporting renderer internals. Returning null or undefined preserves the existing live-build path, cache behavior, and bookkeeping.

Demo

Run the JSFiddle demo (source)

The demo uses an internal shader-capture tool to capture (which will be presented at the Three.js Conference in Paris) one procedural TSL material into a 17 KB JSON artifact containing its WGSL and binding recipes. The toggle creates a fresh renderer in either live or precompiled mode, and remembers the selection for later runs or refreshes. Live mode performs one TSL graph build; precompiled mode fetches the stored artifact, hydrates one fresh NodeBuilderState, renders the same procedural image, and reports zero live TSL builds.

Open it in a WebGPU-capable browser, then switch from Live TSL to Precompiled JSON.

@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown

📦 Bundle size

Full ESM build, minified and gzipped.

Before After Diff
WebGL 365.85
86.73
365.85
86.73
+0 B
+0 B
WebGPU 678.75
187.73
679.03
187.79
+285 B
+60 B
WebGPU Nodes 676.79
187.42
677.07
187.48
+285 B
+59 B

🌳 Bundle size after tree-shaking

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

Before After Diff
WebGL 509.35
123.82
509.35
123.82
+0 B
+0 B
WebGPU 759.51
204.59
759.79
204.65
+285 B
+65 B
WebGPU Nodes 709.04
192.03
709.32
192.09
+285 B
+66 B

@RenaudRohlinger
RenaudRohlinger force-pushed the nodes-builder-state-provider branch 2 times, most recently from fe759f0 to 9c25f74 Compare July 19, 2026 11:48
@RenaudRohlinger
RenaudRohlinger force-pushed the nodes-builder-state-provider branch from 9c25f74 to 9ce97f0 Compare July 19, 2026 12:57
@RenaudRohlinger
RenaudRohlinger marked this pull request as ready for review July 19, 2026 13:47
@RenaudRohlinger
RenaudRohlinger requested a review from sunag July 19, 2026 13:47
@sunag

sunag commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

What do you think about improving the caching systems first?

I’m not very confident in precompiled shaders yet, they seem to require additional work from developers, and given the dynamic nature of TSL creation, it may be difficult to keep the mappings exact.

I think there is still a lot we can improve in the caching. In many cases, compilation issues are caused by the absence of a cache. For example:

I updated the node called hash() to use a layout with { seed: 'uint', return: 'float' }, and I used your webgpu_compile_async benchmark with 50 unique procedural materials, I got an average of ~49.0 ms, compared with ~62.0 ms without using the layout Build on render.

~49.0ms hash() with layout - ~62.0 ms hash() no layout

image

In the build logic, the more materials we create while reusing cached data, the faster the construction of new materials becomes. Soon I would like to move forward with is the automatic generation of function layouts, that would certainly be an improvement in this regard.

export const hash = /*@__PURE__*/ Fn( ( [ seed ] ) => {

	// Taken from https://www.shadertoy.com/view/XlGcRh, originally from pcg-random.org

	const state = seed.mul( 747796405 ).add( 2891336453 );
	const word = state.shiftRight( state.shiftRight( 28 ).add( 4 ) ).bitXor( state ).mul( 277803737 );
	const result = word.shiftRight( 22 ).bitXor( word );

	return result.toFloat().mul( 1 / 2 ** 32 ); // Convert to range [0, 1)

}, { seed: 'uint', return: 'float' } );

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.

2 participants