three.js
three.js copied to clipboard
Defer blocking queries against WebGLPrograms after linking
Program linking can happen asynchronously, but is forced to block and wait for completion the first time information about that program is queried after linking. This includes calls to getAttribLocation
/getUniformLocation
/getUniform
/getProgramParameter
/getProgramLogInfo
, etc.
This change defers most of those calls until the first time until the first time that they're actually needed by the page, which can allow the browser time to complete linking asynchronously and thus reduce blocking, especially when used in conjunction with renderer.compile()
.
It's worth noting that many materials are initialized the first time they are encountered by the render loop and then immediately used for rendering, which negates most of the benefit that this could otherwise provide, but it lays the groundwork for more efficient patterns in the future and can significantly help classes like PMREMGenerator
that are already using renderer.compile()
internally.
The ideal endpoint is for Three to start taking advantage of KHR_parallel_shader_compile as proposed by #16321 and #16662. This is just a lower-impact stepping stone along the way.
Review ping?
Thanks for the feedback! Addressed all comments.
I've tested your branch locally and it seems the webgl_lightningstrike
example is failing. I just get a black screen with no console logs.
I'll look into it, thanks!
Fixed. Explanation above.
Sorry for the delay on this one.
Program linking can happen asynchronously, but is forced to block and wait for completion the first time information about that program is queried after linking.
I think that is working as intended. Otherwise programs may not be ready in the render call, no?
For example, this code renders a cube:
import * as THREE from 'https://cdn.skypack.dev/three'
const camera = new THREE.PerspectiveCamera();
camera.position.z = 1;
const geometry = new THREE.BoxGeometry( 0.2, 0.2, 0.2 );
const material = new THREE.MeshNormalMaterial();
const mesh = new THREE.Mesh( geometry, material );
const renderer = new THREE.WebGLRenderer();
renderer.setSize( 300, 300 );
renderer.render( mesh, camera );
document.body.appendChild( renderer.domElement );
https://jsfiddle.net/L7nkdc56/
If the linking was async, wouldn't it be possible that we would end up with a black render?
This change does not force asynchronous linking. What it does do (or at least will do when I rebase it over a year and a half of Three.js churn) is moves various calls that are forced synchronously to wait on linking to finish (getUniforms()
, etc) to the first time that they are used rather than always happening immediately after link.
Given how Three is architected, this functionally means that most programs still end up blocking because they wait to compile the programs until an object is added to the Scene Graph, so first use happens ~immediately. In some cases, however, such as PMREMGenerator
that links a program on construction but may not use it until much later, it offers an opportunity for most/all of the program linking to happen asynchronously without impacting the scene's rendering.
As stated in the first comment, this was intended only as a lower-impact stepping stone to enabling more robust async shader compiles.
Rebased and confirmed it still works as intended. Code's even a bit simpler before, due to something that's changed about how morph targets are handled that made at least once large chunk of code disappear. 👍
This has great potential in combination with our new support of the fetch API. Imagine: load a GLB, but pull the tiny JSON part first; use that to construct the scene and start the shaders compiling while waiting for the bin and texture data to come down. Once all is ready, start the render loop with all the work already done.
@mrdoob: I'm going to be looking at https://github.com/mrdoob/three.js/pull/19752 again after our discussion on Dec 9th, but this PR is a pre-requisite for that work. It does not change any existing behavior, makes a few isolated scenarios less janky, and breaks up a dependency that prevents further work on making shader compiling async.
Critically, this change does NOT alter the behavior that objects in Three are visible as soon as they are attached to the scene. (Even if that causes jank.)
Verified the PR again and it looks all good!
📦 Bundle size
Full ESM build, minified and gzipped.
Filesize dev |
Filesize PR | Diff |
---|---|---|
651.4 kB (161.4 kB) | 651.5 kB (161.4 kB) | +107 B |
🌳 Bundle size after tree-shaking
Minimal build including a renderer, camera, empty scene, and dependencies.
Filesize dev |
Filesize PR | Diff |
---|---|---|
444.1 kB (107.4 kB) | 444.2 kB (107.4 kB) | +107 B |
Rebased to resolve merge conflicts after discussion kicked up on #19752 again.
Since I think Ricardo feels positive about this change and supporting KHR_parallel_shader_compile
is a valuable addition to the engine, I'll merge so we can move on with https://github.com/mrdoob/three.js/pull/19752.
Thank you!