From 72f99e5f551f87b5a4a30710431e97815bd19c73 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Tue, 1 Sep 2026 12:56:57 -0700 Subject: [PATCH] Reduce MatrixMathHelper allocations (#58275) Summary: Matrix decomposition runs while applying view transforms and currently allocates a nested normalized matrix plus new arrays for each vector operation. Flatten the normalized matrix and update the temporary rows in place to remove ten allocations per decomposition without changing the public helpers. Changelog: [Internal] Reviewed By: cortinico Differential Revision: D118277120 --- .../react/uimanager/MatrixMathHelper.kt | 86 ++++++++++++------- .../react/uimanager/MatrixMathHelperTest.kt | 33 +++++++ 2 files changed, 87 insertions(+), 32 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/MatrixMathHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/MatrixMathHelper.kt index f090e32c515..3f5ffd3700b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/MatrixMathHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/MatrixMathHelper.kt @@ -8,7 +8,6 @@ package com.facebook.react.uimanager import com.facebook.infer.annotation.Assertions -import kotlin.math.abs import kotlin.math.atan2 import kotlin.math.cos import kotlin.math.sin @@ -22,11 +21,7 @@ import kotlin.math.tan public object MatrixMathHelper { private const val EPSILON = .00001 - private fun isZero(d: Double): Boolean { - return if (java.lang.Double.isNaN(d)) { - false - } else abs(d) < EPSILON - } + private fun isZero(d: Double): Boolean = d > -EPSILON && d < EPSILON @JvmStatic public fun multiplyInto(out: DoubleArray, a: DoubleArray, b: DoubleArray) { @@ -92,17 +87,16 @@ public object MatrixMathHelper { val translation = ctx.translation val rotationDegrees = ctx.rotationDegrees - // create normalized, 2d array matrix - // and normalized 1d array perspectiveMatrix with redefined 4th column if (isZero(transformMatrix[15])) { return } - val matrix = Array(4) { DoubleArray(4) } + + val normalizedMatrix = DoubleArray(16) val perspectiveMatrix = DoubleArray(16) for (i in 0..3) { for (j in 0..3) { val value = transformMatrix[i * 4 + j] / transformMatrix[15] - matrix[i][j] = value + normalizedMatrix[i * 4 + j] = value perspectiveMatrix[i * 4 + j] = if (j == 3) 0.0 else value } } @@ -114,10 +108,19 @@ public object MatrixMathHelper { } // isolate perspective - if (!isZero(matrix[0][3]) || !isZero(matrix[1][3]) || !isZero(matrix[2][3])) { + if ( + !isZero(normalizedMatrix[3]) || + !isZero(normalizedMatrix[7]) || + !isZero(normalizedMatrix[11]) + ) { // rightHandSide is the right hand side of the equation. // rightHandSide is a vector, or point in 3d space relative to the origin. - val rightHandSide = doubleArrayOf(matrix[0][3], matrix[1][3], matrix[2][3], matrix[3][3]) + val rightHandSide = doubleArrayOf( + normalizedMatrix[3], + normalizedMatrix[7], + normalizedMatrix[11], + normalizedMatrix[15], + ) // Solve the equation by inverting perspectiveMatrix and multiplying // rightHandSide by the inverse. @@ -126,50 +129,51 @@ public object MatrixMathHelper { multiplyVectorByMatrix(rightHandSide, transposedInversePerspectiveMatrix, perspective) } else { // no perspective + perspective[0] = 0.0 + perspective[1] = 0.0 perspective[2] = 0.0 - perspective[1] = perspective[2] - perspective[0] = perspective[1] perspective[3] = 1.0 } - // translation is simple - for (i in 0..2) { - translation[i] = matrix[3][i] - } + translation[0] = normalizedMatrix[12] + translation[1] = normalizedMatrix[13] + translation[2] = normalizedMatrix[14] // Now get scale and shear. // 'row' is a 3 element array of 3 component vectors val row = Array(3) { DoubleArray(3) } for (i in 0..2) { - row[i][0] = matrix[i][0] - row[i][1] = matrix[i][1] - row[i][2] = matrix[i][2] + row[i][0] = normalizedMatrix[i * 4] + row[i][1] = normalizedMatrix[i * 4 + 1] + row[i][2] = normalizedMatrix[i * 4 + 2] } // Compute X scale factor and normalize first row. scale[0] = v3Length(row[0]) - row[0] = v3Normalize(row[0], scale[0]) + v3NormalizeInPlace(row[0], scale[0]) // Compute XY shear factor and make 2nd row orthogonal to 1st. skew[0] = v3Dot(row[0], row[1]) - row[1] = v3Combine(row[1], row[0], 1.0, -skew[0]) + v3CombineInPlace(row[1], row[0], 1.0, -skew[0]) // Now, compute Y scale and normalize 2nd row. - scale[1] = v3Length(row[1]) - row[1] = v3Normalize(row[1], scale[1]) - skew[0] /= scale[1] + val scaleY = v3Length(row[1]) + scale[1] = scaleY + v3NormalizeInPlace(row[1], scaleY) + skew[0] /= scaleY // Compute XZ and YZ shears, orthogonalize 3rd row skew[1] = v3Dot(row[0], row[2]) - row[2] = v3Combine(row[2], row[0], 1.0, -skew[1]) + v3CombineInPlace(row[2], row[0], 1.0, -skew[1]) skew[2] = v3Dot(row[1], row[2]) - row[2] = v3Combine(row[2], row[1], 1.0, -skew[2]) + v3CombineInPlace(row[2], row[1], 1.0, -skew[2]) // Next, get Z scale and normalize 3rd row. - scale[2] = v3Length(row[2]) - row[2] = v3Normalize(row[2], scale[2]) - skew[1] /= scale[2] - skew[2] /= scale[2] + val scaleZ = v3Length(row[2]) + scale[2] = scaleZ + v3NormalizeInPlace(row[2], scaleZ) + skew[1] /= scaleZ + skew[2] /= scaleZ // At this point, the matrix (in rows) is orthonormal. // Check for a coordinate system flip. If the determinant @@ -340,6 +344,13 @@ public object MatrixMathHelper { return doubleArrayOf(vector[0] * im, vector[1] * im, vector[2] * im) } + private fun v3NormalizeInPlace(vector: DoubleArray, norm: Double) { + val inverseMagnitude = 1.0 / norm + vector[0] *= inverseMagnitude + vector[1] *= inverseMagnitude + vector[2] *= inverseMagnitude + } + /** * The dot product of a and b, two 3-element vectors. From: * https://code.google.com/p/webgl-mjs/source/browse/mjs.js @@ -367,6 +378,17 @@ public object MatrixMathHelper { ) } + private fun v3CombineInPlace( + a: DoubleArray, + b: DoubleArray, + aScale: Double, + bScale: Double, + ) { + a[0] = aScale * a[0] + bScale * b[0] + a[1] = aScale * a[1] + bScale * b[1] + a[2] = aScale * a[2] + bScale * b[2] + } + /** * From: * http://www.opensource.apple.com/source/WebCore/WebCore-514/platform/graphics/transforms/TransformationMatrix.cpp diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/MatrixMathHelperTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/MatrixMathHelperTest.kt index 969409489e9..1e4e5852cc4 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/MatrixMathHelperTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/MatrixMathHelperTest.kt @@ -83,6 +83,39 @@ class MatrixMathHelperTest { verifyZRotatedMatrix(42.55555555555, 0.0, 0.0, 42.556) } + @Test + fun testDecomposingTranslationAndScale() { + val ctx = MatrixDecompositionContext() + + MatrixMathHelper.decomposeMatrix( + doubleArrayOf( + 2.0, + 0.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 0.0, + 0.0, + 0.0, + 4.0, + 0.0, + 5.0, + 6.0, + 7.0, + 1.0, + ), + ctx, + ) + + assertThat(ctx.perspective).containsExactly(0.0, 0.0, 0.0, 1.0) + assertThat(ctx.scale).containsExactly(2.0, 3.0, 4.0) + assertThat(ctx.skew).containsExactly(0.0, 0.0, 0.0) + assertThat(ctx.translation).containsExactly(5.0, 6.0, 7.0) + assertThat(ctx.rotationDegrees).containsExactly(0.0, 0.0, 0.0) + } + @Test fun testDecomposing4x4MatrixToProduceAccurateYaxisAngles() { val angles = doubleArrayOf(30.0, 45.0, 60.0, 75.0, 90.0)