Browse Source

Added performance test app (#25)

* Added application to measure performance increase vs amount of threads in the job system
* Do step validation always single threaded to ensure that multi threaded simulation matches single threaded simulation
* Added overload for Vec3 which doesn't write W (causes issues in the determinism check when JPH_FLOATING_POINT_EXCEPTIONS_ENABLED not defined)
* Fixed writing a pointer to binary stream for the collision groups
* Fixed writing W of bounding boxes
jrouwe 3 years ago
parent
commit
23c1b9d902

+ 12 - 2
Build/CMakeLists.txt

@@ -58,8 +58,8 @@ elseif ("${CMAKE_SYSTEM_NAME}" STREQUAL "Linux")
 
 
 	# Set compiler flags for various configurations
 	# Set compiler flags for various configurations
 	set(CMAKE_CXX_FLAGS_DEBUG "-g -D_DEBUG -DJPH_PROFILE_ENABLED -DJPH_DEBUG_RENDERER") # Clang reorders variables so that div by zero occurs if we enable exception checking
 	set(CMAKE_CXX_FLAGS_DEBUG "-g -D_DEBUG -DJPH_PROFILE_ENABLED -DJPH_DEBUG_RENDERER") # Clang reorders variables so that div by zero occurs if we enable exception checking
-	set(CMAKE_CXX_FLAGS_RELEASE "-DNDEBUG -DJPH_PROFILE_ENABLED -DJPH_DEBUG_RENDERER")
-	set(CMAKE_CXX_FLAGS_DISTRIBUTION "-DNDEBUG")
+	set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG -DJPH_PROFILE_ENABLED -DJPH_DEBUG_RENDERER")
+	set(CMAKE_CXX_FLAGS_DISTRIBUTION "-O3 -DNDEBUG")
 	set(CMAKE_CXX_FLAGS_RELEASEASAN "-DNDEBUG -DJPH_PROFILE_ENABLED -DJPH_DISABLE_TEMP_ALLOCATOR -DJPH_DEBUG_RENDERER -fsanitize=address")
 	set(CMAKE_CXX_FLAGS_RELEASEASAN "-DNDEBUG -DJPH_PROFILE_ENABLED -DJPH_DISABLE_TEMP_ALLOCATOR -DJPH_DEBUG_RENDERER -fsanitize=address")
 	set(CMAKE_CXX_FLAGS_RELEASEUBSAN "-DNDEBUG -DJPH_PROFILE_ENABLED -DJPH_DEBUG_RENDERER -fsanitize=undefined,implicit-conversion")
 	set(CMAKE_CXX_FLAGS_RELEASEUBSAN "-DNDEBUG -DJPH_PROFILE_ENABLED -DJPH_DEBUG_RENDERER -fsanitize=undefined,implicit-conversion")
 	set(CMAKE_CXX_FLAGS_RELEASECOVERAGE "-DNDEBUG -fprofile-instr-generate -fcoverage-mapping")
 	set(CMAKE_CXX_FLAGS_RELEASECOVERAGE "-DNDEBUG -fprofile-instr-generate -fcoverage-mapping")
@@ -100,6 +100,16 @@ if ("${CMAKE_SYSTEM_NAME}" STREQUAL "Windows")
 	target_link_options(HelloWorld PUBLIC "/SUBSYSTEM:CONSOLE")
 	target_link_options(HelloWorld PUBLIC "/SUBSYSTEM:CONSOLE")
 endif()
 endif()
 
 
+# Performance Test application
+include(${PHYSICS_REPO_ROOT}/PerformanceTest/PerformanceTest.cmake)
+add_executable(PerformanceTest ${PERFORMANCE_TEST_SRC_FILES})
+target_include_directories(PerformanceTest PUBLIC ${JOLT_PHYSICS_ROOT} ${PERFORMANCE_TEST_ROOT})
+target_link_libraries (PerformanceTest LINK_PUBLIC Jolt)
+if ("${CMAKE_SYSTEM_NAME}" STREQUAL "Windows")
+	target_link_options(PerformanceTest PUBLIC "/SUBSYSTEM:CONSOLE")
+endif()
+set_property(TARGET PerformanceTest PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${PHYSICS_REPO_ROOT}")
+
 if ("${CMAKE_SYSTEM_NAME}" STREQUAL "Windows")
 if ("${CMAKE_SYSTEM_NAME}" STREQUAL "Windows")
 	# Windows only targets
 	# Windows only targets
 	include(${PHYSICS_REPO_ROOT}/TestFramework/TestFramework.cmake)
 	include(${PHYSICS_REPO_ROOT}/TestFramework/TestFramework.cmake)

+ 7 - 0
Jolt/Core/StreamIn.h

@@ -57,6 +57,13 @@ public:
 		else
 		else
 			outString.clear();
 			outString.clear();
 	}
 	}
+
+	/// Read a Vec3 (don't read W)
+	void				Read(Vec3 &outVec)
+	{
+		ReadBytes(&outVec, 3 * sizeof(float));
+		outVec = Vec3::sFixW(outVec.mValue);
+	}
 };
 };
 
 
 } // JPH
 } // JPH

+ 6 - 0
Jolt/Core/StreamOut.h

@@ -44,6 +44,12 @@ public:
 		if (!IsFailed())
 		if (!IsFailed())
 			WriteBytes(inString.data(), len);
 			WriteBytes(inString.data(), len);
 	}
 	}
+
+	/// Write a Vec3 (don't write W)
+	void				Write(const Vec3 &inVec)
+	{
+		WriteBytes(&inVec, 3 * sizeof(float));
+	}
 };
 };
 
 
 } // JPH
 } // JPH

+ 2 - 2
Jolt/Physics/Body/Body.cpp

@@ -299,7 +299,7 @@ void Body::SaveState(StateRecorder &inStream) const
 	inStream.Write(mRotation);
 	inStream.Write(mRotation);
 	inStream.Write(mFriction);
 	inStream.Write(mFriction);
 	inStream.Write(mRestitution);
 	inStream.Write(mRestitution);
-	inStream.Write(mCollisionGroup);
+	mCollisionGroup.SaveBinaryState(inStream);
 	inStream.Write(mMotionType);
 	inStream.Write(mMotionType);
 
 
 	if (mMotionProperties != nullptr)
 	if (mMotionProperties != nullptr)
@@ -312,7 +312,7 @@ void Body::RestoreState(StateRecorder &inStream)
 	inStream.Read(mRotation);
 	inStream.Read(mRotation);
 	inStream.Read(mFriction);
 	inStream.Read(mFriction);
 	inStream.Read(mRestitution);
 	inStream.Read(mRestitution);
-	inStream.Read(mCollisionGroup);
+	mCollisionGroup.RestoreBinaryState(inStream);
 	inStream.Read(mMotionType);
 	inStream.Read(mMotionType);
 
 
 	if (mMotionProperties != nullptr)
 	if (mMotionProperties != nullptr)

+ 4 - 2
Jolt/Physics/Collision/Shape/CompoundShape.cpp

@@ -276,7 +276,8 @@ void CompoundShape::SaveBinaryState(StreamOut &inStream) const
 	Shape::SaveBinaryState(inStream);
 	Shape::SaveBinaryState(inStream);
 
 
 	inStream.Write(mCenterOfMass);
 	inStream.Write(mCenterOfMass);
-	inStream.Write(mLocalBounds);
+	inStream.Write(mLocalBounds.mMin);
+	inStream.Write(mLocalBounds.mMax);
 	inStream.Write(mInnerRadius);
 	inStream.Write(mInnerRadius);
 
 
 	// Write sub shapes
 	// Write sub shapes
@@ -297,7 +298,8 @@ void CompoundShape::RestoreBinaryState(StreamIn &inStream)
 	Shape::RestoreBinaryState(inStream);
 	Shape::RestoreBinaryState(inStream);
 
 
 	inStream.Read(mCenterOfMass);
 	inStream.Read(mCenterOfMass);
-	inStream.Read(mLocalBounds);
+	inStream.Read(mLocalBounds.mMin);
+	inStream.Read(mLocalBounds.mMax);
 	inStream.Read(mInnerRadius);
 	inStream.Read(mInnerRadius);
 
 
 	// Read sub shapes
 	// Read sub shapes

+ 4 - 2
Jolt/Physics/Collision/Shape/ConvexHullShape.cpp

@@ -1128,7 +1128,8 @@ void ConvexHullShape::SaveBinaryState(StreamOut &inStream) const
 
 
 	inStream.Write(mCenterOfMass);
 	inStream.Write(mCenterOfMass);
 	inStream.Write(mInertia);
 	inStream.Write(mInertia);
-	inStream.Write(mLocalBounds);
+	inStream.Write(mLocalBounds.mMin);
+	inStream.Write(mLocalBounds.mMax);
 	inStream.Write(mPoints);
 	inStream.Write(mPoints);
 	inStream.Write(mFaces);
 	inStream.Write(mFaces);
 	inStream.Write(mPlanes);
 	inStream.Write(mPlanes);
@@ -1144,7 +1145,8 @@ void ConvexHullShape::RestoreBinaryState(StreamIn &inStream)
 
 
 	inStream.Read(mCenterOfMass);
 	inStream.Read(mCenterOfMass);
 	inStream.Read(mInertia);
 	inStream.Read(mInertia);
-	inStream.Read(mLocalBounds);
+	inStream.Read(mLocalBounds.mMin);
+	inStream.Read(mLocalBounds.mMax);
 	inStream.Read(mPoints);
 	inStream.Read(mPoints);
 	inStream.Read(mFaces);
 	inStream.Read(mFaces);
 	inStream.Read(mPlanes);
 	inStream.Read(mPlanes);

+ 11 - 0
PerformanceTest/PerformanceTest.cmake

@@ -0,0 +1,11 @@
+# Root
+set(PERFORMANCE_TEST_ROOT ${PHYSICS_REPO_ROOT}/PerformanceTest)
+
+# Source files
+set(PERFORMANCE_TEST_SRC_FILES
+	${PERFORMANCE_TEST_ROOT}/PerformanceTest.cpp
+	${PERFORMANCE_TEST_ROOT}/PerformanceTest.cmake
+)
+
+# Group source files
+source_group(TREE ${PERFORMANCE_TEST_ROOT} FILES ${PERFORMANCE_TEST_SRC_FILES})

+ 259 - 0
PerformanceTest/PerformanceTest.cpp

@@ -0,0 +1,259 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+// Jolt includes
+#include <Jolt.h>
+#include <RegisterTypes.h>
+#include <Core/TempAllocator.h>
+#include <Core/JobSystemThreadPool.h>
+#include <Physics/PhysicsSettings.h>
+#include <Physics/PhysicsSystem.h>
+#include <Physics/Ragdoll/Ragdoll.h>
+#include <Physics/PhysicsScene.h>
+#include <Physics/Collision/CastResult.h>
+#include <Physics/Collision/RayCast.h>
+
+// STL includes
+#include <iostream>
+#include <thread>
+#include <chrono>
+
+using namespace JPH;
+using namespace std;
+
+namespace Layers
+{
+	static constexpr uint8 NON_MOVING = 0;
+	static constexpr uint8 MOVING = 1;
+	static constexpr uint8 NUM_LAYERS = 2;
+};
+
+bool MyObjectCanCollide(ObjectLayer inObject1, ObjectLayer inObject2)
+{
+	switch (inObject1)
+	{
+	case Layers::NON_MOVING:
+		return inObject2 == Layers::MOVING; // Non moving only collides with moving
+	case Layers::MOVING:
+		return true; // Moving collides with everything
+	default:
+		JPH_ASSERT(false);
+		return false;
+	}
+};
+
+namespace BroadPhaseLayers
+{
+	static constexpr BroadPhaseLayer NON_MOVING(0);
+	static constexpr BroadPhaseLayer MOVING(1);
+};
+
+bool MyBroadPhaseCanCollide(ObjectLayer inLayer1, BroadPhaseLayer inLayer2)
+{
+	switch (inLayer1)
+	{
+	case Layers::NON_MOVING:
+		return inLayer2 == BroadPhaseLayers::MOVING;
+	case Layers::MOVING:
+		return true;	
+	default:
+		JPH_ASSERT(false);
+		return false;
+	}
+}
+
+// Test configuration
+const float cHorizontalSeparation = 4.0f;
+const float cVerticalSeparation = 0.6f;
+#ifdef _DEBUG
+	const int cPileSize = 5;
+	const int cNumRows = 2;
+	const int cNumCols = 2;
+#else
+	const int cPileSize = 10;
+	const int cNumRows = 4;
+	const int cNumCols = 4;
+#endif
+const float cDeltaTime = 1.0f / 60.0f;
+
+// Program entry point
+int main(int argc, char** argv)
+{
+	// Register all Jolt physics types
+	RegisterTypes();
+
+	// Create temp allocator
+	TempAllocatorImpl temp_allocator(10 * 1024 * 1024);
+
+	// Load ragdoll
+	Ref<RagdollSettings> ragdoll_settings;
+	if (!ObjectStreamIn::sReadObject("Assets/Human.tof", ragdoll_settings))
+	{
+		cerr << "Unable to load ragdoll" << endl;
+		return 1;
+	}
+	for (BodyCreationSettings &body : ragdoll_settings->mParts)
+		body.mObjectLayer = Layers::MOVING;
+
+	// Init ragdoll
+	ragdoll_settings->GetSkeleton()->CalculateParentJointIndices();
+	ragdoll_settings->Stabilize();
+	ragdoll_settings->CalculateBodyIndexToConstraintIndex();
+	ragdoll_settings->CalculateConstraintIndexToBodyIdxPair();
+
+	// Load animation
+	Ref<SkeletalAnimation> animation;
+	if (!ObjectStreamIn::sReadObject("Assets/Human/dead_pose1.tof", animation))
+	{
+		cerr << "Unable to load animation" << endl;
+		return 1;
+	}
+
+	// Sample pose
+	SkeletonPose pose;
+	pose.SetSkeleton(ragdoll_settings->GetSkeleton());
+	animation->Sample(0.0f, pose);
+
+	// Read the scene
+	Ref<PhysicsScene> scene;
+	if (!ObjectStreamIn::sReadObject("Assets/terrain2.bof", scene))
+	{
+		cerr << "Unable to load terrain" << endl;
+		return 1;
+	}
+	for (BodyCreationSettings &body : scene->GetBodies())
+		body.mObjectLayer = Layers::NON_MOVING;
+	scene->FixInvalidScales();
+
+	// Create mapping table from object layer to broadphase layer
+	ObjectToBroadPhaseLayer object_to_broadphase;
+	object_to_broadphase.resize(Layers::NUM_LAYERS);
+	object_to_broadphase[Layers::NON_MOVING] = BroadPhaseLayers::NON_MOVING;
+	object_to_broadphase[Layers::MOVING] = BroadPhaseLayers::MOVING;
+
+	// Start profiling this thread
+	JPH_PROFILE_THREAD_START("Main");
+
+	// Trace header
+	cout << "Motion Quality, Thread Count, Time (s), Iterations, Hash" << endl;
+
+	// Iterate motion qualities
+	for (uint mq = 0; mq < 2; ++mq)
+	{
+		// Determine motion quality
+		EMotionQuality motion_quality = mq == 0? EMotionQuality::Discrete : EMotionQuality::LinearCast;
+		string motion_quality_str = mq == 0? "Discrete" : "LinearCast";
+
+		// Set motion quality on ragdoll
+		for (BodyCreationSettings &body : ragdoll_settings->mParts)
+			body.mMotionQuality = motion_quality;
+
+		// Test thread permutations
+		for (uint num_threads = 0; num_threads < thread::hardware_concurrency(); ++num_threads)
+		{
+			// Create job system with desired number of threads
+			JobSystemThreadPool job_system(cMaxPhysicsJobs, cMaxPhysicsBarriers, num_threads);
+
+			// Create physics system
+			PhysicsSystem physics_system;
+			physics_system.Init(10240, 0, 65536, 10240, object_to_broadphase, MyBroadPhaseCanCollide, MyObjectCanCollide);
+
+			// Add background geometry
+			scene->CreateBodies(&physics_system);
+
+			// Create ragdoll piles
+			vector<Ref<Ragdoll>> ragdolls;
+			mt19937 random;
+			uniform_real_distribution<float> angle(0.0f, JPH_PI);
+			CollisionGroup::GroupID group_id = 1;
+			for (int row = 0; row < cNumRows; ++row)
+				for (int col = 0; col < cNumCols; ++col)
+				{
+					// Determine start location of ray
+					Vec3 start = Vec3(cHorizontalSeparation * (col - (cNumCols - 1) / 2.0f), 100, cHorizontalSeparation * (row - (cNumRows - 1) / 2.0f));
+
+					// Cast ray down to terrain
+					RayCastResult hit;
+					Vec3 ray_direction(0, -200, 0);
+					RayCast ray { start, ray_direction };
+					if (physics_system.GetNarrowPhaseQuery().CastRay(ray, hit, SpecifiedBroadPhaseLayerFilter(BroadPhaseLayers::NON_MOVING), SpecifiedObjectLayerFilter(Layers::NON_MOVING)))
+						start = start + hit.mFraction * ray_direction;
+
+					for (int i = 0; i < cPileSize; ++i)
+					{
+						// Create ragdoll
+						Ref<Ragdoll> ragdoll = ragdoll_settings->CreateRagdoll(group_id++, nullptr, &physics_system);
+	
+						// Override root
+						SkeletonPose pose_copy = pose;
+						SkeletonPose::JointState &root = pose_copy.GetJoint(0);
+						root.mTranslation = start + Vec3(0, cVerticalSeparation * (i + 1), 0);
+						root.mRotation = Quat::sRotation(Vec3::sAxisY(), angle(random)) * root.mRotation;
+						pose_copy.CalculateJointMatrices();
+
+						// Drive to pose
+						ragdoll->SetPose(pose_copy);
+						ragdoll->DriveToPoseUsingMotors(pose_copy);
+						ragdoll->AddToPhysicsSystem(EActivation::Activate);
+
+						// Keep reference
+						ragdolls.push_back(ragdoll);
+					}
+				}
+
+			// Calculate total amount of ragdoll bodies
+			uint num_bodies = uint(ragdolls.size() * ragdolls[0]->GetBodyCount());
+
+			chrono::nanoseconds total_duration(0);
+			uint iterations = 0;
+
+			// Step the world until half of the bodies are sleeping
+			while (physics_system.GetNumActiveBodies() > num_bodies / 2)
+			{
+				JPH_PROFILE_NEXTFRAME();
+
+				// Start measuring
+				chrono::high_resolution_clock::time_point clock_start = chrono::high_resolution_clock::now();
+
+				// Do a physics step
+				physics_system.Update(cDeltaTime, 1, 1, &temp_allocator, &job_system);
+
+				// Stop measuring
+				chrono::high_resolution_clock::time_point clock_end = chrono::high_resolution_clock::now();
+				total_duration += chrono::duration_cast<chrono::nanoseconds>(clock_end - clock_start);
+
+				// Increment step count
+				++iterations;
+
+				// Dump profile information every 100 iterations
+				if (iterations % 100 == 0)
+				{
+					JPH_PROFILE_DUMP();
+				}
+			}
+
+			// Calculate hash of all positions and rotations of the bodies
+			size_t hash = 0;
+			BodyInterface &bi = physics_system.GetBodyInterfaceNoLock();
+			for (Ragdoll *ragdoll : ragdolls)
+				for (BodyID id : ragdoll->GetBodyIDs())
+				{
+					Vec3 pos = bi.GetPosition(id);
+					Quat rot = bi.GetRotation(id);
+					hash_combine(hash, pos.GetX(), pos.GetY(), pos.GetZ(), rot.GetX(), rot.GetY(), rot.GetZ(), rot.GetW());
+				}
+
+			// Remove ragdolls
+			for (Ragdoll *ragdoll : ragdolls)
+				ragdoll->RemoveFromPhysicsSystem();
+
+			// Trace stat line
+			cout << motion_quality_str << ", " << num_threads + 1 << ", " << 1.0e-9 * total_duration.count() << ", " << iterations << ", " << hash << endl;
+		}
+	}
+
+	// End profiling this thread
+	JPH_PROFILE_THREAD_END();
+
+	return 0;
+}

+ 9 - 5
Samples/SamplesApp.cpp

@@ -304,6 +304,9 @@ SamplesApp::SamplesApp()
 	// Create job system
 	// Create job system
 	mJobSystem = new JobSystemThreadPool(cMaxPhysicsJobs, cMaxPhysicsBarriers, mMaxConcurrentJobs - 1);
 	mJobSystem = new JobSystemThreadPool(cMaxPhysicsJobs, cMaxPhysicsBarriers, mMaxConcurrentJobs - 1);
 
 
+	// Create job system without extra threads for validating
+	mJobSystemValidating = new JobSystemThreadPool(cMaxPhysicsJobs, cMaxPhysicsBarriers, 0);
+
 	// Create UI
 	// Create UI
 	UIElement *main_menu = mDebugUI->CreateMenu();
 	UIElement *main_menu = mDebugUI->CreateMenu();
 	mDebugUI->CreateTextButton(main_menu, "Select Test", [this]() { 
 	mDebugUI->CreateTextButton(main_menu, "Select Test", [this]() { 
@@ -480,6 +483,7 @@ SamplesApp::~SamplesApp()
 	delete mTest;
 	delete mTest;
 	delete mContactListener;
 	delete mContactListener;
 	delete mPhysicsSystem;
 	delete mPhysicsSystem;
+	delete mJobSystemValidating;
 	delete mJobSystem;
 	delete mJobSystem;
 	delete mTempAllocator;
 	delete mTempAllocator;
 }
 }
@@ -1791,7 +1795,7 @@ bool SamplesApp::RenderFrame(float inDeltaTime)
 			DrawPhysics();
 			DrawPhysics();
 
 
 			// Step the world (with fixed frequency)
 			// Step the world (with fixed frequency)
-			StepPhysics();
+			StepPhysics(mJobSystem);
 
 
 		#ifdef JPH_DEBUG_RENDERER
 		#ifdef JPH_DEBUG_RENDERER
 			// Draw any contacts that were collected through the contact listener
 			// Draw any contacts that were collected through the contact listener
@@ -1838,7 +1842,7 @@ bool SamplesApp::RenderFrame(float inDeltaTime)
 			DrawPhysics();
 			DrawPhysics();
 
 
 			// Update the physics world
 			// Update the physics world
-			StepPhysics();
+			StepPhysics(mJobSystem);
 
 
 		#ifdef JPH_DEBUG_RENDERER
 		#ifdef JPH_DEBUG_RENDERER
 			// Draw any contacts that were collected through the contact listener
 			// Draw any contacts that were collected through the contact listener
@@ -1856,7 +1860,7 @@ bool SamplesApp::RenderFrame(float inDeltaTime)
 				RestoreState(mPlaybackFrames.back());
 				RestoreState(mPlaybackFrames.back());
 
 
 				// Step again
 				// Step again
-				StepPhysics();
+				StepPhysics(mJobSystemValidating);
 
 
 				// Validate that the result is the same
 				// Validate that the result is the same
 				ValidateState(post_step_state);
 				ValidateState(post_step_state);
@@ -2018,7 +2022,7 @@ void SamplesApp::DrawPhysics()
 	mShapeToGeometry = move(shape_to_geometry);
 	mShapeToGeometry = move(shape_to_geometry);
 }
 }
 
 
-void SamplesApp::StepPhysics()
+void SamplesApp::StepPhysics(JobSystem *inJobSystem)
 {
 {
 	float delta_time = 1.0f / mUpdateFrequency;
 	float delta_time = 1.0f / mUpdateFrequency;
 
 
@@ -2039,7 +2043,7 @@ void SamplesApp::StepPhysics()
 	uint64 start_tick = GetProcessorTickCount();
 	uint64 start_tick = GetProcessorTickCount();
 
 
 	// Step the world (with fixed frequency)
 	// Step the world (with fixed frequency)
-	mPhysicsSystem->Update(delta_time, mCollisionSteps, mIntegrationSubSteps, mTempAllocator, mJobSystem);
+	mPhysicsSystem->Update(delta_time, mCollisionSteps, mIntegrationSubSteps, mTempAllocator, inJobSystem);
 
 
 	// Accumulate time
 	// Accumulate time
 	mTotalTime += GetProcessorTickCount() - start_tick;
 	mTotalTime += GetProcessorTickCount() - start_tick;

+ 2 - 1
Samples/SamplesApp.h

@@ -66,7 +66,7 @@ private:
 	void					DrawPhysics();
 	void					DrawPhysics();
 
 
 	// Update the physics system with a fixed delta time
 	// Update the physics system with a fixed delta time
-	void					StepPhysics();
+	void					StepPhysics(JobSystem *inJobSystem);
 
 
 	// Save state of simulation
 	// Save state of simulation
 	void					SaveState(StateRecorderImpl &inStream);
 	void					SaveState(StateRecorderImpl &inStream);
@@ -84,6 +84,7 @@ private:
 	int						mIntegrationSubSteps = 1;									// How many integration steps per physics update
 	int						mIntegrationSubSteps = 1;									// How many integration steps per physics update
 	TempAllocator *			mTempAllocator = nullptr;									// Allocator for temporary allocations
 	TempAllocator *			mTempAllocator = nullptr;									// Allocator for temporary allocations
 	JobSystem *				mJobSystem = nullptr;										// The job system that runs physics jobs
 	JobSystem *				mJobSystem = nullptr;										// The job system that runs physics jobs
+	JobSystem *				mJobSystemValidating = nullptr;								// The job system to use when validating determinism
 	PhysicsSystem *			mPhysicsSystem = nullptr;									// The physics system that simulates the world
 	PhysicsSystem *			mPhysicsSystem = nullptr;									// The physics system that simulates the world
 	ContactListenerImpl *	mContactListener = nullptr;									// Contact listener implementation
 	ContactListenerImpl *	mContactListener = nullptr;									// Contact listener implementation
 	PhysicsSettings			mPhysicsSettings;											// Main physics simulation settings
 	PhysicsSettings			mPhysicsSettings;											// Main physics simulation settings