THREE.DrawCallInspector - visualize draw call cost (experimental)

This is a quick experimental attempt of a helper to monitor draw call costs. It could help spotting expensive draw calls caused by costly shaders and geometries. For WebGL1 using a blocking method and for WebGL2 the timer API.

(A test gltf scene from sketchfab)

The output:

The output is a map that renders all objects tinted as a heatmap by how much time they took relative to each other. This means a average scene with equally expensive meshes in view is likely going to be mostly blueish, while if there is a more expensive objectt with an expensive shader it will be more red while the cheapest go towards solid blue.

Usage:

Create the inspector, call mount in order to attach the UI and add the hooks into THREE.

const dci = new THREE.DrawCallInspector( renderer, scene, camera );
dci.mount();

In your render loop right at the beginning call dci.update() for the overlay output. And at your scene draw call, call dci.begin() before and dci.end() after your scene is rendered to the screen or a render target.

dci.update();

dci.begin();
renderer.render( scene, camera );
dci.end();

For WebGL1: Click on the overlay for a capture or enter a number of frames in the input, so it will automatically take a snapshot after that number of frames.

Parameters:

  • renderer
  • scene
  • camera
  • options

Options

  • enabled (bool)
    A flag to disable the tool, it’s wont create UI or do anything, to leave it in the code but enable it when required.

  • record (constant)
    Either THREE.DrawCallInspector.RecordDraw (default) or THREE.DrawCallInspector.RecordRange, RecordDraw will measure right before and right after the actual WebGL drawcall, while RecordRender will measure before and after renderBufferDirect.

  • wait (bool)
    For WebGL2, will wait till all timing queries finished before making the next so the timings of the same draw call are available.

  • enableMaterials (bool)
    You have the option to let the plugin extend your scenes original materials which are going to be extended, this is only useful for custom and special shaders where the transformation otherwise would make it not visible without.

  • overlayStrength (float , 0-1)
    When enableMaterials is enabled, this is how much the heat coloring is mixed with it’s original rendered color, default is 0.5.

  • skipFrames (number)
    For WebGL1 this should be set to manual (-1) to render by clicking on it, or a higher number as the blocking methods will cause a lagg when measuring, for WebGL2 the measurments can be done in realtime, however timings are fluctuating so it makes sense to use some delay before the next time queries are issued.

  • scale (float , 0-1)
    The size of the overlay relative to the screen.

  • fade (float, 0+-1 )
    The results are lerped across captures to get closer to some average viewable result, with a value of 1 the measured delta time is instantly used for the next capture preview.

  • bias (float, 0+-1 )
    When the results are lerped and they were shorter they will decrease slower by this factor, if an object really doesn’t take long it should be able to cool down then.

Compatibility:

This should work with all most recent 113+ revisions.

Demo

You see a sphere with a expensive shader, a high-poly sphere in the middle and boxes, when going close you see the boxes become hotter even if the spheres are in view, as the boxes cover more pixels on the screen.

19 Likes

This is great! Love seeing debug tools like this. I see you’re calling gl.finish as well as gl.readPixels to ensure the draw commands have finished. Is there a reason you need both? Shouldn’t gl.finish guarantee that the drawing has finished? And would it make sense to call gl.finish before calling the original renderBufferDirect to ensure there are no lingering commands that may inadvertently be measured?

Awesome work!

1 Like

Unfortunately no (but apparently gl.readPixels), even though it literally says it waits and blocks till all commands are finished, this isn’t the case in the WebGL implementation and is afaik actually the same as gl.flush, however this likely would be browser/driver dependent anyway. I think gl.readPixels does actually wait then for the commands to finish, at least it seems to give less fluctuating timings when gl.finish is called before, and gl.readPixels seems to be the only real blocking method.

It is actually, this is where the start time is measured, in the original method isMesh is tested which is modified with a getter on all relevant classes, which when recording is enabled will try to finish/block first then take the first timestamp, this property is tested right before the real draw call - it is also tested one time before (before uniforms/vertex buffers handled) which i first skipped, but it tuned out to only give a reliable result when this is done here too, as otherwise the uniforms or geometry setup gives a longer delay to the first object being drawn with it.

I’ll see if i can improve it further. Since this is about relative weighting of cost it might help repeating draw calls a couple times to get a more significant result to compare with. Or optionally using reference weights such as a average low cost poly-count and material and a highly poly-count with costly material so get some general heat-map. But the weighting has the advantage that it highlights cost-proportions, more independent of how powerful the GPU used is.

1 Like

I’ve made a update now adding the timer API of WebGL2 for more precise and non-blocking results. Also the measurements are made straight at before/after the WebGL API draw call now.

I’m going to extend this further soon with different analysis options, multiple inspector views etc. Especially to clean handle removed meshes.

Demo in the first post.

(Demo scene from sketchfab not in the codepen. That one chair has color write disabled or such)

3 Likes

awesome, is there an es6 module version package?