Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
38 changes: 22 additions & 16 deletions packages/engine/Source/Core/Matrix3.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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<Matrix3>}
* @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<Matrix3>}
* @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;
158 changes: 108 additions & 50 deletions packages/engine/Source/Core/Matrix4.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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<Matrix4>} a frozen matrix
*/
// eslint-disable-next-line no-unused-vars
function makeFrozenMatrix4(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is basically just creating the old matrix type and manually binding the few added functions.

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<Matrix4>}
* @constant
*/
Matrix4.IDENTITY = freezeMatrix(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently using the freezeMatrix utility rather than makeFrozenMatrix, but they are fundamentally the same. The actual matrix is just a regular object that behaves exaclty like the previous object matrix, which are 100% compatible within the codebase because I didn't change any of the behavior when using a new Float64Array matrix.

The main difference is future-proofing/avoiding accidents where someone does choose to work with them as real typed arrays (maybe for performance reasons or whatever), in which case freezeMatrix is a much more solid solution that won't accidentally break because apparently the IDENTITY and ZERO matrices dont behave like Typed Arrays (which is the case with makeFrozenMatrix4).

Unlike with a Proxy, there is no misdirection so V8 can still attempt to optimize these, albeit only to the performance level of the old matrices.

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<Matrix4>}
* @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;
88 changes: 88 additions & 0 deletions packages/engine/Source/Core/freezeMatrix.js
Original file line number Diff line number Diff line change
@@ -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<T>} 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);
}
Loading