Skip to content

Commit 644c183

Browse files
authored
Merge pull request #12983 from alarkbentley/alark/async-picking
Added Scene.pickAsync for non GPU blocking picking
2 parents a6e0226 + 71a12a3 commit 644c183

File tree

14 files changed

+1045
-69
lines changed

14 files changed

+1045
-69
lines changed

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
- Added experimental support for loading 3D Tiles as terrain, via `Cesium3DTilesTerrainProvider`. See [the PR](https://github.com/CesiumGS/cesium/pull/12963) for limitations on the types of 3D Tiles that can be used. [#12296](https://github.com/CesiumGS/cesium/issues/12296)
2323
- Added support for [EXT_mesh_primitive_edge_visibility](https://github.com/KhronosGroup/glTF/pull/2479) glTF extension. [#12765](https://github.com/CesiumGS/cesium/issues/12765)
24+
- Added `scene.pickAsync` for non GPU blocking picking using WebGL2 [#12983](https://github.com/CesiumGS/cesium/pull/12983)
2425

2526
#### Fixes :wrench:
2627

@@ -34,6 +35,7 @@
3435
- Fixed picking of `GroundPrimitive` with multiple `PolygonGeometry` instances selecting the wrong instance. [#12978](https://github.com/CesiumGS/cesium/pull/12978)
3536
- Fixed a bug where the removal of draped imagery layers did not update the rendered state [#12923](https://github.com/CesiumGS/cesium/issues/12923)
3637
- Fixed precision issues with Gaussian splat tilesets where the root tile does not have a world transform. [#12925](https://github.com/CesiumGS/cesium/issues/12925)
38+
- Fixed infinite recursion that would happen if user append post-render callbacks within existing callbacks [#12983](https://github.com/CesiumGS/cesium/pull/12983)
3739

3840
## 1.134.1 - 2025-10-10
3941

CONTRIBUTORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to Cesiu
104104
- [Erin Ingram](https://github.com/eringram)
105105
- [Daniel Zhong](https://github.com/danielzhong)
106106
- [Mark Schlosser](https://github.com/markschlosseratbentley)
107+
- [Adam Larkeryd](https://github.com/alarkbentley)
107108
- [Flightradar24 AB](https://www.flightradar24.com)
108109
- [Aleksei Kalmykov](https://github.com/kalmykov)
109110
- [BIT Systems](http://www.caci.com/bit-systems)

packages/engine/Source/Renderer/Buffer.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,26 @@ function Buffer(options) {
7272
this.vertexArrayDestroyable = true;
7373
}
7474

75+
Buffer.createPixelBuffer = function (options) {
76+
//>>includeStart('debug', pragmas.debug);
77+
Check.defined("options.context", options.context);
78+
//>>includeEnd('debug');
79+
80+
if (!options.context._webgl2) {
81+
throw new DeveloperError(
82+
"A WebGL 2 context is required to create PixelBuffers.",
83+
);
84+
}
85+
86+
return new Buffer({
87+
context: options.context,
88+
bufferTarget: WebGLConstants.PIXEL_PACK_BUFFER,
89+
typedArray: options.typedArray,
90+
sizeInBytes: options.sizeInBytes,
91+
usage: options.usage,
92+
});
93+
};
94+
7595
/**
7696
* Creates a vertex buffer, which contains untyped vertex data in GPU-controlled memory.
7797
* <br /><br />
@@ -242,6 +262,18 @@ Buffer.prototype._getBuffer = function () {
242262
return this._buffer;
243263
};
244264

265+
Buffer.prototype._bind = function () {
266+
const gl = this._gl;
267+
const target = this._bufferTarget;
268+
gl.bindBuffer(target, this._buffer);
269+
};
270+
271+
Buffer.prototype._unBind = function () {
272+
const gl = this._gl;
273+
const target = this._bufferTarget;
274+
gl.bindBuffer(target, null);
275+
};
276+
245277
Buffer.prototype.copyFromArrayView = function (arrayView, offsetInBytes) {
246278
offsetInBytes = offsetInBytes ?? 0;
247279

packages/engine/Source/Renderer/BufferUsage.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ const BufferUsage = {
77
STREAM_DRAW: WebGLConstants.STREAM_DRAW,
88
STATIC_DRAW: WebGLConstants.STATIC_DRAW,
99
DYNAMIC_DRAW: WebGLConstants.DYNAMIC_DRAW,
10+
DYNAMIC_READ: WebGLConstants.DYNAMIC_READ,
1011

1112
validate: function (bufferUsage) {
1213
return (
1314
bufferUsage === BufferUsage.STREAM_DRAW ||
1415
bufferUsage === BufferUsage.STATIC_DRAW ||
15-
bufferUsage === BufferUsage.DYNAMIC_DRAW
16+
bufferUsage === BufferUsage.DYNAMIC_DRAW ||
17+
bufferUsage === BufferUsage.DYNAMIC_READ
1618
);
1719
},
1820
};

packages/engine/Source/Renderer/Context.js

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Buffer from "./Buffer.js";
12
import Check from "../Core/Check.js";
23
import Color from "../Core/Color.js";
34
import ComponentDatatype from "../Core/ComponentDatatype.js";
@@ -1442,13 +1443,88 @@ Context.prototype.endFrame = function () {
14421443
};
14431444

14441445
/**
1446+
* @typedef {object} ReadState
1447+
*
1448+
* Options defining a rectangle to read pixels from.
1449+
*
1450+
* @private
1451+
* @property {number} [x=0] The x offset of the rectangle to read from.
1452+
* @property {number} [y=0] The y offset of the rectangle to read from.
1453+
* @property {number} [width=this.drawingBufferWidth] The width of the rectangle to read from.
1454+
* @property {number} [height=this.drawingBufferHeight] The height of the rectangle to read from.
1455+
* @property {FrameBuffer|undefined} [framebuffer] The framebuffer to read from. If undefined, the read will be from the default framebuffer.
1456+
*/
1457+
1458+
/**
1459+
* Read pixels from a framebuffer into a Pixel Buffer Object (PBO).
1460+
*
1461+
* @private
1462+
* @param {ReadState} readState Options defining a rectangle to read pixels from.
1463+
* @returns {Buffer} A PixelBuffer containing the pixels read from the specified rectangle.
1464+
*
1465+
* @exception {DeveloperError} A WebGL 2 context is required to read pixels using a PBO.
1466+
*/
1467+
Context.prototype.readPixelsToPBO = function (readState) {
1468+
const gl = this._gl;
1469+
1470+
readState = readState ?? Frozen.EMPTY_OBJECT;
1471+
const x = Math.max(readState.x ?? 0, 0);
1472+
const y = Math.max(readState.y ?? 0, 0);
1473+
const width = readState.width ?? this.drawingBufferWidth;
1474+
const height = readState.height ?? this.drawingBufferHeight;
1475+
const framebuffer = readState.framebuffer;
1476+
1477+
if (!this._webgl2) {
1478+
throw new DeveloperError(
1479+
"A WebGL 2 context is required to read pixels using a PBO.",
1480+
);
1481+
}
1482+
1483+
//>>includeStart('debug', pragmas.debug);
1484+
Check.typeOf.number.greaterThan("readState.width", width, 0);
1485+
Check.typeOf.number.greaterThan("readState.height", height, 0);
1486+
//>>includeEnd('debug');
1487+
1488+
let pixelDatatype = PixelDatatype.UNSIGNED_BYTE;
1489+
let pixelFormat = PixelFormat.RGBA;
1490+
if (defined(framebuffer) && framebuffer.numberOfColorAttachments > 0) {
1491+
pixelDatatype = framebuffer.getColorTexture(0).pixelDatatype;
1492+
pixelFormat = framebuffer.getColorTexture(0).pixelFormat;
1493+
}
1494+
1495+
const pixels = Buffer.createPixelBuffer({
1496+
context: this,
1497+
sizeInBytes: PixelFormat.textureSizeInBytes(
1498+
pixelFormat,
1499+
pixelDatatype,
1500+
width,
1501+
height,
1502+
),
1503+
usage: BufferUsage.DYNAMIC_READ,
1504+
});
1505+
1506+
bindFramebuffer(this, framebuffer);
1507+
1508+
pixels._bind();
1509+
gl.readPixels(
1510+
x,
1511+
y,
1512+
width,
1513+
height,
1514+
pixelFormat,
1515+
PixelDatatype.toWebGLConstant(pixelDatatype, this),
1516+
0,
1517+
);
1518+
pixels._unBind();
1519+
1520+
return pixels;
1521+
};
1522+
1523+
/**
1524+
* Read pixels from a framebuffer into a typed array.
1525+
*
14451526
* @private
1446-
* @param {object} readState An object with the following properties:
1447-
* @param {number} [readState.x=0] The x offset of the rectangle to read from.
1448-
* @param {number} [readState.y=0] The y offset of the rectangle to read from.
1449-
* @param {number} [readState.width=this.drawingBufferWidth] The width of the rectangle to read from.
1450-
* @param {number} [readState.height=this.drawingBufferHeight] The height of the rectangle to read from.
1451-
* @param {Framebuffer} [readState.framebuffer] The framebuffer to read from. If undefined, the read will be from the default framebuffer.
1527+
* @param {ReadState} readState Options defining a rectangle to read pixels from.
14521528
* @returns {Uint8Array|Uint16Array|Float32Array|Uint32Array} The pixels in the specified rectangle.
14531529
*/
14541530
Context.prototype.readPixels = function (readState) {
@@ -1467,12 +1543,14 @@ Context.prototype.readPixels = function (readState) {
14671543
//>>includeEnd('debug');
14681544

14691545
let pixelDatatype = PixelDatatype.UNSIGNED_BYTE;
1546+
let pixelFormat = PixelFormat.RGBA;
14701547
if (defined(framebuffer) && framebuffer.numberOfColorAttachments > 0) {
14711548
pixelDatatype = framebuffer.getColorTexture(0).pixelDatatype;
1549+
pixelFormat = framebuffer.getColorTexture(0).pixelFormat;
14721550
}
14731551

14741552
const pixels = PixelFormat.createTypedArray(
1475-
PixelFormat.RGBA,
1553+
pixelFormat,
14761554
pixelDatatype,
14771555
width,
14781556
height,
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import Check from "../Core/Check.js";
2+
import destroyObject from "../Core/destroyObject.js";
3+
import DeveloperError from "../Core/DeveloperError.js";
4+
import Frozen from "../Core/Frozen.js";
5+
import RuntimeError from "../Core/RuntimeError.js";
6+
import WebGLConstants from "../Core/WebGLConstants.js";
7+
8+
/**
9+
* The WebGLSync interface is part of the WebGL 2 API and is used to synchronize activities between the GPU and the application.
10+
*
11+
* @param {object} options Object with the following properties:
12+
* @param {Context} context
13+
*
14+
* @exception {DeveloperError} A WebGL 2 context is required to use Sync operations.
15+
*
16+
* @private
17+
* @constructor
18+
*/
19+
function Sync(options) {
20+
options = options ?? Frozen.EMPTY_OBJECT;
21+
const context = options.context;
22+
23+
//>>includeStart('debug', pragmas.debug);
24+
Check.defined("options.context", context);
25+
//>>includeEnd('debug');
26+
27+
if (!context._webgl2) {
28+
throw new DeveloperError(
29+
"A WebGL 2 context is required to use Sync operations.",
30+
);
31+
}
32+
33+
const gl = context._gl;
34+
const sync = gl.fenceSync(WebGLConstants.SYNC_GPU_COMMANDS_COMPLETE, 0);
35+
36+
this._gl = gl;
37+
this._sync = sync;
38+
}
39+
Sync.create = function (options) {
40+
return new Sync(options);
41+
};
42+
/**
43+
* Query the sync status of this Sync object.
44+
*
45+
* @returns {number} Returns a WebGLConstants indicating the status of the sync object (WebGLConstants.SIGNALED or WebGLConstants.UNSIGNALED).
46+
*
47+
* @private
48+
*/
49+
Sync.prototype.getStatus = function () {
50+
const status = this._gl.getSyncParameter(
51+
this._sync,
52+
WebGLConstants.SYNC_STATUS,
53+
);
54+
return status;
55+
};
56+
Sync.prototype.isDestroyed = function () {
57+
return false;
58+
};
59+
Sync.prototype.destroy = function () {
60+
this._gl.deleteSync(this._sync);
61+
return destroyObject(this);
62+
};
63+
64+
/**
65+
* Incremantally polls the status of the Sync object until signaled then resolves.
66+
* Usually polling should be done once per frame.
67+
*
68+
* @example
69+
* try {
70+
* await sync.waitForSignal(function (next) {
71+
* setTimeout(next, 100);
72+
* });
73+
*} catch (e) {
74+
* throw "Signal timeout";
75+
*} finally {
76+
* sync.destroy();
77+
*}
78+
*
79+
* @param {function} scheduleFunction Function for scheduling the next poll. Receives a callback as its only parameter.
80+
* @param {number} [ttl=10] Max number of iterations to poll until timeout.
81+
*
82+
* @exception {RuntimeError} Wait for signal timeout.
83+
*/
84+
Sync.prototype.waitForSignal = async function (scheduleFunction, ttl) {
85+
const self = this;
86+
ttl = ttl ?? 10;
87+
function waitForSignal0(resolve, reject, ttl) {
88+
return () => {
89+
const syncStatus = self.getStatus();
90+
const signaled = syncStatus === WebGLConstants.SIGNALED;
91+
if (signaled) {
92+
resolve();
93+
} else if (ttl <= 0) {
94+
reject(new RuntimeError("Wait for signal timeout"));
95+
} else {
96+
scheduleFunction(waitForSignal0(resolve, reject, ttl - 1));
97+
}
98+
};
99+
}
100+
return new Promise((resolve, reject) => {
101+
scheduleFunction(waitForSignal0(resolve, reject, ttl));
102+
});
103+
};
104+
export default Sync;

0 commit comments

Comments
 (0)