Skip to content
Closed
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
3 changes: 2 additions & 1 deletion packages/engine/Source/Core/Credit.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@ Credit.getIonCredit = function (attribution) {
*/
Credit.clone = function (credit) {
if (defined(credit)) {
return new Credit(credit.html, credit.showOnScreen);
return credit;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

What I did: loaded the basic Google Photorealistic sandcastle and profiled me just moving around slowly

Performance Profile:
I analyzed multiple frames, and saw that CreditDisplay.endFrame was taking up about 2% of frame time.

Image

Why this change:

The performance profile showed that due to the credit clones only passing the original HTML, every frame for all credits:

  1. The HTML has to be sanitized
  2. created as a DOM element
  3. appended to the CreditDisplay.

By passing credits by reference, the steps 1+2 are executed only once when the first credit of that id is added, and then every frame the generated element is reused.

Credits still properly get removed/added when displayed tilesets change. I'm not sure why credits aren't passed by reference, and this isn't a significant performance improvement alone, but together with the others it stacks up.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is fundamentally changing the behavior of this function to not actually be a clone like it's named. I don't think we want to do this. There may be merit in reducing the need to call .clone wherever it's called but a clone function itself should not be returning the original object.

I believe @mzschwartz5 was looking into credits recently and might have some more insights. I feel like this is something that could benefit from being managed higher up where we can store the references and reduce the amount .clone needs to be called in the first place.

Copy link
Contributor Author

@Beilinson Beilinson Oct 10, 2025

Choose a reason for hiding this comment

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

For sure, this was more of a POC that passing them by reference does not break rendering them fundamentally. If the team is already looking into this I'll refrain from opening this as a separate PR

// return new Credit(credit.html, credit.showOnScreen);
}
};
export default Credit;
52 changes: 25 additions & 27 deletions packages/engine/Source/Core/Matrix3.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,28 +35,30 @@ import CesiumMath from "./Math.js";
* @see Matrix2
* @see Matrix4
*/
function Matrix3(
column0Row0,
column1Row0,
column2Row0,
column0Row1,
column1Row1,
column2Row1,
column0Row2,
column1Row2,
column2Row2,
) {
this[0] = column0Row0 ?? 0.0;
this[1] = column0Row1 ?? 0.0;
this[2] = column0Row2 ?? 0.0;
this[3] = column1Row0 ?? 0.0;
this[4] = column1Row1 ?? 0.0;
this[5] = column1Row2 ?? 0.0;
this[6] = column2Row0 ?? 0.0;
this[7] = column2Row1 ?? 0.0;
this[8] = column2Row2 ?? 0.0;
class Matrix3 extends Float64Array {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Extended the improvements on Matrix4 to Matrix3 as well

constructor(
column0Row0,
column1Row0,
column2Row0,
column0Row1,
column1Row1,
column2Row1,
column0Row2,
column1Row2,
column2Row2,
) {
super(9);
this[0] = column0Row0 ?? 0.0;
this[1] = column0Row1 ?? 0.0;
this[2] = column0Row2 ?? 0.0;
this[3] = column1Row0 ?? 0.0;
this[4] = column1Row1 ?? 0.0;
this[5] = column1Row2 ?? 0.0;
this[6] = column2Row0 ?? 0.0;
this[7] = column2Row1 ?? 0.0;
this[8] = column2Row2 ?? 0.0;
}
}

/**
* The number of elements used to pack the object into an array.
* @type {number}
Expand Down Expand Up @@ -1671,19 +1673,15 @@ Matrix3.equalsEpsilon = function (left, right, epsilon) {
* @type {Matrix3}
* @constant
*/
Matrix3.IDENTITY = Object.freeze(
new Matrix3(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0),
);
Matrix3.IDENTITY = new Matrix3(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the limitation that it's not possible to .freeze() a typed array in JS (another explination) is enough to give me pause about changing this implementation.

The identity and zero matrices are used frequently across CesiumJS and I'd assume other projects that use CesiumJS. There needs to be a way to guarantee these are always the expected values. Users should not be allowed to do this:

console.log(Cesium.Matrix3.IDENTITY);
Cesium.Matrix3.IDENTITY[2] = 27.0;
console.log(Cesium.Matrix3.IDENTITY);

Currently this code throws an error but in this branch this works just fine and the identity is now wrong in every place it's used.

It's possible this could become a getter style function that always returns a new matrix but that defeats the purpose of having a shared object and could create more memory issues as it creates new objects every time.

Are there ways to mix frozen, normal arrays and typed arrays? Is there a different way to change internal structure based on expected mutability? Would that just make it more painful and inconsistent to use?

I don't know the answer to these questions but we need to figure it out before pushing forward with this change.

Copy link
Contributor Author

@Beilinson Beilinson Oct 10, 2025

Choose a reason for hiding this comment

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

There is no issue mixing old object matrices and new array ones, so keeping these as old frozen object based matrices is a valid solution.

However:

  1. It will slow down the total performance gains to make the frozen matrices not typed array based
  2. if anyone changes the frozen matrices that would instantly break the entire application (so in my opinion it's an unreasonable expectation)
  3. Nothing prevents in the current cesium code someone from doing Cesium.Matrix3.IDENTITY = new Cesium.Matrix3(), since the actual Matrix3,Matrix4,etc. namespace objects aren't frozen themselves.

If after further consideration you believe it's still important these specifically can be reverted to frozen object matrices

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Lets continue this conversation in #12973


/**
* An immutable Matrix3 instance initialized to the zero matrix.
*
* @type {Matrix3}
* @constant
*/
Matrix3.ZERO = Object.freeze(
new Matrix3(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
);
Matrix3.ZERO = new Matrix3(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);

/**
* The index into Matrix3 for column 0, row 0.
Expand Down
187 changes: 84 additions & 103 deletions packages/engine/Source/Core/Matrix4.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,40 +53,43 @@ import RuntimeError from "./RuntimeError.js";
* @see Matrix3
* @see Packable
*/
function Matrix4(
column0Row0,
column1Row0,
column2Row0,
column3Row0,
column0Row1,
column1Row1,
column2Row1,
column3Row1,
column0Row2,
column1Row2,
column2Row2,
column3Row2,
column0Row3,
column1Row3,
column2Row3,
column3Row3,
) {
this[0] = column0Row0 ?? 0.0;
this[1] = column0Row1 ?? 0.0;
this[2] = column0Row2 ?? 0.0;
this[3] = column0Row3 ?? 0.0;
this[4] = column1Row0 ?? 0.0;
this[5] = column1Row1 ?? 0.0;
this[6] = column1Row2 ?? 0.0;
this[7] = column1Row3 ?? 0.0;
this[8] = column2Row0 ?? 0.0;
this[9] = column2Row1 ?? 0.0;
this[10] = column2Row2 ?? 0.0;
this[11] = column2Row3 ?? 0.0;
this[12] = column3Row0 ?? 0.0;
this[13] = column3Row1 ?? 0.0;
this[14] = column3Row2 ?? 0.0;
this[15] = column3Row3 ?? 0.0;
class Matrix4 extends Float64Array {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The most important improvement here. Matrices are at the heart of 3D graphics, and any performance improvement here radiates throughout the entire codebase.

I approached this primarily to speed up pickModel, where getVertexPosition is primarily limited by the performance of Matrix4.multiplyByPoint.

By making Matrix4 a class extending Float64Array, we get a 2x speedup in that method (as well as various levels of speedups on other methods):

Image

I show the performance improvements of this combined with @javagl 's branch #12658 in #11814.

Importantly, changing the backing datastructure to Float64Array has 100% identical behavior and results as the previous implementation. As per https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number#number_encoding, regular numbers in JS are also 64 bit floats. By using the class .. extends syntax, we continue having values accessed by doing matrix[i], rather than forcing something like matrix.buffer[i].

constructor(
column0Row0,
column1Row0,
column2Row0,
column3Row0,
column0Row1,
column1Row1,
column2Row1,
column3Row1,
column0Row2,
column1Row2,
column2Row2,
column3Row2,
column0Row3,
column1Row3,
column2Row3,
column3Row3,
) {
super(16);
this[0] = column0Row0 ?? 0.0;
this[1] = column0Row1 ?? 0.0;
this[2] = column0Row2 ?? 0.0;
this[3] = column0Row3 ?? 0.0;
this[4] = column1Row0 ?? 0.0;
this[5] = column1Row1 ?? 0.0;
this[6] = column1Row2 ?? 0.0;
this[7] = column1Row3 ?? 0.0;
this[8] = column2Row0 ?? 0.0;
this[9] = column2Row1 ?? 0.0;
this[10] = column2Row2 ?? 0.0;
this[11] = column2Row3 ?? 0.0;
this[12] = column3Row0 ?? 0.0;
this[13] = column3Row1 ?? 0.0;
this[14] = column3Row2 ?? 0.0;
this[15] = column3Row3 ?? 0.0;
}
}

/**
Expand Down Expand Up @@ -1929,41 +1932,23 @@ Matrix4.multiplyTransformation = function (left, right, result) {
const right13 = right[13];
const right14 = right[14];

const column0Row0 = left0 * right0 + left4 * right1 + left8 * right2;
const column0Row1 = left1 * right0 + left5 * right1 + left9 * right2;
const column0Row2 = left2 * right0 + left6 * right1 + left10 * right2;

const column1Row0 = left0 * right4 + left4 * right5 + left8 * right6;
const column1Row1 = left1 * right4 + left5 * right5 + left9 * right6;
const column1Row2 = left2 * right4 + left6 * right5 + left10 * right6;

const column2Row0 = left0 * right8 + left4 * right9 + left8 * right10;
const column2Row1 = left1 * right8 + left5 * right9 + left9 * right10;
const column2Row2 = left2 * right8 + left6 * right9 + left10 * right10;

const column3Row0 =
left0 * right12 + left4 * right13 + left8 * right14 + left12;
const column3Row1 =
left1 * right12 + left5 * right13 + left9 * right14 + left13;
const column3Row2 =
left2 * right12 + left6 * right13 + left10 * right14 + left14;

result[0] = column0Row0;
result[1] = column0Row1;
result[2] = column0Row2;
result[0] = left0 * right0 + left4 * right1 + left8 * right2;
result[1] = left1 * right0 + left5 * right1 + left9 * right2;
result[2] = left2 * right0 + left6 * right1 + left10 * right2;
result[3] = 0.0;
result[4] = column1Row0;
result[5] = column1Row1;
result[6] = column1Row2;
result[4] = left0 * right4 + left4 * right5 + left8 * right6;
result[5] = left1 * right4 + left5 * right5 + left9 * right6;
result[6] = left2 * right4 + left6 * right5 + left10 * right6;
result[7] = 0.0;
result[8] = column2Row0;
result[9] = column2Row1;
result[10] = column2Row2;
result[8] = left0 * right8 + left4 * right9 + left8 * right10;
result[9] = left1 * right8 + left5 * right9 + left9 * right10;
result[10] = left2 * right8 + left6 * right9 + left10 * right10;
result[11] = 0.0;
result[12] = column3Row0;
result[13] = column3Row1;
result[14] = column3Row2;
result[12] = left0 * right12 + left4 * right13 + left8 * right14 + left12;
result[13] = left1 * right12 + left5 * right13 + left9 * right14 + left13;
result[14] = left2 * right12 + left6 * right13 + left10 * right14 + left14;
result[15] = 1.0;

return result;
};

Expand Down Expand Up @@ -2968,25 +2953,23 @@ Matrix4.inverseTranspose = function (matrix, result) {
* @type {Matrix4}
* @constant
*/
Matrix4.IDENTITY = Object.freeze(
new Matrix4(
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
),
Matrix4.IDENTITY = new Matrix4(
Copy link
Contributor Author

@Beilinson Beilinson Oct 10, 2025

Choose a reason for hiding this comment

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

1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
);

/**
Expand All @@ -2995,25 +2978,23 @@ Matrix4.IDENTITY = Object.freeze(
* @type {Matrix4}
* @constant
*/
Matrix4.ZERO = Object.freeze(
new Matrix4(
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
),
Matrix4.ZERO = new Matrix4(
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
);

/**
Expand Down
16 changes: 7 additions & 9 deletions packages/engine/Source/Scene/Cesium3DTilesetStatistics.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function Cesium3DTilesetStatistics() {
// Memory statistics
this.geometryByteLength = 0;
this.texturesByteLength = 0;
this.texturesReferenceCounterById = {};
this.texturesReferenceCounterById = new Map();
this.batchTableByteLength = 0; // batch textures and any binary metadata properties not otherwise accounted for
}

Expand Down Expand Up @@ -100,12 +100,12 @@ Cesium3DTilesetStatistics.prototype.incrementLoadCounts = function (content) {
const textureIds = content.getTextureIds();
for (const textureId of textureIds) {
const referenceCounter =
this.texturesReferenceCounterById[textureId] ?? 0;
this.texturesReferenceCounterById.get(textureId) ?? 0;
if (referenceCounter === 0) {
const textureByteLength = content.getTextureByteLengthById(textureId);
this.texturesByteLength += textureByteLength;
}
this.texturesReferenceCounterById[textureId] = referenceCounter + 1;
this.texturesReferenceCounterById.set(textureId, referenceCounter + 1);
}
}

Expand Down Expand Up @@ -146,13 +146,13 @@ Cesium3DTilesetStatistics.prototype.decrementLoadCounts = function (content) {
// total textures byte length
const textureIds = content.getTextureIds();
for (const textureId of textureIds) {
const referenceCounter = this.texturesReferenceCounterById[textureId];
const referenceCounter = this.texturesReferenceCounterById.get(textureId);
if (referenceCounter === 1) {
delete this.texturesReferenceCounterById[textureId];
this.texturesReferenceCounterById.delete(textureId);
const textureByteLength = content.getTextureByteLengthById(textureId);
this.texturesByteLength -= textureByteLength;
} else {
this.texturesReferenceCounterById[textureId] = referenceCounter - 1;
this.texturesReferenceCounterById.set(textureId, referenceCounter - 1);
}
}
}
Expand Down Expand Up @@ -187,9 +187,7 @@ Cesium3DTilesetStatistics.clone = function (statistics, result) {
statistics.numberOfTilesCulledWithChildrenUnion;
result.geometryByteLength = statistics.geometryByteLength;
result.texturesByteLength = statistics.texturesByteLength;
result.texturesReferenceCounterById = {
...statistics.texturesReferenceCounterById,
};
result.texturesReferenceCounterById = statistics.texturesReferenceCounterById;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

What I did: loaded the basic Google Photorealistic sandcastle and profiled me just moving around slowly

Performance Profile:
I ordered by Self Time descending, and saw that this Cesium3DTilesetStatistics.clone takes about 8% of total frame time.
Image

Why this change:
The performance profile showed that this object destructure was taking up the main bulk of the time of this function.

I audited the code, and saw that while the statistics object is copied frame-to-frame, this property specifically is only ever used by the new statistics object, and passing it by reference kept all the same behavior in prod. Changing it a Map is also a minor performance improvement.

result.batchTableByteLength = statistics.batchTableByteLength;
};
export default Cesium3DTilesetStatistics;
8 changes: 8 additions & 0 deletions packages/engine/Source/Scene/Cesium3DTilesetTraversal.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,11 @@ function isOnScreenLongEnough(tile, frameState) {
* @param {FrameState} frameState
*/
Cesium3DTilesetTraversal.updateTile = function (tile, frameState) {
if (tile._visitedFrame === frameState.frameNumber) {
// Prevents another pass from visiting the frame again
return;
Comment on lines +191 to +193
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The tileset statistics are kind of wrong today. Only tiles which are marked as visible and added to the traversal queue are marked as visited, however in anyChildrenVisible and in updateVisibility there is the possibility that additional tiles are updated and visited but because they return false they are not added to the traversal queue and therefore not marked as visited for the statistics.

This fixes the above, now all tiles that have they visibility updated are correctly marked as visited. In addition, any previously visited tiles this frame are early returned right away, which already exists but is slightly further below in the call chain somewhere inside of updateTileVisibility.

This change shows that on average, there are actually around 60% more visited tiles than previously reported!!

}
Cesium3DTilesetTraversal.visitTile(tile, frameState);
updateTileVisibility(tile, frameState);
tile.updateExpiration();

Expand Down Expand Up @@ -279,6 +284,9 @@ function anyChildrenVisible(tile, frameState) {
const child = children[i];
child.updateVisibility(frameState);
anyVisible = anyVisible || child.isVisible;
if (anyVisible) {
break;
}
Comment on lines +287 to +289
Copy link
Contributor Author

Choose a reason for hiding this comment

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

What I did: loaded the basic Google Photorealistic sandcastle and profiled me just moving around slowly

Performance Profile:

I checked in total, what part of the frametime was spent by the 3D Tileset. Between 15-30% of each frame is spent in either of the Cesium3DTileset<TYPE>Traversal.selectTiles:

Image Image

Why this change:

The performance profile showed that vast majority of time was spent in the tileset traversal updating visibility. I noticed that this method while by name acts like Array.some, which returns early after one item returns true, it actually iterates over all tile children.

This change results in about 10% fewer tiles visited total (after the change above to make visited tiles more accurate), but I didn't notice a consistent improvement in FPS from this sadly.

This change does not from my testing affect the final selected tiles.

}
return anyVisible;
}
Expand Down
Loading
Loading