From 460a8d6d963fb77213c954d740064e431c106b69 Mon Sep 17 00:00:00 2001 From: Adam Beili Date: Fri, 10 Oct 2025 22:37:01 +0300 Subject: [PATCH 1/6] Float64Buffer Matrices --- packages/engine/Source/Core/Matrix3.js | 52 +++--- packages/engine/Source/Core/Matrix4.js | 187 ++++++++++------------ packages/engine/Specs/Core/Matrix3Spec.js | 6 +- packages/engine/Specs/Core/Matrix4Spec.js | 6 +- 4 files changed, 115 insertions(+), 136 deletions(-) diff --git a/packages/engine/Source/Core/Matrix3.js b/packages/engine/Source/Core/Matrix3.js index 7a7ab6c3213e..7887f3c53ba3 100644 --- a/packages/engine/Source/Core/Matrix3.js +++ b/packages/engine/Source/Core/Matrix3.js @@ -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 { + 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} @@ -1671,9 +1673,7 @@ 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); /** * An immutable Matrix3 instance initialized to the zero matrix. @@ -1681,9 +1681,7 @@ Matrix3.IDENTITY = Object.freeze( * @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. diff --git a/packages/engine/Source/Core/Matrix4.js b/packages/engine/Source/Core/Matrix4.js index f30b8a733905..80e0afeea836 100644 --- a/packages/engine/Source/Core/Matrix4.js +++ b/packages/engine/Source/Core/Matrix4.js @@ -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 { + 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; + } } /** @@ -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; }; @@ -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( + 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, ); /** @@ -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, ); /** diff --git a/packages/engine/Specs/Core/Matrix3Spec.js b/packages/engine/Specs/Core/Matrix3Spec.js index 3d2f4641aeb7..61aeb35c4cb0 100644 --- a/packages/engine/Specs/Core/Matrix3Spec.js +++ b/packages/engine/Specs/Core/Matrix3Spec.js @@ -358,7 +358,7 @@ describe("Core/Matrix3", function () { it("fromRotationX works without a result parameter", function () { const matrix = Matrix3.fromRotationX(0.0); - expect(matrix).toEqual(Matrix3.IDENTITY); + expect(Matrix3.equals(matrix, Matrix3.IDENTITY)).toBe(true); }); it("fromRotationX works with a result parameter", function () { @@ -371,7 +371,7 @@ describe("Core/Matrix3", function () { it("fromRotationY works without a result parameter", function () { const matrix = Matrix3.fromRotationY(0.0); - expect(matrix).toEqual(Matrix3.IDENTITY); + expect(Matrix3.equals(matrix, Matrix3.IDENTITY)).toBe(true); }); it("fromRotationY works with a result parameter", function () { @@ -384,7 +384,7 @@ describe("Core/Matrix3", function () { it("fromRotationZ works without a result parameter", function () { const matrix = Matrix3.fromRotationZ(0.0); - expect(matrix).toEqual(Matrix3.IDENTITY); + expect(Matrix3.equals(matrix, Matrix3.IDENTITY)).toBe(true); }); it("fromRotationZ works with a result parameter", function () { diff --git a/packages/engine/Specs/Core/Matrix4Spec.js b/packages/engine/Specs/Core/Matrix4Spec.js index 375416d0a849..439d9ec78f86 100644 --- a/packages/engine/Specs/Core/Matrix4Spec.js +++ b/packages/engine/Specs/Core/Matrix4Spec.js @@ -727,7 +727,7 @@ describe("Core/Matrix4", function () { direction: Cartesian3.negate(Cartesian3.UNIT_Z, new Cartesian3()), up: Cartesian3.UNIT_Y, }); - expect(expected).toEqual(returnedResult); + expect(Matrix4.equals(expected, returnedResult)).toBe(true); }); it("fromCamera works with a result parameter", function () { @@ -742,7 +742,7 @@ describe("Core/Matrix4", function () { result, ); expect(returnedResult).toBe(result); - expect(returnedResult).toEqual(expected); + expect(Matrix4.equals(expected, returnedResult)).toBe(true); }); it("computeOrthographicOffCenter works", function () { @@ -1392,7 +1392,7 @@ describe("Core/Matrix4", function () { it("setRotation works", function () { const scaleVec = new Cartesian3(2.0, 3.0, 4.0); - const scale = Matrix4.fromScale(scaleVec, new Matrix3()); + const scale = Matrix4.fromScale(scaleVec, new Matrix4()); const rotation = Matrix3.fromRotationX(0.5, new Matrix3()); const scaleRotation = Matrix4.setRotation(scale, rotation, new Matrix4()); From 7dd0a5ac46c28b350bf8544998fe0556c631b686 Mon Sep 17 00:00:00 2001 From: Adam Beili Date: Fri, 10 Oct 2025 22:58:25 +0300 Subject: [PATCH 2/6] Fix failing tests --- packages/engine/Specs/Scene/CameraSpec.js | 2 +- packages/engine/Specs/Scene/GltfLoaderUtilSpec.js | 4 +++- packages/engine/Specs/Scene/PropertyTexturePropertySpec.js | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/engine/Specs/Scene/CameraSpec.js b/packages/engine/Specs/Scene/CameraSpec.js index 6cda5632cd20..dda1db7ce6a4 100644 --- a/packages/engine/Specs/Scene/CameraSpec.js +++ b/packages/engine/Specs/Scene/CameraSpec.js @@ -130,7 +130,7 @@ describe("Scene/Camera", function () { 1.0, ); const expected = Matrix4.multiply(rotation, translation, new Matrix4()); - expect(viewMatrix).toEqual(expected); + expect(Matrix4.equals(viewMatrix, expected)).toBe(true); }); it("get inverse view matrix", function () { diff --git a/packages/engine/Specs/Scene/GltfLoaderUtilSpec.js b/packages/engine/Specs/Scene/GltfLoaderUtilSpec.js index bef2cf665012..235def55ddcd 100644 --- a/packages/engine/Specs/Scene/GltfLoaderUtilSpec.js +++ b/packages/engine/Specs/Scene/GltfLoaderUtilSpec.js @@ -328,7 +328,9 @@ describe( }); expect(modelTexture.texCoord).toBe(1); - expect(modelTexture.transform).toEqual(expectedTransform); + expect(Matrix3.equals(modelTexture.transform, expectedTransform)).toBe( + true, + ); }); it("createModelTextureReader handles KHR_texture_transform rotation correctly", function () { diff --git a/packages/engine/Specs/Scene/PropertyTexturePropertySpec.js b/packages/engine/Specs/Scene/PropertyTexturePropertySpec.js index 0bdb4bc5bea5..db6cc8d0c7fa 100644 --- a/packages/engine/Specs/Scene/PropertyTexturePropertySpec.js +++ b/packages/engine/Specs/Scene/PropertyTexturePropertySpec.js @@ -109,7 +109,9 @@ describe( const modelTextureReader = propertyTextureProperty.textureReader; expect(modelTextureReader.texture).toBe(texture); expect(modelTextureReader.texCoord).toBe(1); - expect(modelTextureReader.transform).toEqual(expectedTransform); + expect( + Matrix3.equals(modelTextureReader.transform, expectedTransform), + ).toBe(true); expect(modelTextureReader.channels).toBe("rgb"); }); From 6a759d3b9f9db13f05978ad72a4495f312c093f4 Mon Sep 17 00:00:00 2001 From: Adam Beili Date: Fri, 10 Oct 2025 23:08:18 +0300 Subject: [PATCH 3/6] Add changelog --- CHANGES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 23f5e58cbf33..497d34fef2ba 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,10 @@ - `scene.drillPick` now uses a breadth-first search strategy instead of depth-first. This may change which entities are picked when using large values of `width` and `height` when providing a `limit`, prioritizing entities closer to the camera. +#### Additions :tada: + +- Matrices are now backed by `Float64Array`'s, making operations up to 2x faster, resulting in performance improvements across the board. [#12973](https://github.com/CesiumGS/cesium/pull/12973) + #### Fixes :wrench: - Fixes an event bug following recent changes, where adding a new listener during an event callback caused an infinite loop. [#12955](https://github.com/CesiumGS/cesium/pull/12955) From 7c80345833d32461ba0106cdd4814287c7c8c304 Mon Sep 17 00:00:00 2001 From: Adam Beili Date: Sun, 12 Oct 2025 01:00:28 +0300 Subject: [PATCH 4/6] Options for Frozen Matrices --- packages/engine/Source/Core/Matrix3.js | 38 +++-- packages/engine/Source/Core/Matrix4.js | 158 +++++++++++++------- packages/engine/Source/Core/freezeMatrix.js | 88 +++++++++++ 3 files changed, 218 insertions(+), 66 deletions(-) create mode 100644 packages/engine/Source/Core/freezeMatrix.js diff --git a/packages/engine/Source/Core/Matrix3.js b/packages/engine/Source/Core/Matrix3.js index 7887f3c53ba3..0a066385be75 100644 --- a/packages/engine/Source/Core/Matrix3.js +++ b/packages/engine/Source/Core/Matrix3.js @@ -2,6 +2,7 @@ import Cartesian3 from "./Cartesian3.js"; import Check from "./Check.js"; import defined from "./defined.js"; import DeveloperError from "./DeveloperError.js"; +import freezeMatrix from "./freezeMatrix.js"; import CesiumMath from "./Math.js"; /** @@ -1667,22 +1668,6 @@ Matrix3.equalsEpsilon = function (left, right, epsilon) { ); }; -/** - * An immutable Matrix3 instance initialized to the identity matrix. - * - * @type {Matrix3} - * @constant - */ -Matrix3.IDENTITY = new Matrix3(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); - -/** - * An immutable Matrix3 instance initialized to the zero matrix. - * - * @type {Matrix3} - * @constant - */ -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. * @@ -1833,4 +1818,25 @@ Matrix3.prototype.toString = function () { `(${this[2]}, ${this[5]}, ${this[8]})` ); }; + +/** + * An immutable Matrix3 instance initialized to the identity matrix. + * + * @type {Readonly} + * @constant + */ +Matrix3.IDENTITY = freezeMatrix( + new Matrix3(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0), +); + +/** + * An immutable Matrix3 instance initialized to the zero matrix. + * + * @type {Readonly} + * @constant + */ +Matrix3.ZERO = freezeMatrix( + new Matrix3(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), +); + export default Matrix3; diff --git a/packages/engine/Source/Core/Matrix4.js b/packages/engine/Source/Core/Matrix4.js index 80e0afeea836..9d67d9a61124 100644 --- a/packages/engine/Source/Core/Matrix4.js +++ b/packages/engine/Source/Core/Matrix4.js @@ -7,6 +7,7 @@ import DeveloperError from "./DeveloperError.js"; import CesiumMath from "./Math.js"; import Matrix3 from "./Matrix3.js"; import RuntimeError from "./RuntimeError.js"; +import freezeMatrix from "./freezeMatrix.js"; /** * A 4x4 matrix, indexable as a column-major order array. @@ -2947,56 +2948,6 @@ Matrix4.inverseTranspose = function (matrix, result) { ); }; -/** - * An immutable Matrix4 instance initialized to the identity matrix. - * - * @type {Matrix4} - * @constant - */ -Matrix4.IDENTITY = 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, -); - -/** - * An immutable Matrix4 instance initialized to the zero matrix. - * - * @type {Matrix4} - * @constant - */ -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, -); - /** * The index into Matrix4 for column 0, row 0. * @@ -3211,4 +3162,111 @@ Matrix4.prototype.toString = function () { `(${this[3]}, ${this[7]}, ${this[11]}, ${this[15]})` ); }; + +// Frozen Matrix option 1: Only implement the interface as generated by Cesium.d.ts (meaning, without exposing any Float64Array methods) +/** + * @returns {Readonly} a frozen matrix + */ +// eslint-disable-next-line no-unused-vars +function makeFrozenMatrix4( + column0Row0, + column1Row0, + column2Row0, + column3Row0, + column0Row1, + column1Row1, + column2Row1, + column3Row1, + column0Row2, + column1Row2, + column2Row2, + column3Row2, + column0Row3, + column1Row3, + column2Row3, + column3Row3, +) { + /** @type {Matrix4} */ + const matrix = { + [0]: column0Row0 ?? 0.0, + [1]: column0Row1 ?? 0.0, + [2]: column0Row2 ?? 0.0, + [3]: column0Row3 ?? 0.0, + [4]: column1Row0 ?? 0.0, + [5]: column1Row1 ?? 0.0, + [6]: column1Row2 ?? 0.0, + [7]: column1Row3 ?? 0.0, + [8]: column2Row0 ?? 0.0, + [9]: column2Row1 ?? 0.0, + [10]: column2Row2 ?? 0.0, + [11]: column2Row3 ?? 0.0, + [12]: column3Row0 ?? 0.0, + [13]: column3Row1 ?? 0.0, + [14]: column3Row2 ?? 0.0, + [15]: column3Row3 ?? 0.0, + length: Matrix4.packedLength, + }; + + matrix.clone = Matrix4.prototype.clone.bind(matrix); + matrix.equals = Matrix4.prototype.equals.bind(matrix); + matrix.equalsEpsilon = Matrix4.prototype.equalsEpsilon.bind(matrix); + matrix.toString = Matrix4.prototype.toString.bind(matrix); + + return Object.freeze(matrix); +} + +/** + * An immutable Matrix4 instance initialized to the identity matrix. + * + * @type {Readonly} + * @constant + */ +Matrix4.IDENTITY = freezeMatrix( + 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, + ), +); + +/** + * An immutable Matrix4 instance initialized to the zero matrix. + * + * @type {Readonly} + * @constant + */ +Matrix4.ZERO = freezeMatrix( + 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, + ), +); + export default Matrix4; diff --git a/packages/engine/Source/Core/freezeMatrix.js b/packages/engine/Source/Core/freezeMatrix.js new file mode 100644 index 000000000000..069e2534ffa5 --- /dev/null +++ b/packages/engine/Source/Core/freezeMatrix.js @@ -0,0 +1,88 @@ +import DeveloperError from "./DeveloperError"; + +// Deep copies all property descriptors of the provided object. +function getPropertyDescriptorMap(object) { + /** @type {PropertyDescriptorMap} */ + const propertyDescriptorMap = {}; + let current = object; + + while (current) { + Object.entries(Object.getOwnPropertyDescriptors(current)).forEach( + ([name, descriptor]) => { + if (propertyDescriptorMap[name]) { + return; + } + propertyDescriptorMap[name] = descriptor; + }, + ); + Object.getOwnPropertySymbols(current).forEach( + // eslint-disable-next-line no-loop-func + (symbol) => { + if (propertyDescriptorMap[symbol]) { + return; + } + propertyDescriptorMap[symbol] = { + value: object[symbol], + enumerable: false, + }; + }, + ); + + // Move up the prototype chain + current = Object.getPrototypeOf(current); + if (current.constructor.name === "Object") { + break; + } + } + + return propertyDescriptorMap; +} + +// Frozen Matrix option 2: Generate a true Matrix4, including all underlying Float64Array methods. +// This has 100% parity with a regular Matrix4, with the exception that calling any methods that change the underlying matrix, +// or accessing the buffer will throw a DeveloperError +/** + * @template T + * @param {T} matrix + * @returns {Readonly} a frozen matrix + */ +export default function freezeMatrix(matrix) { + const properties = getPropertyDescriptorMap(matrix); + // Get both regular and symbol keys + for (const property of Reflect.ownKeys(properties)) { + const descriptor = properties[property]; + // These methods manipulate directly or allow the manipulation of the underlying buffer. Make them illegal. + if ( + ["copyWithin", "fill", "reverse", "set", "sort", "subarray"].includes( + property, + ) + ) { + descriptor.value = function () { + throw new DeveloperError(`Cannot call ${property} on a frozen Matrix`); + }; + } else if (typeof descriptor.value === "function") { + // Proxy all functions back onto the original matrix + descriptor.value = descriptor.value.bind(matrix); + } + // Deny access the underlying buffer on the facade + if (property === "buffer") { + descriptor.get = function () { + throw new DeveloperError(`Cannot access buffer of a frozen Matrix`); + }; + delete descriptor.value; + } + } + properties.toString = { + value: matrix.toString, + }; + properties.constructor = { + value: matrix.constructor, + }; + + // Create a fake matrix object and map all properties of the real matrix onto it + const fakeMatrix = Object.create(Object, properties); + + // Freeze the fake matrix object. We know have a fully compatible Matrix clone which is still backed + // by an underlying Float64Array, but is completed frozen to external changes. + return Object.freeze(fakeMatrix); +} From 3075077452e69c60f843f2b03952c8ca09fc2e5f Mon Sep 17 00:00:00 2001 From: Adam Beili Date: Sun, 12 Oct 2025 01:08:07 +0300 Subject: [PATCH 5/6] Fix build error --- packages/engine/Source/Core/freezeMatrix.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/engine/Source/Core/freezeMatrix.js b/packages/engine/Source/Core/freezeMatrix.js index 069e2534ffa5..a1b66a28d4ed 100644 --- a/packages/engine/Source/Core/freezeMatrix.js +++ b/packages/engine/Source/Core/freezeMatrix.js @@ -1,4 +1,4 @@ -import DeveloperError from "./DeveloperError"; +import DeveloperError from "./DeveloperError.js"; // Deep copies all property descriptors of the provided object. function getPropertyDescriptorMap(object) { From 28286e9e28f512b38eeabca3adc79288872d130f Mon Sep 17 00:00:00 2001 From: Adam Beili Date: Thu, 13 Nov 2025 19:33:03 +0100 Subject: [PATCH 6/6] Update changes --- CHANGES.md | 5 +---- packages/engine/Source/Core/freezeMatrix.js | 21 +++++++++------------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c4fc015da67d..d6262e226ec3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,7 @@ #### Additions :tada: - Added `scene.pickAsync` for non GPU blocking picking using WebGL2 [#12983](https://github.com/CesiumGS/cesium/pull/12983) +- Speed up `MatrixN` operations, improving performance when picking terrain and 3D tiles. [#12973](https://github.com/CesiumGS/cesium/pull/12973) ## 1.135 - 2025-11-03 @@ -26,10 +27,6 @@ - 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) - 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) -#### Additions :tada: - -- Matrices are now backed by `Float64Array`'s, making operations up to 2x faster, resulting in performance improvements across the board. [#12973](https://github.com/CesiumGS/cesium/pull/12973) - #### Fixes :wrench: - Improved performance of `scene.drillPick`. [#12916](https://github.com/CesiumGS/cesium/pull/12916) diff --git a/packages/engine/Source/Core/freezeMatrix.js b/packages/engine/Source/Core/freezeMatrix.js index a1b66a28d4ed..27b199e60dea 100644 --- a/packages/engine/Source/Core/freezeMatrix.js +++ b/packages/engine/Source/Core/freezeMatrix.js @@ -15,18 +15,15 @@ function getPropertyDescriptorMap(object) { propertyDescriptorMap[name] = descriptor; }, ); - Object.getOwnPropertySymbols(current).forEach( - // eslint-disable-next-line no-loop-func - (symbol) => { - if (propertyDescriptorMap[symbol]) { - return; - } - propertyDescriptorMap[symbol] = { - value: object[symbol], - enumerable: false, - }; - }, - ); + Object.getOwnPropertySymbols(current).forEach((symbol) => { + if (propertyDescriptorMap[symbol]) { + return; + } + propertyDescriptorMap[symbol] = { + value: object[symbol], + enumerable: false, + }; + }); // Move up the prototype chain current = Object.getPrototypeOf(current);