Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- Fix issues with label background when updating properties while `label.show` is `false`. [#12138](https://github.com/CesiumGS/cesium/issues/12138)
- Improved performance of `scene.drillPick`. [#12916](https://github.com/CesiumGS/cesium/pull/12916)
- Improved performance when removing primitives. [#3018](https://github.com/CesiumGS/cesium/pull/3018)
- Improved performance of terrain Quadtree handling of custom data [#12907](https://github.com/CesiumGS/cesium/pull/12907)

## 1.134.1 - 2025-10-10

Expand Down
4 changes: 1 addition & 3 deletions packages/engine/Source/Scene/GlobeSurfaceTile.js
Original file line number Diff line number Diff line change
Expand Up @@ -493,10 +493,8 @@ GlobeSurfaceTile.prototype.updateExaggeration = function (
if (quadtree !== undefined) {
quadtree._tileToUpdateHeights.push(tile);
const customData = tile.customData;
const customDataLength = customData.length;
for (let i = 0; i < customDataLength; i++) {
for (const data of customData) {
// Restart the level so that a height update is triggered
const data = customData[i];
data.level = -1;
}
}
Expand Down
60 changes: 34 additions & 26 deletions packages/engine/Source/Scene/QuadtreePrimitive.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ function QuadtreePrimitive(options) {
this._removeHeightCallbacks = [];

this._tileToUpdateHeights = [];
this._lastTileIndex = 0;
this._updateHeightsTimeSlice = 2.0;

// If a culled tile contains _cameraPositionCartographic or _cameraReferenceFrameOriginCartographic, it will be marked
Expand Down Expand Up @@ -217,10 +216,8 @@ function invalidateAllTiles(primitive) {
for (let i = 0; i < levelZeroTiles.length; ++i) {
const tile = levelZeroTiles[i];
const customData = tile.customData;
const customDataLength = customData.length;

for (let j = 0; j < customDataLength; ++j) {
const data = customData[j];
for (const data of customData) {
data.level = 0;
primitive._addHeightCallbacks.push(data);
}
Expand Down Expand Up @@ -513,7 +510,6 @@ function selectTilesForRendering(primitive, frameState) {
tilesToRender.length = 0;

// We can't render anything before the level zero tiles exist.
let i;
const tileProvider = primitive._tileProvider;
if (!defined(primitive._levelZeroTiles)) {
const tilingScheme = tileProvider.tilingScheme;
Expand All @@ -524,7 +520,7 @@ function selectTilesForRendering(primitive, frameState) {
const numberOfRootTiles = primitive._levelZeroTiles.length;
if (rootTraversalDetails.length < numberOfRootTiles) {
rootTraversalDetails = new Array(numberOfRootTiles);
for (i = 0; i < numberOfRootTiles; ++i) {
for (let i = 0; i < numberOfRootTiles; ++i) {
if (rootTraversalDetails[i] === undefined) {
rootTraversalDetails[i] = new TraversalDetails();
}
Expand All @@ -537,7 +533,6 @@ function selectTilesForRendering(primitive, frameState) {

primitive._occluders.ellipsoid.cameraPosition = frameState.camera.positionWC;

let tile;
const levelZeroTiles = primitive._levelZeroTiles;
const occluders =
levelZeroTiles.length > 1 ? primitive._occluders : undefined;
Expand All @@ -550,18 +545,28 @@ function selectTilesForRendering(primitive, frameState) {

const customDataAdded = primitive._addHeightCallbacks;
const customDataRemoved = primitive._removeHeightCallbacks;
const frameNumber = frameState.frameNumber;

let len;
if (customDataAdded.length > 0 || customDataRemoved.length > 0) {
for (i = 0, len = levelZeroTiles.length; i < len; ++i) {
tile = levelZeroTiles[i];
tile._updateCustomData(frameNumber, customDataAdded, customDataRemoved);
customDataAdded.forEach((data) => {
const tile = levelZeroTiles.find((tile) =>
Rectangle.contains(tile.rectangle, data.positionCartographic),
);
if (tile) {
tile._addedCustomData.push(data);
}
});

customDataAdded.length = 0;
customDataRemoved.length = 0;
}
customDataRemoved.forEach((data) => {
const tile = levelZeroTiles.find((tile) =>
Rectangle.contains(tile.rectangle, data.positionCartographic),
);
if (tile) {
tile._removedCustomData.push(data);
}
});

levelZeroTiles.forEach((tile) => tile.updateCustomData());
customDataAdded.length = 0;
customDataRemoved.length = 0;

const camera = frameState.camera;

Expand All @@ -577,8 +582,8 @@ function selectTilesForRendering(primitive, frameState) {
);

// Traverse in depth-first, near-to-far order.
for (i = 0, len = levelZeroTiles.length; i < len; ++i) {
tile = levelZeroTiles[i];
for (let i = 0; i < levelZeroTiles.length; ++i) {
const tile = levelZeroTiles[i];
primitive._tileReplacementQueue.markTileRendered(tile);
if (!tile.renderable) {
queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState);
Expand All @@ -596,7 +601,7 @@ function selectTilesForRendering(primitive, frameState) {
}
}

primitive._lastSelectionFrameNumber = frameNumber;
primitive._lastSelectionFrameNumber = frameState.frameNumber;
}

function queueTileLoad(primitive, queue, tile, frameState) {
Expand Down Expand Up @@ -716,7 +721,7 @@ function visitTile(
++debug.tilesVisited;

primitive._tileReplacementQueue.markTileRendered(tile);
tile._updateCustomData(frameState.frameNumber);
tile.updateCustomData();

if (tile.level > debug.maxDepthVisited) {
debug.maxDepthVisited = tile.level;
Expand Down Expand Up @@ -1417,15 +1422,18 @@ function updateHeights(primitive, frameState) {
// Ensure stale position cache is cleared
tile.clearPositionCache();
tilesToUpdateHeights.shift();
primitive._lastTileIndex = 0;
continue;
}
const customData = tile.customData;
const customDataLength = customData.length;
if (!defined(tile._customDataIterator)) {
tile._customDataIterator = customData.values();
}
const customDataIterator = tile._customDataIterator;

let timeSliceMax = false;
for (i = primitive._lastTileIndex; i < customDataLength; ++i) {
const data = customData[i];
let nextData;
while (!(nextData = customDataIterator.next()).done) {
const data = nextData.value;

// No need to run this code when the tile is upsampled, because the height will be the same as its parent.
const terrainData = tile.data.terrainData;
Expand Down Expand Up @@ -1543,10 +1551,10 @@ function updateHeights(primitive, frameState) {
}

if (timeSliceMax) {
primitive._lastTileIndex = i;
tile._customDataIterator = customDataIterator;
break;
} else {
primitive._lastTileIndex = 0;
tile._customDataIterator = undefined;
tilesToUpdateHeights.shift();
}
}
Expand Down
108 changes: 64 additions & 44 deletions packages/engine/Source/Scene/QuadtreeTile.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import defined from "../Core/defined.js";
import DeveloperError from "../Core/DeveloperError.js";
import Rectangle from "../Core/Rectangle.js";
import Cartographic from "../Core/Cartographic.js";
import QuadtreeTileLoadState from "./QuadtreeTileLoadState.js";
import TileSelectionResult from "./TileSelectionResult.js";

Expand Down Expand Up @@ -107,8 +108,10 @@ function QuadtreeTile(options) {
this._distance = 0.0;
this._loadPriority = 0.0;

this._customData = [];
this._frameUpdated = undefined;
this._customData = new Set();
this._customDataIterator = undefined;
this._addedCustomData = [];
this._removedCustomData = [];
this._lastSelectionResult = TileSelectionResult.NONE;
this._lastSelectionResultFrame = undefined;
this._loadedCallbacks = {};
Expand Down Expand Up @@ -311,52 +314,69 @@ QuadtreeTile.prototype.clearPositionCache = function () {
}
};

QuadtreeTile.prototype._updateCustomData = function (
frameNumber,
added,
removed,
) {
let customData = this.customData;

let i;
let data;
let rectangle;

if (defined(added) && defined(removed)) {
customData = customData.filter(function (value) {
return removed.indexOf(value) === -1;
});
this._customData = customData;

rectangle = this._rectangle;
for (i = 0; i < added.length; ++i) {
data = added[i];
if (Rectangle.contains(rectangle, data.positionCartographic)) {
customData.push(data);
}
}
QuadtreeTile.prototype.updateCustomData = function () {
const added = this._addedCustomData;
const removed = this._removedCustomData;
if (added.length === 0 && removed.length === 0) {
return;
}

this._frameUpdated = frameNumber;
} else {
// interior or leaf tile, update from parent
const parent = this._parent;
if (defined(parent) && this._frameUpdated !== parent._frameUpdated) {
customData.length = 0;

rectangle = this._rectangle;
const parentCustomData = parent.customData;
for (i = 0; i < parentCustomData.length; ++i) {
data = parentCustomData[i];
if (Rectangle.contains(rectangle, data.positionCartographic)) {
customData.push(data);
}
}
const customData = this.customData;
for (let i = 0; i < added.length; ++i) {
const data = added[i];
customData.add(data);

const child = childTileAtPosition(this, data.positionCartographic);
child._addedCustomData.push(data);
}
this._addedCustomData.length = 0;

this._frameUpdated = parent._frameUpdated;
for (let i = 0; i < removed.length; ++i) {
const data = removed[i];
if (customData.has(data)) {
customData.delete(data);
}

const child = childTileAtPosition(this, data.positionCartographic);
child._removedCustomData.push(data);
}
this._removedCustomData.length = 0;
};

const splitPointScratch = new Cartographic();

/**
* Determines which child tile that contains the specified position. Assumes the position is within
* the bounds of the parent tile.
* @private
* @param {QuadtreeTile} tile - The parent tile.
* @param {Cartographic} positionCartographic - The cartographic position.
* @returns {QuadtreeTile} The child tile that contains the position.
*/
function childTileAtPosition(tile, positionCartographic) {
// Can't assume that a given tiling scheme divides a parent into four tiles at its rectangle's center.
// But we can safely take any child tile's rectangle and take its center-facing corner as the parent's split point.
const nwChildRectangle = tile.northwestChild.rectangle;
const tileSplitPoint = Rectangle.southeast(
nwChildRectangle,
splitPointScratch,
);

const x = positionCartographic.longitude >= tileSplitPoint.longitude ? 1 : 0;
const y = positionCartographic.latitude < tileSplitPoint.latitude ? 1 : 0;

switch (y * 2 + x) {
case 0:
return tile.northwestChild;
case 1:
return tile.northeastChild;
case 2:
return tile.southwestChild;
default:
return tile.southeastChild;
}
}

Object.defineProperties(QuadtreeTile.prototype, {
/**
* Gets the tiling scheme used to tile the surface.
Expand Down Expand Up @@ -522,9 +542,9 @@ Object.defineProperties(QuadtreeTile.prototype, {
},

/**
* An array of objects associated with this tile.
* A set of objects associated with this tile.
* @memberof QuadtreeTile.prototype
* @type {Array}
* @type {Set}
*/
customData: {
get: function () {
Expand Down
4 changes: 2 additions & 2 deletions packages/engine/Specs/Scene/QuadtreePrimitiveSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -899,7 +899,7 @@ describe("Scene/QuadtreePrimitive", function () {

let addedCallback = false;
quadtree.forEachLoadedTile(function (tile) {
addedCallback = addedCallback || tile.customData.length > 0;
addedCallback = addedCallback || tile.customData.size > 0;
});

expect(addedCallback).toEqual(true);
Expand All @@ -915,7 +915,7 @@ describe("Scene/QuadtreePrimitive", function () {

let removedCallback = true;
quadtree.forEachLoadedTile(function (tile) {
removedCallback = removedCallback && tile.customData.length === 0;
removedCallback = removedCallback && tile.customData.size === 0;
});

expect(removedCallback).toEqual(true);
Expand Down
49 changes: 49 additions & 0 deletions packages/engine/Specs/Scene/QuadtreeTileSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -458,4 +458,53 @@ describe("Scene/QuadtreeTile", function () {
expect(cachedData).toEqual(dummyPosition);
});
});

Copy link
Contributor Author

@mzschwartz5 mzschwartz5 Oct 13, 2025

Choose a reason for hiding this comment

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

Added some unit tests to make sure the update function is doing what it's supposed to, and that it works regardless of tiling scheme...

That said, I think it's bad practice to have a test per tiling scheme here. If someone adds a new one, how would they know we want to test it in QuadtreeTile of all places? Perhaps, instead, I should have defined the childTileAtPosition function in each tiling scheme instead of in QuadtreeTile, and that way each tiling scheme tests itself.

However, childTileAtPosition is currently implemented in such a way that a single implementation should be correct for all tiling schemes... So it doesn't need to be implemented per tiling scheme, and perhaps testing the two existing schemes is sufficient for now and the future? Input welcome.

describe("updateCustomData", function () {
function addAndRemoveCustomData(tilingScheme) {
const tile = new QuadtreeTile({
level: 0,
x: 0,
y: 0,
tilingScheme: tilingScheme,
});

const child = tile.northwestChild;
const centerCartographic = Rectangle.center(child.rectangle);

const data = {
positionCartographic: centerCartographic,
};

tile._addedCustomData.push(data);
tile.updateCustomData();

expect(tile.customData.has(data)).toBe(true);
expect(tile._addedCustomData.length).toBe(0);
expect(child._addedCustomData.length).toBe(1);
expect(child._addedCustomData[0]).toBe(data);

child.updateCustomData();
expect(child.customData.has(data)).toBe(true);

// Now remove the data from the parent tile.
tile._removedCustomData.push(data);
tile.updateCustomData();

expect(tile.customData.has(data)).toBe(false);
expect(tile._removedCustomData.length).toBe(0);
expect(child._removedCustomData.length).toBe(1);
expect(child._removedCustomData[0]).toBe(data);

child.updateCustomData();
expect(child.customData.has(data)).toBe(false);
}

it("can add and remove custom data when tiling scheme is GeographicTilingScheme", function () {
addAndRemoveCustomData(new GeographicTilingScheme());
});

it("can add and remove custom data when tiling scheme is WebMercatorTilingScheme", function () {
addAndRemoveCustomData(new WebMercatorTilingScheme());
});
});
});