Skip to content

Conversation

@alarkbentley
Copy link

@alarkbentley alarkbentley commented Oct 16, 2025

Description

Scene.pick is internally using gl.readPixels that performs a synchronous GPU readback which is blocking the main render thread and very costly especially on low end GPUs. With WebGL2 a non GPU blocking method is available that uses PBO and Sync operations to asynchronously readback the pixels data without blocking.

Reference: https://registry.khronos.org/webgl/specs/latest/2.0/
Section: 3.7.10 Reading back pixels

This PR implements support for this and exposes it under a new API method Scene.pickAsync -> Promise

Asynchronous picking gives precedence to the rendering over returning the pick information. This introduces a slight delay compared to using the blocking pick method. Depending on what you prefer there might be value in having both options available in API.

Example

import * as Cesium from "cesium";

const widget = new Cesium.CesiumWidget("cesiumContainer", {
  terrain: Cesium.Terrain.fromWorldTerrain(),
});
const scene = widget.scene;

handler.setInputAction(function(movement) {
    scene.pickAsync(movement.position).then((feature) => {
         feature.color = Cesium.Color.YELLOW;
    });
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

Demo

Browser: Microsoft Edge Version 141.0.3537.71 (Official build) (64-bit)
GPU: ANGLE (Intel, Intel(R) Arc(TM) Pro Graphics (0x00007D55) Direct3D11 vs_5_0 ps_5_0, D3D11)

edge_sync_async.mp4

Profiling results

As seen in the synchronous picking (left), readpixels can stall for longer time than it takes to render the entire frame. In this example 46% of the total execution time is taken by readpixels.

In the asynchronous example (right) the bottleneck is the actual frame rendering which makes more sense.

edge_profile_sync_async

Issue number and link

#630

Testing plan

Author checklist

  • I have submitted a Contributor License Agreement
  • I have added my name to CONTRIBUTORS.md
  • I have updated CHANGES.md with a short summary of my change
  • I have added or updated unit tests to ensure consistent code coverage
  • I have updated the inline documentation, and included code examples where relevant
  • I have performed a self-review of my code

@github-actions
Copy link

github-actions bot commented Oct 16, 2025

Thank you for the pull request, @alarkbentley! Welcome to the Cesium community!

In order for us to review your PR, please complete the following steps:

Review Pull Request Guidelines to make sure your PR gets accepted quickly.

@alarkbentley alarkbentley marked this pull request as ready for review October 21, 2025 01:57
Copy link
Contributor

@mzschwartz5 mzschwartz5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @alarkbentley,

Thanks for the contribution! Crazy this goes all the way back to issue #630.

I left a number of comments, mostly style related, a few more important, but nothing that should drastically change the overall shape of the PR.

Comment on lines +1483 to +1484
let pixels;
if (pbo) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style comment - I'm a stickler for simplifying conditional logic when possible.

This function has basically become two different functions depending on whether pbo is passed in. Let's formalize that:

const readPixelsFunc = pbo ? readPixelsAsync() : readPixelsSync();
readPixelsFunc();

Then we have just a single branching point instead of multiple.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two funcs can take pixelFormat, pixelDatatype etc. as args

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I agree that this would be simpler, could you perhaps show in a commit?
If we go this route I would rather create two separate prototype functions. But they have a lot in common.

Comment on lines +147 to +161
if (debounce && picking && now - debounceTime < DEBOUNCE_TIMEOUT_MS) {
return true;
}
if (debounce) {
picking = true;
debounceTime = now;
}
if (async) {
result[0] = await scene.pickAsync(position);
} else {
result[0] = scene.pick(position);
}
if (debounce) {
picking = false;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps debouncing is something that should be handled internally by the implementation? (As an option, perhaps?)

Or maybe the internal implementation should use a set of results each frame, rather than an array, so that debouncing isn't even necessary (duplicates across frames will hash to the same entry)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love the bloat this introduces to the sandcastle. I'm honestly not even sure if such a change is really necessary to show off in a sandcastle. I'm not sure I was even able to tell the difference between (a)sync in the video of this PR's description.

alarkbentley and others added 5 commits October 22, 2025 09:49
Simplify promise using await

Co-authored-by: Matt Schwartz <mzschwartz5@gmail.com>
spelling

Co-authored-by: Matt Schwartz <mzschwartz5@gmail.com>
@mzschwartz5
Copy link
Contributor

I haven't reviewed your unit tests yet, but given some of our conversations, I'm guessing we probably need more testing around:

  1. TTL on the async pick (some unit test should have picked up on the fact that it was never being decremented)
  2. (Maybe) Rejection of the async pick promise

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.

4 participants