Skip to content

TSL: Introduce uniformFlow() #31531

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Aug 4, 2025
Merged

Conversation

cmhhelgeson
Copy link
Contributor

Description

Add the ability to access the native select functionality within WGSL for programs specifically targeting the WebGPUBackend. Arguments are in the same order as the existing TSL select, and are rearranged in the generate build stage.

Copy link

github-actions bot commented Jul 29, 2025

📦 Bundle size

Full ESM build, minified and gzipped.

Before After Diff
WebGL 338.62
79.03
338.62
79.03
+0 B
+0 B
WebGPU 566.96
156.76
567.29
156.89
+337 B
+127 B
WebGPU Nodes 565.56
156.51
565.9
156.64
+337 B
+128 B

🌳 Bundle size after tree-shaking

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

Before After Diff
WebGL 470.05
113.73
470.05
113.73
+0 B
+0 B
WebGPU 638.44
172.77
638.76
172.87
+316 B
+103 B
WebGPU Nodes 593.09
162.03
593.4
162.16
+316 B
+124 B

@cmhhelgeson
Copy link
Contributor Author

cmhhelgeson commented Jul 30, 2025

Ideally we could just overload select, switching between ConditionalNode select and nativeSelect depending on the renderer backend. However, I'm not sure if it's a great approach to push TempNode code into ConditionalNode just to make that happen. Is there any mechanism within the API that would allow us to do something like this?

export const select = backend === 'WebGPU' ? nativeSelect : conditionalSelect

Alternatively, you can just switch between select based on the backend.

import {nativeSelect, select} from 'three/tsl

const selectFunc = WebGPU.isAvailable() ? nativeSelect : select

@sunag
Copy link
Collaborator

sunag commented Jul 30, 2025

Wouldn't they be different things?

WGSL's select() performs the two flows to return one, unless something hidden is going on there, while real ternary only performs one. I believe select() was created to meet the uniformity limitations...

@cmhhelgeson
Copy link
Contributor Author

cmhhelgeson commented Jul 30, 2025

Wouldn't they be different things?

Yes, on a technical level the TSL select and the WGSL select() (which is represented in Node form by nativeSelect), are two separate implementations/approachs to conditional logic within the shader. Accordingly, TSL select() and WGSL select()/TSL nativeSelect() will generate two different code strings.

const computeFn = Fn(() => {

     const {arbitraryUniform} = effectController;
     currentStorage.element(instanceIndex).assign(select(arbitraryUniform.equal(0), 20, 40))
}
// TSL select()

if ( ( object.nodeUniform1 == 0.0 ) ) {
	nodeVar0 = 20.0;
} else {
	nodeVar0 = 40.0;
}

Current_Left.value[ instanceIndex ] = u32( nodeVar0 );
// WGSL select()/TSL nativeSelect()

Current_Left.value[ instanceIndex ] = u32( select( 40.0, 20.0, ( object.nodeUniform1 == 0.0 ) ) );

However, the functional purpose of both of these code blocks is the same: When assigning a variable, select between two options based on the provided condition.

For me, the benefits of having the later approach be the native WebGPU approach are obvious. We (hypothetically, depending on how WGSL's select works under the hood on different APIs) avoid unnecessary branch divergence, align more closely with how the user intuitively expects the select functionality to work, and, as you mentioned,
give the user the ability to conditionally assign variables based on operations that can only be called from a uniform control flow.

For instance, in WGSL, this code is invalid when using the existing TSL select() Node.

const computeArbitraryFn = Fn( () => {
	const subgroupMetaRank = ( invocationLocalIndex.div( subgroupSize ) );
	const currentElement = inputStorage.element( invocationLocalIndex );
	// Add together values that are only in first subgroup of a workgroup
	currentElement.assign( select( subgroupMetaRank.equal( 0 ), subgroupAdd( currentElement ), 0 ) );
} )().compute( size );
// TSL select()
if ( ( f32( ( invocationLocalIndex / subgroupSize ) ) == 0.0 ) ) {
	nodeVar0 = subgroupAdd( Current_Right.value[ invocationLocalIndex ] );
} else {
	nodeVar0 = 0u;
}

Current_Right.value[ invocationLocalIndex ] = nodeVar0;

// Error while parsing WGSL: :50:14 error: 'subgroupAdd' must only be called from uniform control flow
// nodeVar0 = subgroupAdd( Current_Right.value[ invocationLocalIndex ] );

When using nativeSelect() it works perfectly fine.

// WGSL select/TSL nativeSelect()

Current_Left.value[ invocationLocalIndex ] = u32( select( 0.0, f32( subgroupAdd( Current_Left.value[ invocationLocalIndex ] ) ), ( f32( ( invocationLocalIndex / subgroupSize ) ) == 0.0 ) ) );

In fact, the whole impetus behind implementing nativeSelect is largely to facilitate the use of code like that seen above in the Compute Reduction (#31378) example. The only bottleneck to switching over every WebGPU example that uses select to nativeSelect is compatibility with the WebGL2 backend.

As I suggested in the comment above, and since the core functionality of both select() and nativeSelect() is essentially the same, even if the implementation is different, this could be solved by a mechanism that providing a different select depending on the backend the program is using. That way, WebGPU users could get the most performant and flexible version of select while the WebGL backend continues to use the existing ConditionalNode logic.

Alternatively, we can avoid this workaround all together and just expose the nativeSelect functionality to users who wish to exclusively target WebGPU and want the native WGSL select function's associated benefits.

NOTE: May test other examples that use select with nativeSelect later to see if any marginal improvements exist.

@sunag
Copy link
Collaborator

sunag commented Jul 30, 2025

The only bottleneck to switching over every WebGPU example that uses select to nativeSelect is compatibility with the WebGL2 backend.

I think this could be easily solved with a TSL function.

That way, WebGPU users could get the most performant and flexible version of select while the WebGL backend continues to use the existing ConditionalNode logic.

I can't imagine how it could be more performant to execute two flows to deliver one, especially if we are executing complex operations within the conditionals.

For me, the benefits of having the later approach be the native WebGPU approach are obvious. We (hypothetically, depending on how WGSL's select works under the hood on different APIs) avoid unnecessary branch divergence, align more closely with how the user intuitively expects the select functionality to work, and, as you mentioned,
give the user the ability to conditionally assign variables based on operations that can only be called from a uniform control flow.

The intention is not to make TSL similar to WGSL in this sense, but rather to simplify it. Uniform control flow is currently ignored, and in another case the implementation should replace this at runtime and detect this limitation through the compiler(NodeBuilder) and not leave this responsibility to the user. It's not the kind of redundancy I'd want to bring into TSL, that should be higher abstraction language than WGSL.

@cmhhelgeson
Copy link
Contributor Author

cmhhelgeson commented Jul 30, 2025

That's understandable. For now, would it at least be possible to expose both options so there's an option for those who want to use the more abstracted version of select and another option for those who want to target specific behavior that's only possible with the WGSL select? The comment/function signature for native select can be adjusted to heavily emphasize that the nativeSelect should only be used in specific WebGPU use cases where select does not apply and where programs are only targeting WebGPU.

I'm open to any solution that allows this functionality to enter the repository for certain use cases without harming the larger goals of the library.

@sunag
Copy link
Collaborator

sunag commented Jul 30, 2025

Could you contextualize an uniform control flow?

This could be using node.uniformFlow() or uniformFlow( node ).

// ContextNode.js

export const uniformFlow = ( node ) => context( node, { uniformFlow: true } );

addMethodChaining( 'uniformFlow', uniformFlow );

You can use it in select() or in a Fn() call for example.
For a Fn(), it should resolve all select of the function.

myFn().uniformFlow();

This would be more in line with an automatic detection we can do in the future.

The ConditionNode should check if it is within the context.uniformFlow === true and resolve this using WGSL select. You can do this in GLSL also using a final variable in the ternary flows, what matters in this case is that both flows are executed.

@cmhhelgeson
Copy link
Contributor Author

Could you contextualize an uniform control flow?

Sure, I'll try to implement it this way. I'll also try to find areas in the existing examples where setting uniformFlow to true makes sense.

@cmhhelgeson cmhhelgeson marked this pull request as draft July 31, 2025 00:03
@cmhhelgeson
Copy link
Contributor Author

@sunag Would it make sense to rename nodeProperty in ConditionalNode to propertyName ala TempNode? It seems like they both hold property names for their respective nodes.

@cmhhelgeson cmhhelgeson marked this pull request as ready for review August 3, 2025 00:10
@sunag sunag changed the title TSL - Native Select Node TSL: Introduce uniformFlow() Aug 4, 2025
@sunag sunag added this to the r180 milestone Aug 4, 2025
@branthoughton336-a11y
Copy link

branthoughton336-a11y commented Aug 4, 2025 via email

@sunag
Copy link
Collaborator

sunag commented Aug 4, 2025

Merging, thanks!

@sunag sunag merged commit 60d8954 into mrdoob:dev Aug 4, 2025
9 checks passed
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