Explorar o código

Add ragdoll scene to emscripten determinism test (#1203)

* Made EigenValueSymmetric cross platform deterministic with emscripten
WASM doesn't support enabling flushing denormalized floats to zero so we cannot use this.
Instead we will just disable the float exception since the rest of the code handles the case fine.
* Add unit test for EigenValueSymmetric
Jorrit Rouwe hai 1 ano
pai
achega
1cb59a566d

+ 18 - 15
.github/workflows/determinism_check.yml

@@ -38,10 +38,10 @@ jobs:
       run: ctest --output-on-failure --verbose
     - name: Test ConvexVsMesh
       working-directory: ${{github.workspace}}/Build/Linux_Distribution
-      run: ./PerformanceTest -q=LinearCast -t=2 -s=ConvexVsMesh -validate_hash=${CONVEX_VS_MESH_HASH}
+      run: ./PerformanceTest -q=LinearCast -t=max -s=ConvexVsMesh -validate_hash=${CONVEX_VS_MESH_HASH}
     - name: Test Ragdoll
       working-directory: ${{github.workspace}}/Build/Linux_Distribution
-      run: ./PerformanceTest -q=LinearCast -t=2 -s=Ragdoll -validate_hash=${RAGDOLL_HASH}
+      run: ./PerformanceTest -q=LinearCast -t=max -s=Ragdoll -validate_hash=${RAGDOLL_HASH}
 
   linux_gcc:
     runs-on: ubuntu-latest
@@ -60,10 +60,10 @@ jobs:
       run: ctest --output-on-failure --verbose
     - name: Test ConvexVsMesh
       working-directory: ${{github.workspace}}/Build/Linux_Distribution
-      run: ./PerformanceTest -q=LinearCast -t=2 -s=ConvexVsMesh -validate_hash=${CONVEX_VS_MESH_HASH}
+      run: ./PerformanceTest -q=LinearCast -t=max -s=ConvexVsMesh -validate_hash=${CONVEX_VS_MESH_HASH}
     - name: Test Ragdoll
       working-directory: ${{github.workspace}}/Build/Linux_Distribution
-      run: ./PerformanceTest -q=LinearCast -t=2 -s=Ragdoll -validate_hash=${RAGDOLL_HASH}
+      run: ./PerformanceTest -q=LinearCast -t=max -s=Ragdoll -validate_hash=${RAGDOLL_HASH}
 
   msvc_cl:
     runs-on: windows-latest
@@ -84,10 +84,10 @@ jobs:
       run: ./UnitTests.exe
     - name: Test ConvexVsMesh
       working-directory: ${{github.workspace}}/Build/VS2022_CL/Distribution
-      run: ./PerformanceTest -q=LinearCast -t=2 -s=ConvexVsMesh "-validate_hash=$env:CONVEX_VS_MESH_HASH"
+      run: ./PerformanceTest -q=LinearCast -t=max -s=ConvexVsMesh "-validate_hash=$env:CONVEX_VS_MESH_HASH"
     - name: Test Ragdoll
       working-directory: ${{github.workspace}}/Build/VS2022_CL/Distribution
-      run: ./PerformanceTest -q=LinearCast -t=2 -s=Ragdoll "-validate_hash=$env:RAGDOLL_HASH"
+      run: ./PerformanceTest -q=LinearCast -t=max -s=Ragdoll "-validate_hash=$env:RAGDOLL_HASH"
 
   msvc_cl_32:
     runs-on: windows-latest
@@ -108,10 +108,10 @@ jobs:
       run: ./UnitTests.exe
     - name: Test ConvexVsMesh
       working-directory: ${{github.workspace}}/Build/VS2022_CL_32BIT/Distribution
-      run: ./PerformanceTest -q=LinearCast -t=2 -s=ConvexVsMesh "-validate_hash=$env:CONVEX_VS_MESH_HASH"
+      run: ./PerformanceTest -q=LinearCast -t=max -s=ConvexVsMesh "-validate_hash=$env:CONVEX_VS_MESH_HASH"
     - name: Test Ragdoll
       working-directory: ${{github.workspace}}/Build/VS2022_CL_32BIT/Distribution
-      run: ./PerformanceTest -q=LinearCast -t=2 -s=Ragdoll "-validate_hash=$env:RAGDOLL_HASH"
+      run: ./PerformanceTest -q=LinearCast -t=max -s=Ragdoll "-validate_hash=$env:RAGDOLL_HASH"
 
   macos:
     runs-on: macos-latest
@@ -130,10 +130,10 @@ jobs:
       run: ctest --output-on-failure --verbose
     - name: Test ConvexVsMesh
       working-directory: ${{github.workspace}}/Build/Linux_Distribution
-      run: ./PerformanceTest -q=LinearCast -t=2 -s=ConvexVsMesh -validate_hash=${CONVEX_VS_MESH_HASH}
+      run: ./PerformanceTest -q=LinearCast -t=max -s=ConvexVsMesh -validate_hash=${CONVEX_VS_MESH_HASH}
     - name: Test Ragdoll
       working-directory: ${{github.workspace}}/Build/Linux_Distribution
-      run: ./PerformanceTest -q=LinearCast -t=2 -s=Ragdoll -validate_hash=${RAGDOLL_HASH}
+      run: ./PerformanceTest -q=LinearCast -t=max -s=Ragdoll -validate_hash=${RAGDOLL_HASH}
 
   arm_clang:
     runs-on: ubuntu-latest
@@ -155,10 +155,10 @@ jobs:
       run: qemu-aarch64 -L /usr/aarch64-linux-gnu/ ./UnitTests
     - name: Test ConvexVsMesh
       working-directory: ${{github.workspace}}/Build/Linux_Distribution
-      run: qemu-aarch64 -L /usr/aarch64-linux-gnu/ ./PerformanceTest -q=LinearCast -t=2 -s=ConvexVsMesh -validate_hash=${CONVEX_VS_MESH_HASH}
+      run: qemu-aarch64 -L /usr/aarch64-linux-gnu/ ./PerformanceTest -q=LinearCast -t=max -s=ConvexVsMesh -validate_hash=${CONVEX_VS_MESH_HASH}
     - name: Test Ragdoll
       working-directory: ${{github.workspace}}/Build/Linux_Distribution
-      run: qemu-aarch64 -L /usr/aarch64-linux-gnu/ ./PerformanceTest -q=LinearCast -t=2 -s=Ragdoll -validate_hash=${RAGDOLL_HASH}
+      run: qemu-aarch64 -L /usr/aarch64-linux-gnu/ ./PerformanceTest -q=LinearCast -t=max -s=Ragdoll -validate_hash=${RAGDOLL_HASH}
 
   arm_gcc:
     runs-on: ubuntu-latest
@@ -180,10 +180,10 @@ jobs:
       run: qemu-aarch64 -L /usr/aarch64-linux-gnu/ ./UnitTests
     - name: Test ConvexVsMesh
       working-directory: ${{github.workspace}}/Build/Linux_Distribution
-      run: qemu-aarch64 -L /usr/aarch64-linux-gnu/ ./PerformanceTest -q=LinearCast -t=2 -s=ConvexVsMesh -validate_hash=${CONVEX_VS_MESH_HASH}
+      run: qemu-aarch64 -L /usr/aarch64-linux-gnu/ ./PerformanceTest -q=LinearCast -t=max -s=ConvexVsMesh -validate_hash=${CONVEX_VS_MESH_HASH}
     - name: Test Ragdoll
       working-directory: ${{github.workspace}}/Build/Linux_Distribution
-      run: qemu-aarch64 -L /usr/aarch64-linux-gnu/ ./PerformanceTest -q=LinearCast -t=2 -s=Ragdoll -validate_hash=${RAGDOLL_HASH}
+      run: qemu-aarch64 -L /usr/aarch64-linux-gnu/ ./PerformanceTest -q=LinearCast -t=max -s=Ragdoll -validate_hash=${RAGDOLL_HASH}
 
   emscripten:
     runs-on: ubuntu-latest
@@ -213,4 +213,7 @@ jobs:
       run: node UnitTests.js
     - name: Test ConvexVsMesh
       working-directory: ${{github.workspace}}/Build/WASM_Distribution
-      run: node PerformanceTest.js -q=LinearCast -t=2 -s=ConvexVsMesh -validate_hash=${CONVEX_VS_MESH_HASH}
+      run: node PerformanceTest.js -q=LinearCast -t=max -s=ConvexVsMesh -validate_hash=${CONVEX_VS_MESH_HASH}
+    - name: Test Ragdoll
+      working-directory: ${{github.workspace}}/Build/WASM_Distribution
+      run: node PerformanceTest.js -q=LinearCast -t=max -s=Ragdoll -validate_hash=${RAGDOLL_HASH}

+ 6 - 0
Build/CMakeLists.txt

@@ -323,6 +323,12 @@ if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
 			if (MSVC)
 				target_link_options(PerformanceTest PUBLIC "/SUBSYSTEM:CONSOLE")
 			endif()
+			if (EMSCRIPTEN)
+				# Embed the assets for the RagdollScene
+				target_link_options(PerformanceTest PUBLIC "SHELL:--preload-file ${PHYSICS_REPO_ROOT}/Assets/Human.tof@/Assets/Human.tof")
+				target_link_options(PerformanceTest PUBLIC "SHELL:--preload-file ${PHYSICS_REPO_ROOT}/Assets/Human/dead_pose1.tof@/Assets/Human/dead_pose1.tof")
+				target_link_options(PerformanceTest PUBLIC "SHELL:--preload-file ${PHYSICS_REPO_ROOT}/Assets/terrain2.bof@/Assets/terrain2.bof")
+			endif()
 			set_property(TARGET PerformanceTest PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${PHYSICS_REPO_ROOT}")
 
 			# Copy the assets folder

+ 1 - 0
Docs/ReleaseNotes.md

@@ -45,6 +45,7 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
 * QuadTree / FixedSizeFreeList: Reorder variable layout to reduce false sharing & thread syncs to reduce simulation time by approximately 5%.
 * Generate a CMake config file when the project is installed. Allows for other projects to import Jolt using the find_package() functionality.
 * Added USE_WASM_SIMD cmake option. This will enable SIMD on the emscripten WASM build.
+* Emscripten WASM build can now be compiled cross platform deterministic and deliver the same results as Windows, Linux etc.
 * Added Shape::MakeScaleValid function. This function will take a scale vector and check it against the scaling rules for the shape. If it is not valid, it will return a scale that is close to the provided scale which is valid.
 
 ### Bug fixes

+ 1 - 1
Jolt/Jolt.cmake

@@ -674,5 +674,5 @@ endif()
 if (EMSCRIPTEN)
 	# We need more than the default 64KB stack and 16MB memory
 	# Also disable warning: running limited binaryen optimizations because DWARF info requested (or indirectly required)
-	target_link_options(Jolt PUBLIC -sSTACK_SIZE=1048576 -sINITIAL_MEMORY=67108864 -Wno-limited-postlink-optimizations)
+	target_link_options(Jolt PUBLIC -sSTACK_SIZE=1048576 -sINITIAL_MEMORY=134217728 -Wno-limited-postlink-optimizations)
 endif()

+ 41 - 39
Jolt/Math/EigenValueSymmetric.h

@@ -4,7 +4,7 @@
 
 #pragma once
 
-#include <Jolt/Core/FPFlushDenormals.h>
+#include <Jolt/Core/FPException.h>
 
 JPH_NAMESPACE_BEGIN
 
@@ -28,9 +28,9 @@ JPH_NAMESPACE_BEGIN
 template <class Vector, class Matrix>
 bool EigenValueSymmetric(const Matrix &inMatrix, Matrix &outEigVec, Vector &outEigVal)
 {
-	// This algorithm works with very small numbers and can trigger invalid float exceptions when not flushing denormals
-	FPFlushDenormals flush_denormals;
-	(void)flush_denormals;
+	// This algorithm can generate infinite values, see comment below
+	FPExceptionDisableInvalid disable_invalid;
+	(void)disable_invalid;
 
 	// Maximum number of sweeps to make
 	const int cMaxSweeps = 50;
@@ -70,9 +70,10 @@ bool EigenValueSymmetric(const Matrix &inMatrix, Matrix &outEigVec, Vector &outE
 		for (uint ip = 0; ip < n - 1; ++ip)
 			for (uint iq = ip + 1; iq < n; ++iq)
 				sm += abs(a(ip, iq));
+		float avg_sm = sm / Square(n);
 
 		// Normal return, convergence to machine underflow
-		if (sm == 0.0f)
+		if (avg_sm < FLT_MIN) // Original code: sm == 0.0f, when the average is denormal, we also consider it machine underflow
 		{
 			// Sanity checks
 			#ifdef JPH_ENABLE_ASSERTS
@@ -93,68 +94,69 @@ bool EigenValueSymmetric(const Matrix &inMatrix, Matrix &outEigVec, Vector &outE
 		}
 
 		// On the first three sweeps use a fraction of the sum of the off diagonal elements as threshold
-		float tresh = sweep < 4? 0.2f * sm / Square(n) : 0.0f;
+		// Note that we pick a minimum threshold of FLT_MIN because dividing by a denormalized number is likely to result in infinity.
+		float tresh = sweep < 4? 0.2f * avg_sm : FLT_MIN; // Original code: 0.0f instead of FLT_MIN
 
 		for (uint ip = 0; ip < n - 1; ++ip)
 			for (uint iq = ip + 1; iq < n; ++iq)
 			{
-				float g = 100.0f * abs(a(ip, iq));
+				float &a_pq = a(ip, iq);
+				float &eigval_p = outEigVal[ip];
+				float &eigval_q = outEigVal[iq];
+
+				float abs_a_pq = abs(a_pq);
+				float g = 100.0f * abs_a_pq;
 
 				// After four sweeps, skip the rotation if the off-diagonal element is small
 				if (sweep > 4
-					&& abs(outEigVal[ip]) + g == abs(outEigVal[ip])
-					&& abs(outEigVal[iq]) + g == abs(outEigVal[iq]))
+					&& abs(eigval_p) + g == abs(eigval_p)
+					&& abs(eigval_q) + g == abs(eigval_q))
 				{
-					a(ip, iq) = 0.0f;
+					a_pq = 0.0f;
 				}
-				else if (abs(a(ip, iq)) > tresh)
+				else if (abs_a_pq > tresh)
 				{
-					float h = outEigVal[iq] - outEigVal[ip];
+					float h = eigval_q - eigval_p;
+					float abs_h = abs(h);
 
 					float t;
-					if (abs(h) + g == abs(h))
+					if (abs_h + g == abs_h)
 					{
-						t = a(ip, iq) / h;
+						t = a_pq / h;
 					}
 					else
 					{
-						float theta = 0.5f * h / a(ip, iq); // Warning: Can become inf if a(ip, iq) too small
-						t = 1.0f / (abs(theta) + sqrt(1.0f + theta * theta)); // Warning: Squaring large value can make it inf
+						float theta = 0.5f * h / a_pq; // Warning: Can become infinite if a(ip, iq) is very small which may trigger an invalid float exception
+						t = 1.0f / (abs(theta) + sqrt(1.0f + theta * theta)); // If theta becomes inf, t will be 0 so the infinite is not a problem for the algorithm
 						if (theta < 0.0f) t = -t;
 					}
 
 					float c = 1.0f / sqrt(1.0f + t * t);
 					float s = t * c;
 					float tau = s / (1.0f + c);
-					h = t * a(ip, iq);
+					h = t * a_pq;
 
-					a(ip, iq) = 0.0f;
+					a_pq = 0.0f;
 
-					// !Modification from Numerical Recipes!
-					// h can become infinite due to numerical overflow, this only happens when a(ip, iq) is very small
-					// so we can safely set a(ip, iq) to zero and skip the rotation, see lines marked with 'Warning' above.
-					if (!isnan(h))
-					{
-						z[ip] -= h;
-						z[iq] += h;
+					z[ip] -= h;
+					z[iq] += h;
 
-						outEigVal[ip] -= h;
-						outEigVal[iq] += h;
+					eigval_p -= h;
+					eigval_q += h;
 
-						#define JPH_EVS_ROTATE(a, i, j, k, l)		\
-							g = a(i, j),							\
-							h = a(k, l),							\
-							a(i, j) = g - s * (h + g * tau),		\
-							a(k, l) = h + s * (g - h * tau)
+					#define JPH_EVS_ROTATE(a, i, j, k, l)		\
+						g = a(i, j),							\
+						h = a(k, l),							\
+						a(i, j) = g - s * (h + g * tau),		\
+						a(k, l) = h + s * (g - h * tau)
 
-						uint j;
-						for (j = 0; j < ip; ++j)		JPH_EVS_ROTATE(a, j, ip, j, iq);
-						for (j = ip + 1; j < iq; ++j)	JPH_EVS_ROTATE(a, ip, j, j, iq);
-						for (j = iq + 1; j < n; ++j)	JPH_EVS_ROTATE(a, ip, j, iq, j);
-						for (j = 0; j < n; ++j)			JPH_EVS_ROTATE(outEigVec, j, ip, j, iq);
+					uint j;
+					for (j = 0; j < ip; ++j)		JPH_EVS_ROTATE(a, j, ip, j, iq);
+					for (j = ip + 1; j < iq; ++j)	JPH_EVS_ROTATE(a, ip, j, j, iq);
+					for (j = iq + 1; j < n; ++j)	JPH_EVS_ROTATE(a, ip, j, iq, j);
+					for (j = 0; j < n; ++j)			JPH_EVS_ROTATE(outEigVec, j, ip, j, iq);
 
-						#undef JPH_EVS_ROTATE
-					}
+					#undef JPH_EVS_ROTATE
 				}
 			}
 

+ 57 - 0
UnitTests/Math/EigenValueSymmetricTests.cpp

@@ -0,0 +1,57 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include "UnitTestFramework.h"
+#include <Jolt/Math/EigenValueSymmetric.h>
+#include <Jolt/Math/Matrix.h>
+
+TEST_SUITE("EigenValueSymmetricTests")
+{
+	TEST_CASE("TestEigenValueSymmetric")
+	{
+		default_random_engine random;
+		uniform_real_distribution<float> angle_distribution(0, 2.0f * JPH_PI);
+		uniform_real_distribution<float> scale_distribution(0.1f, 10.0f);
+
+		for (int i = 0; i < 1000; ++i)
+		{
+			// Random scale vector
+			Vec3 scale(scale_distribution(random), scale_distribution(random), scale_distribution(random));
+
+			// Random rotation matrix
+			Mat44 rotation = Mat44::sRotation(Vec3::sRandom(random), angle_distribution(random));
+
+			// Construct a symmetric tensor from this rotation and scale
+			Mat44 tensor4 = rotation.Multiply3x3(Mat44::sScale(scale)).Multiply3x3RightTransposed(rotation);
+
+			// Get the eigenvalues and eigenvectors
+			Matrix<3, 3> tensor;
+			Matrix<3, 3> eigen_vec = Matrix<3, 3>::sIdentity();
+			Vector<3> eigen_val;
+			tensor.CopyPart(tensor4, 0, 0, 3, 3, 0, 0);
+			CHECK(EigenValueSymmetric(tensor, eigen_vec, eigen_val));
+
+			for (int c = 0; c < 3; ++c)
+			{
+				// Check that we found a valid eigenvalue
+				bool found = false;
+				for (int c2 = 0; c2 < 3; ++c2)
+					if (abs(scale[c2] - eigen_val[c]) < 1.0e-5f)
+					{
+						found = true;
+						break;
+					}
+				CHECK(found);
+
+				// Check if the eigenvector is normalized
+				CHECK(eigen_vec.GetColumn(c).IsNormalized());
+
+				// Check if matrix * eigen_vector = eigen_value * eigen_vector
+				Vector mat_eigvec = tensor * eigen_vec.GetColumn(c);
+				Vector eigval_eigvec = eigen_val[c] * eigen_vec.GetColumn(c);
+				CHECK(mat_eigvec.IsClose(eigval_eigvec, Square(1.0e-5f)));
+			}
+		}
+	}
+}

+ 1 - 0
UnitTests/UnitTests.cmake

@@ -25,6 +25,7 @@ set(UNIT_TESTS_SRC_FILES
 	${UNIT_TESTS_ROOT}/LoggingContactListener.h
 	${UNIT_TESTS_ROOT}/Math/DMat44Tests.cpp
 	${UNIT_TESTS_ROOT}/Math/DVec3Tests.cpp
+	${UNIT_TESTS_ROOT}/Math/EigenValueSymmetricTests.cpp
 	${UNIT_TESTS_ROOT}/Math/HalfFloatTests.cpp
 	${UNIT_TESTS_ROOT}/Math/Mat44Tests.cpp
 	${UNIT_TESTS_ROOT}/Math/MathTests.cpp