Browse Source

Added new define JPH_TRACK_SIMULATION_STATS (#1773)

This tracks simulation statistics on a per body basis. It can be used to figure out bodies are the most expensive to simulate.
Jorrit Rouwe 1 week ago
parent
commit
f6d41d9d1c

+ 3 - 0
Build/CMakeLists.txt

@@ -78,6 +78,9 @@ option(TRACK_BROADPHASE_STATS "Track Broadphase Stats" OFF)
 # Setting to periodically trace narrowphase stats to help determine which collision queries could be optimized
 # Setting to periodically trace narrowphase stats to help determine which collision queries could be optimized
 option(TRACK_NARROWPHASE_STATS "Track Narrowphase Stats" OFF)
 option(TRACK_NARROWPHASE_STATS "Track Narrowphase Stats" OFF)
 
 
+# Setting to track simulation timings per body
+option(JPH_TRACK_SIMULATION_STATS "Track Simulation Stats" OFF)
+
 # Enable the debug renderer in the Debug and Release builds. Note that DEBUG_RENDERER_IN_DISTRIBUTION will override this setting.
 # Enable the debug renderer in the Debug and Release builds. Note that DEBUG_RENDERER_IN_DISTRIBUTION will override this setting.
 option(DEBUG_RENDERER_IN_DEBUG_AND_RELEASE "Enable debug renderer in Debug and Release builds" ON)
 option(DEBUG_RENDERER_IN_DEBUG_AND_RELEASE "Enable debug renderer in Debug and Release builds" ON)
 
 

+ 4 - 0
Docs/ReleaseNotes.md

@@ -4,6 +4,10 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
 
 
 ## Unreleased Changes
 ## Unreleased Changes
 
 
+### New functionality
+
+* Added new define `JPH_TRACK_SIMULATION_STATS` which tracks simulation statistics on a per body basis. This can be used to figure out bodies are the most expensive to simulate.
+
 ### Bug Fixes
 ### Bug Fixes
 
 
 * A 6DOF constraint that constrains all rotation axis in combination with a body that has some of its rotation axis locked would not constrain the rotation in the unlocked axis
 * A 6DOF constraint that constrains all rotation axis in combination with a body that has some of its rotation axis locked would not constrain the rotation in the unlocked axis

+ 3 - 3
Jolt/Core/Profiler.h

@@ -113,6 +113,9 @@ public:
 	/// Remove a thread from being instrumented
 	/// Remove a thread from being instrumented
 	void						RemoveThread(ProfileThread *inThread);
 	void						RemoveThread(ProfileThread *inThread);
 
 
+	/// Get the amount of ticks per second, note that this number will never be fully accurate as the amount of ticks per second may vary with CPU load, so this number is only to be used to give an indication of time for profiling purposes
+	uint64						GetProcessorTicksPerSecond() const;
+
 	/// Singleton instance
 	/// Singleton instance
 	static Profiler *			sInstance;
 	static Profiler *			sInstance;
 
 
@@ -167,9 +170,6 @@ private:
 	/// We measure the amount of ticks per second, this function resets the reference time point
 	/// We measure the amount of ticks per second, this function resets the reference time point
 	void						UpdateReferenceTime();
 	void						UpdateReferenceTime();
 
 
-	/// Get the amount of ticks per second, note that this number will never be fully accurate as the amount of ticks per second may vary with CPU load, so this number is only to be used to give an indication of time for profiling purposes
-	uint64						GetProcessorTicksPerSecond() const;
-
 	/// Dump profiling statistics
 	/// Dump profiling statistics
 	void						DumpInternal();
 	void						DumpInternal();
 	void						DumpChart(const char *inTag, const Threads &inThreads, const KeyToAggregator &inKeyToAggregators, const Aggregators &inAggregators);
 	void						DumpChart(const char *inTag, const Threads &inThreads, const KeyToAggregator &inKeyToAggregators, const Aggregators &inAggregators);

+ 5 - 0
Jolt/Jolt.cmake

@@ -562,6 +562,11 @@ if (TRACK_NARROWPHASE_STATS)
 	target_compile_definitions(Jolt PUBLIC JPH_TRACK_NARROWPHASE_STATS)
 	target_compile_definitions(Jolt PUBLIC JPH_TRACK_NARROWPHASE_STATS)
 endif()
 endif()
 
 
+# Setting to track simulation timings per body
+if (JPH_TRACK_SIMULATION_STATS)
+	target_compile_definitions(Jolt PUBLIC JPH_TRACK_SIMULATION_STATS)
+endif()
+
 # Enable the debug renderer
 # Enable the debug renderer
 if (DEBUG_RENDERER_IN_DISTRIBUTION)
 if (DEBUG_RENDERER_IN_DISTRIBUTION)
 	target_compile_definitions(Jolt PUBLIC "JPH_DEBUG_RENDERER")
 	target_compile_definitions(Jolt PUBLIC "JPH_DEBUG_RENDERER")

+ 50 - 0
Jolt/Physics/Body/BodyManager.cpp

@@ -610,6 +610,11 @@ void BodyManager::DeactivateBodies(const BodyID *inBodyIDs, int inNumber)
 				// Mark this body as no longer active
 				// Mark this body as no longer active
 				body.mMotionProperties->mIslandIndex = Body::cInactiveIndex;
 				body.mMotionProperties->mIslandIndex = Body::cInactiveIndex;
 
 
+			#ifdef JPH_TRACK_SIMULATION_STATS
+				// Reset simulation stats
+				body.mMotionProperties->mSimulationStats.Reset();
+			#endif
+
 				// Reset velocity
 				// Reset velocity
 				body.mMotionProperties->mLinearVelocity = Vec3::sZero();
 				body.mMotionProperties->mLinearVelocity = Vec3::sZero();
 				body.mMotionProperties->mAngularVelocity = Vec3::sZero();
 				body.mMotionProperties->mAngularVelocity = Vec3::sZero();
@@ -1164,4 +1169,49 @@ void BodyManager::ValidateActiveBodyBounds()
 }
 }
 #endif // JPH_DEBUG
 #endif // JPH_DEBUG
 
 
+#ifdef JPH_TRACK_SIMULATION_STATS
+void BodyManager::ResetSimulationStats()
+{
+	JPH_PROFILE_FUNCTION();
+
+	UniqueLock lock(mActiveBodiesMutex JPH_IF_ENABLE_ASSERTS(, this, EPhysicsLockTypes::ActiveBodiesList));
+
+	for (uint type = 0; type < cBodyTypeCount; ++type)
+		for (BodyID *id = mActiveBodies[type], *id_end = mActiveBodies[type] + mNumActiveBodies[type].load(memory_order_relaxed); id < id_end; ++id)
+		{
+			const Body *body = mBodies[id->GetIndex()];
+			body->mMotionProperties->GetSimulationStats().Reset();
+		}
+}
+
+#ifdef JPH_PROFILE_ENABLED
+void BodyManager::ReportSimulationStats()
+{
+	UniqueLock lock(mActiveBodiesMutex JPH_IF_ENABLE_ASSERTS(, this, EPhysicsLockTypes::ActiveBodiesList));
+
+	Trace("BodyID, IslandIndex, NarrowPhase (us), VelocityConstraint (us), PositionConstraint (us), CCD (us), NumContactConstraints, NumVelocitySteps, NumPositionSteps");
+
+	double us_per_tick = 1000000.0 / Profiler::sInstance->GetProcessorTicksPerSecond();
+
+	for (uint type = 0; type < cBodyTypeCount; ++type)
+		for (BodyID *id = mActiveBodies[type], *id_end = mActiveBodies[type] + mNumActiveBodies[type].load(memory_order_relaxed); id < id_end; ++id)
+		{
+			const Body *body = mBodies[id->GetIndex()];
+			const MotionProperties *mp = body->mMotionProperties;
+			const MotionProperties::SimulationStats &stats = mp->GetSimulationStats();
+			Trace("%u, %u, %.2f, %.2f, %.2f, %.2f, %u, %u, %u",
+				body->GetID().GetIndex(),
+				mp->GetIslandIndexInternal(),
+				double(stats.mNarrowPhaseTicks) * us_per_tick,
+				double(stats.mVelocityConstraintTicks) * us_per_tick,
+				double(stats.mPositionConstraintTicks) * us_per_tick,
+				double(stats.mCCDTicks) * us_per_tick,
+				stats.mNumContactConstraints.load(memory_order_relaxed),
+				stats.mNumVelocitySteps,
+				stats.mNumPositionSteps);
+		}
+}
+#endif // JPH_PROFILE_ENABLED
+#endif // JPH_TRACK_SIMULATION_STATS
+
 JPH_NAMESPACE_END
 JPH_NAMESPACE_END

+ 10 - 0
Jolt/Physics/Body/BodyManager.h

@@ -292,6 +292,16 @@ public:
 	void							ValidateActiveBodyBounds();
 	void							ValidateActiveBodyBounds();
 #endif // JPH_DEBUG
 #endif // JPH_DEBUG
 
 
+#ifdef JPH_TRACK_SIMULATION_STATS
+	/// Resets the per body simulation stats
+	void							ResetSimulationStats();
+
+#ifdef JPH_PROFILE_ENABLED
+	/// Dump the per body simulation stats to the TTY
+	void							ReportSimulationStats();
+#endif
+#endif
+
 private:
 private:
 	/// Increment and get the sequence number of the body
 	/// Increment and get the sequence number of the body
 #ifdef JPH_COMPILER_CLANG
 #ifdef JPH_COMPILER_CLANG

+ 23 - 0
Jolt/Physics/Body/MotionProperties.h

@@ -186,6 +186,25 @@ public:
 	void					SetNumPositionStepsOverride(uint inN)							{ JPH_ASSERT(inN < 256); mNumPositionStepsOverride = uint8(inN); }
 	void					SetNumPositionStepsOverride(uint inN)							{ JPH_ASSERT(inN < 256); mNumPositionStepsOverride = uint8(inN); }
 	uint					GetNumPositionStepsOverride() const								{ return mNumPositionStepsOverride; }
 	uint					GetNumPositionStepsOverride() const								{ return mNumPositionStepsOverride; }
 
 
+#ifdef JPH_TRACK_SIMULATION_STATS
+	/// Stats for this body. These are average for the simulation island the body was part of.
+	struct SimulationStats
+	{
+		void				Reset()															{ mNarrowPhaseTicks.store(0, memory_order_relaxed); mVelocityConstraintTicks = 0; mPositionConstraintTicks = 0; mCCDTicks.store(0, memory_order_relaxed); mNumContactConstraints.store(0, memory_order_relaxed); mNumVelocitySteps = 0; mNumPositionSteps = 0; }
+
+		atomic<uint64>		mNarrowPhaseTicks = 0;											///< Number of processor ticks spent doing narrow phase collision detection
+		uint64				mVelocityConstraintTicks = 0;									///< Number of ticks spent solving velocity constraints
+		uint64				mPositionConstraintTicks = 0;									///< Number of ticks spent solving position constraints
+		atomic<uint64>		mCCDTicks = 0;													///< Number of ticks spent doing CCD
+		atomic<uint32>		mNumContactConstraints = 0;										///< Number of contact constraints created for this body
+		uint8				mNumVelocitySteps = 0;											///< Number of velocity iterations performed
+		uint8				mNumPositionSteps = 0;											///< Number of position iterations performed
+	};
+
+	const SimulationStats &	GetSimulationStats() const										{ return mSimulationStats; }
+	SimulationStats &		GetSimulationStats()											{ return mSimulationStats; }
+#endif // JPH_TRACK_SIMULATION_STATS
+
 	////////////////////////////////////////////////////////////
 	////////////////////////////////////////////////////////////
 	// FUNCTIONS BELOW THIS LINE ARE FOR INTERNAL USE ONLY
 	// FUNCTIONS BELOW THIS LINE ARE FOR INTERNAL USE ONLY
 	////////////////////////////////////////////////////////////
 	////////////////////////////////////////////////////////////
@@ -275,6 +294,10 @@ private:
 	EBodyType				mCachedBodyType;												///< Copied from Body::mBodyType and cached for asserting purposes
 	EBodyType				mCachedBodyType;												///< Copied from Body::mBodyType and cached for asserting purposes
 	EMotionType				mCachedMotionType;												///< Copied from Body::mMotionType and cached for asserting purposes
 	EMotionType				mCachedMotionType;												///< Copied from Body::mMotionType and cached for asserting purposes
 #endif
 #endif
+
+#ifdef JPH_TRACK_SIMULATION_STATS
+	SimulationStats			mSimulationStats;
+#endif // JPH_TRACK_SIMULATION_STATS
 };
 };
 
 
 JPH_NAMESPACE_END
 JPH_NAMESPACE_END

+ 16 - 0
Jolt/Physics/Constraints/ContactConstraintManager.cpp

@@ -980,6 +980,14 @@ void ContactConstraintManager::GetContactsFromCache(ContactAllocator &ioContactA
 			if (sDrawContactManifolds)
 			if (sDrawContactManifolds)
 				constraint.Draw(DebugRenderer::sInstance, Color::sYellow);
 				constraint.Draw(DebugRenderer::sInstance, Color::sYellow);
 		#endif // JPH_DEBUG_RENDERER
 		#endif // JPH_DEBUG_RENDERER
+
+		#ifdef JPH_TRACK_SIMULATION_STATS
+			// Track new contact constraints
+			if (!body1->IsStatic())
+				body1->GetMotionPropertiesUnchecked()->GetSimulationStats().mNumContactConstraints.fetch_add(1, memory_order_relaxed);
+			if (!body2->IsStatic())
+				body2->GetMotionPropertiesUnchecked()->GetSimulationStats().mNumContactConstraints.fetch_add(1, memory_order_relaxed);
+		#endif
 		}
 		}
 
 
 		// Mark contact as persisted so that we won't fire OnContactRemoved callbacks
 		// Mark contact as persisted so that we won't fire OnContactRemoved callbacks
@@ -1222,6 +1230,14 @@ bool ContactConstraintManager::TemplatedAddContactConstraint(ContactAllocator &i
 		if (sDrawContactManifolds)
 		if (sDrawContactManifolds)
 			constraint.Draw(DebugRenderer::sInstance, Color::sOrange);
 			constraint.Draw(DebugRenderer::sInstance, Color::sOrange);
 	#endif // JPH_DEBUG_RENDERER
 	#endif // JPH_DEBUG_RENDERER
+
+	#ifdef JPH_TRACK_SIMULATION_STATS
+		// Track new contact constraints
+		if constexpr (Type1 != EMotionType::Static)
+			inBody1.GetMotionPropertiesUnchecked()->GetSimulationStats().mNumContactConstraints.fetch_add(1, memory_order_relaxed);
+		if constexpr (Type2 != EMotionType::Static)
+			inBody2.GetMotionPropertiesUnchecked()->GetSimulationStats().mNumContactConstraints.fetch_add(1, memory_order_relaxed);
+	#endif
 	}
 	}
 	else
 	else
 	{
 	{

+ 10 - 0
Jolt/Physics/IslandBuilder.cpp

@@ -382,6 +382,12 @@ void IslandBuilder::Finalize(const BodyID *inActiveBodies, uint32 inNumActiveBod
 	SortIslands(inTempAllocator);
 	SortIslands(inTempAllocator);
 
 
 	mNumPositionSteps = (uint8 *)inTempAllocator->Allocate(mNumIslands * sizeof(uint8));
 	mNumPositionSteps = (uint8 *)inTempAllocator->Allocate(mNumIslands * sizeof(uint8));
+
+#ifdef JPH_TRACK_SIMULATION_STATS
+	mIslandStats = (IslandStats *)inTempAllocator->Allocate(mNumIslands * sizeof(IslandStats));
+	for (uint32 i = 0; i < mNumIslands; ++i)
+		new (&mIslandStats[i]) IslandStats();
+#endif
 }
 }
 
 
 void IslandBuilder::GetBodiesInIsland(uint32 inIslandIndex, BodyID *&outBodiesBegin, BodyID *&outBodiesEnd) const
 void IslandBuilder::GetBodiesInIsland(uint32 inIslandIndex, BodyID *&outBodiesBegin, BodyID *&outBodiesEnd) const
@@ -432,6 +438,10 @@ void IslandBuilder::ResetIslands(TempAllocator *inTempAllocator)
 {
 {
 	JPH_PROFILE_FUNCTION();
 	JPH_PROFILE_FUNCTION();
 
 
+#ifdef JPH_TRACK_SIMULATION_STATS
+	inTempAllocator->Free(mIslandStats, mNumIslands * sizeof(IslandStats));
+#endif
+
 	inTempAllocator->Free(mNumPositionSteps, mNumIslands * sizeof(uint8));
 	inTempAllocator->Free(mNumPositionSteps, mNumIslands * sizeof(uint8));
 
 
 	if (mIslandsSorted != nullptr)
 	if (mIslandsSorted != nullptr)

+ 16 - 0
Jolt/Physics/IslandBuilder.h

@@ -54,6 +54,18 @@ public:
 	void					SetNumPositionSteps(uint32 inIslandIndex, uint inNumPositionSteps)	{ JPH_ASSERT(inIslandIndex < mNumIslands); JPH_ASSERT(inNumPositionSteps < 256); mNumPositionSteps[inIslandIndex] = uint8(inNumPositionSteps); }
 	void					SetNumPositionSteps(uint32 inIslandIndex, uint inNumPositionSteps)	{ JPH_ASSERT(inIslandIndex < mNumIslands); JPH_ASSERT(inNumPositionSteps < 256); mNumPositionSteps[inIslandIndex] = uint8(inNumPositionSteps); }
 	uint					GetNumPositionSteps(uint32 inIslandIndex) const						{ JPH_ASSERT(inIslandIndex < mNumIslands); return mNumPositionSteps[inIslandIndex]; }
 	uint					GetNumPositionSteps(uint32 inIslandIndex) const						{ JPH_ASSERT(inIslandIndex < mNumIslands); return mNumPositionSteps[inIslandIndex]; }
 
 
+#ifdef JPH_TRACK_SIMULATION_STATS
+	struct IslandStats
+	{
+		atomic<uint64>		mVelocityConstraintTicks = 0;
+		atomic<uint64>		mPositionConstraintTicks = 0;
+		uint8				mNumVelocitySteps = 0;
+	};
+
+	/// Tracks simulation stats per island
+	IslandStats &			GetIslandStats(uint32 inIslandIndex)								{ return mIslandStats[inIslandIndex]; }
+#endif
+
 	/// After you're done calling the three functions above, call this function to free associated data
 	/// After you're done calling the three functions above, call this function to free associated data
 	void					ResetIslands(TempAllocator *inTempAllocator);
 	void					ResetIslands(TempAllocator *inTempAllocator);
 
 
@@ -101,6 +113,10 @@ private:
 
 
 	uint8 *					mNumPositionSteps = nullptr;					///< Number of position steps for each island
 	uint8 *					mNumPositionSteps = nullptr;					///< Number of position steps for each island
 
 
+#ifdef JPH_TRACK_SIMULATION_STATS
+	IslandStats *			mIslandStats = nullptr;							///< Per island statistics
+#endif
+
 	// Counters
 	// Counters
 	uint32					mMaxActiveBodies;								///< Maximum size of the active bodies list (see BodyManager::mActiveBodies)
 	uint32					mMaxActiveBodies;								///< Maximum size of the active bodies list (see BodyManager::mActiveBodies)
 	uint32					mNumActiveBodies = 0;							///< Number of active bodies passed to
 	uint32					mNumActiveBodies = 0;							///< Number of active bodies passed to

+ 146 - 30
Jolt/Physics/PhysicsSystem.cpp

@@ -129,6 +129,35 @@ void PhysicsSystem::RemoveStepListener(PhysicsStepListener *inListener)
 	mStepListeners.pop_back();
 	mStepListeners.pop_back();
 }
 }
 
 
+#ifdef JPH_TRACK_SIMULATION_STATS
+void PhysicsSystem::GatherIslandStats()
+{
+	JPH_PROFILE_FUNCTION();
+
+	for (uint32 island_idx = 0; island_idx < mIslandBuilder.GetNumIslands(); ++island_idx)
+	{
+		BodyID *bodies_begin, *bodies_end;
+		mIslandBuilder.GetBodiesInIsland(island_idx, bodies_begin, bodies_end);
+		uint64 num_bodies = bodies_end - bodies_begin;
+
+		// Equally distribute the stats over all bodies
+		const IslandBuilder::IslandStats &stats = mIslandBuilder.GetIslandStats(island_idx);
+		uint64 num_velocity_ticks = stats.mVelocityConstraintTicks / num_bodies;
+		uint64 num_position_ticks = stats.mPositionConstraintTicks / num_bodies;
+		uint8 num_position_steps = (uint8)mIslandBuilder.GetNumPositionSteps(island_idx);
+
+		for (BodyID *body_id = bodies_begin; body_id < bodies_end; ++body_id)
+		{
+			MotionProperties::SimulationStats &out_stats = mBodyManager.GetBody(*body_id).GetMotionProperties()->GetSimulationStats();
+			out_stats.mNumVelocitySteps = stats.mNumVelocitySteps;
+			out_stats.mNumPositionSteps = num_position_steps;
+			out_stats.mVelocityConstraintTicks += num_velocity_ticks; // In case of multiple collision steps we accumulate
+			out_stats.mPositionConstraintTicks += num_position_ticks;
+		}
+	}
+}
+#endif // JPH_TRACK_SIMULATION_STATS
+
 EPhysicsUpdateError PhysicsSystem::Update(float inDeltaTime, int inCollisionSteps, TempAllocator *inTempAllocator, JobSystem *inJobSystem)
 EPhysicsUpdateError PhysicsSystem::Update(float inDeltaTime, int inCollisionSteps, TempAllocator *inTempAllocator, JobSystem *inJobSystem)
 {
 {
 	JPH_PROFILE_FUNCTION();
 	JPH_PROFILE_FUNCTION();
@@ -162,6 +191,11 @@ EPhysicsUpdateError PhysicsSystem::Update(float inDeltaTime, int inCollisionStep
 		return EPhysicsUpdateError::None;
 		return EPhysicsUpdateError::None;
 	}
 	}
 
 
+#ifdef JPH_TRACK_SIMULATION_STATS
+	// Reset accumulated stats on the active bodies
+	mBodyManager.ResetSimulationStats();
+#endif
+
 	// Calculate ratio between current and previous frame delta time to scale initial constraint forces
 	// Calculate ratio between current and previous frame delta time to scale initial constraint forces
 	float step_delta_time = inDeltaTime / inCollisionSteps;
 	float step_delta_time = inDeltaTime / inCollisionSteps;
 	float warm_start_impulse_ratio = mPhysicsSettings.mConstraintWarmStart && mPreviousStepDeltaTime > 0.0f? step_delta_time / mPreviousStepDeltaTime : 0.0f;
 	float warm_start_impulse_ratio = mPhysicsSettings.mConstraintWarmStart && mPreviousStepDeltaTime > 0.0f? step_delta_time / mPreviousStepDeltaTime : 0.0f;
@@ -399,6 +433,11 @@ EPhysicsUpdateError PhysicsSystem::Update(float inDeltaTime, int inCollisionStep
 						mBodyManager.ValidateActiveBodyBounds();
 						mBodyManager.ValidateActiveBodyBounds();
 					#endif // JPH_DEBUG
 					#endif // JPH_DEBUG
 
 
+					#ifdef JPH_TRACK_SIMULATION_STATS
+						// Gather stats from the islands and distribute them over the bodies
+						GatherIslandStats();
+					#endif
+
 						// Store the number of active bodies at the start of the step
 						// Store the number of active bodies at the start of the step
 						next_step->mNumActiveBodiesAtStepStart = mBodyManager.GetNumActiveBodies(EBodyType::RigidBody);
 						next_step->mNumActiveBodiesAtStepStart = mBodyManager.GetNumActiveBodies(EBodyType::RigidBody);
 
 
@@ -580,6 +619,11 @@ EPhysicsUpdateError PhysicsSystem::Update(float inDeltaTime, int inCollisionStep
 	mBodyManager.ValidateActiveBodyBounds();
 	mBodyManager.ValidateActiveBodyBounds();
 #endif // JPH_DEBUG
 #endif // JPH_DEBUG
 
 
+#ifdef JPH_TRACK_SIMULATION_STATS
+	// Gather stats from the islands and distribute them over the bodies
+	GatherIslandStats();
+#endif
+
 	// Clear the large island splitter
 	// Clear the large island splitter
 	mLargeIslandSplitter.Reset(inTempAllocator);
 	mLargeIslandSplitter.Reset(inTempAllocator);
 
 
@@ -1023,6 +1067,10 @@ void PhysicsSystem::ProcessBodyPair(ContactAllocator &ioContactAllocator, const
 	// If the cache hasn't handled this body pair do actual collision detection
 	// If the cache hasn't handled this body pair do actual collision detection
 	if (!pair_handled)
 	if (!pair_handled)
 	{
 	{
+	#ifdef JPH_TRACK_SIMULATION_STATS
+		uint64 start_ticks = GetProcessorTickCount();
+	#endif
+
 		// Create entry in the cache for this body pair
 		// Create entry in the cache for this body pair
 		// Needs to happen irrespective if we found a collision or not (we want to remember that no collision was found too)
 		// Needs to happen irrespective if we found a collision or not (we want to remember that no collision was found too)
 		ContactConstraintManager::BodyPairHandle body_pair_handle = mContactManager.AddBodyPair(ioContactAllocator, *body1, *body2);
 		ContactConstraintManager::BodyPairHandle body_pair_handle = mContactManager.AddBodyPair(ioContactAllocator, *body1, *body2);
@@ -1262,6 +1310,24 @@ void PhysicsSystem::ProcessBodyPair(ContactAllocator &ioContactAllocator, const
 
 
 			constraint_created = collector.mConstraintCreated;
 			constraint_created = collector.mConstraintCreated;
 		}
 		}
+
+	#ifdef JPH_TRACK_SIMULATION_STATS
+		// Track time spent processing collision for this body pair
+		uint64 num_ticks = GetProcessorTickCount() - start_ticks;
+		if (body1->GetMotionType() > body2->GetMotionType())
+		{
+			// Assign all ticks to the body with the higher motion type
+			body1->GetMotionProperties()->GetSimulationStats().mNarrowPhaseTicks.fetch_add(num_ticks, memory_order_relaxed);
+		}
+		else
+		{
+			// When two bodies with the same motion type are involved, we give both bodies half the ticks
+			JPH_ASSERT(body1->GetMotionType() == body2->GetMotionType());
+			num_ticks /= 2;
+			body1->GetMotionProperties()->GetSimulationStats().mNarrowPhaseTicks.fetch_add(num_ticks, memory_order_relaxed);
+			body1->GetMotionProperties()->GetSimulationStats().mNarrowPhaseTicks.fetch_add(num_ticks, memory_order_relaxed);
+		}
+	#endif
 	}
 	}
 
 
 	// If a contact constraint was created, we need to do some extra work
 	// If a contact constraint was created, we need to do some extra work
@@ -1343,6 +1409,10 @@ void PhysicsSystem::JobSolveVelocityConstraints(PhysicsUpdateContext *ioContext,
 			{
 			{
 			case LargeIslandSplitter::EStatus::BatchRetrieved:
 			case LargeIslandSplitter::EStatus::BatchRetrieved:
 				{
 				{
+				#ifdef JPH_TRACK_SIMULATION_STATS
+					uint64 start_tick = GetProcessorTickCount();
+				#endif
+
 					if (first_iteration)
 					if (first_iteration)
 					{
 					{
 						// Iteration 0 is used to warm start the batch (we added 1 to the number of iterations in LargeIslandSplitter::SplitIsland)
 						// Iteration 0 is used to warm start the batch (we added 1 to the number of iterations in LargeIslandSplitter::SplitIsland)
@@ -1365,6 +1435,11 @@ void PhysicsSystem::JobSolveVelocityConstraints(PhysicsUpdateContext *ioContext,
 					if (last_iteration)
 					if (last_iteration)
 						mContactManager.StoreAppliedImpulses(contacts_begin, contacts_end);
 						mContactManager.StoreAppliedImpulses(contacts_begin, contacts_end);
 
 
+				#ifdef JPH_TRACK_SIMULATION_STATS
+					uint64 num_ticks = GetProcessorTickCount() - start_tick;
+					mIslandBuilder.GetIslandStats(mLargeIslandSplitter.GetIslandIndex(split_island_index)).mVelocityConstraintTicks.fetch_add(num_ticks, memory_order_relaxed);
+				#endif
+
 					// We processed work, loop again
 					// We processed work, loop again
 					continue;
 					continue;
 				}
 				}
@@ -1414,6 +1489,10 @@ void PhysicsSystem::JobSolveVelocityConstraints(PhysicsUpdateContext *ioContext,
 				continue;
 				continue;
 			}
 			}
 
 
+		#ifdef JPH_TRACK_SIMULATION_STATS
+			uint64 start_tick = GetProcessorTickCount();
+		#endif
+
 			// Sorting is costly but needed for a deterministic simulation, allow the user to turn this off
 			// Sorting is costly but needed for a deterministic simulation, allow the user to turn this off
 			if (mPhysicsSettings.mDeterministicSimulation)
 			if (mPhysicsSettings.mDeterministicSimulation)
 			{
 			{
@@ -1426,29 +1505,36 @@ void PhysicsSystem::JobSolveVelocityConstraints(PhysicsUpdateContext *ioContext,
 
 
 			// Split up large islands
 			// Split up large islands
 			CalculateSolverSteps steps_calculator(mPhysicsSettings);
 			CalculateSolverSteps steps_calculator(mPhysicsSettings);
-			if (mPhysicsSettings.mUseLargeIslandSplitter
-				&& mLargeIslandSplitter.SplitIsland(island_idx, mIslandBuilder, mBodyManager, mContactManager, active_constraints, steps_calculator))
-				continue; // Loop again to try to fetch the newly split island
+			if (!mPhysicsSettings.mUseLargeIslandSplitter
+				|| !mLargeIslandSplitter.SplitIsland(island_idx, mIslandBuilder, mBodyManager, mContactManager, active_constraints, steps_calculator))
+			{
+				// We didn't create a split, just run the solver now for this entire island. Begin by warm starting.
+				ConstraintManager::sWarmStartVelocityConstraints(active_constraints, constraints_begin, constraints_end, warm_start_impulse_ratio, steps_calculator);
+				mContactManager.WarmStartVelocityConstraints(contacts_begin, contacts_end, warm_start_impulse_ratio, steps_calculator);
+				steps_calculator.Finalize();
+
+				// Solve velocity constraints
+				for (uint velocity_step = 0; velocity_step < steps_calculator.GetNumVelocitySteps(); ++velocity_step)
+				{
+					bool applied_impulse = ConstraintManager::sSolveVelocityConstraints(active_constraints, constraints_begin, constraints_end, delta_time);
+					applied_impulse |= mContactManager.SolveVelocityConstraints(contacts_begin, contacts_end);
+					if (!applied_impulse)
+						break;
+				}
 
 
-			// We didn't create a split, just run the solver now for this entire island. Begin by warm starting.
-			ConstraintManager::sWarmStartVelocityConstraints(active_constraints, constraints_begin, constraints_end, warm_start_impulse_ratio, steps_calculator);
-			mContactManager.WarmStartVelocityConstraints(contacts_begin, contacts_end, warm_start_impulse_ratio, steps_calculator);
-			steps_calculator.Finalize();
+				// Save back the lambdas in the contact cache for the warm start of the next physics update
+				mContactManager.StoreAppliedImpulses(contacts_begin, contacts_end);
+			}
 
 
 			// Store the number of position steps for later
 			// Store the number of position steps for later
 			mIslandBuilder.SetNumPositionSteps(island_idx, steps_calculator.GetNumPositionSteps());
 			mIslandBuilder.SetNumPositionSteps(island_idx, steps_calculator.GetNumPositionSteps());
 
 
-			// Solve velocity constraints
-			for (uint velocity_step = 0; velocity_step < steps_calculator.GetNumVelocitySteps(); ++velocity_step)
-			{
-				bool applied_impulse = ConstraintManager::sSolveVelocityConstraints(active_constraints, constraints_begin, constraints_end, delta_time);
-				applied_impulse |= mContactManager.SolveVelocityConstraints(contacts_begin, contacts_end);
-				if (!applied_impulse)
-					break;
-			}
-
-			// Save back the lambdas in the contact cache for the warm start of the next physics update
-			mContactManager.StoreAppliedImpulses(contacts_begin, contacts_end);
+		#ifdef JPH_TRACK_SIMULATION_STATS
+			uint64 num_ticks = GetProcessorTickCount() - start_tick;
+			IslandBuilder::IslandStats &stats = mIslandBuilder.GetIslandStats(island_idx);
+			stats.mNumVelocitySteps = (uint8)steps_calculator.GetNumVelocitySteps();
+			stats.mVelocityConstraintTicks.fetch_add(num_ticks, memory_order_relaxed);
+		#endif
 
 
 			// We processed work, loop again
 			// We processed work, loop again
 			continue;
 			continue;
@@ -1716,7 +1802,7 @@ void PhysicsSystem::JobFindCCDContacts(const PhysicsUpdateContext *ioContext, Ph
 		if (idx >= ioStep->mNumCCDBodies)
 		if (idx >= ioStep->mNumCCDBodies)
 			break;
 			break;
 		CCDBody &ccd_body = ioStep->mCCDBodies[idx];
 		CCDBody &ccd_body = ioStep->mCCDBodies[idx];
-		const Body &body = mBodyManager.GetBody(ccd_body.mBodyID1);
+		Body &body = mBodyManager.GetBody(ccd_body.mBodyID1);
 
 
 		// Filter out layers
 		// Filter out layers
 		DefaultBroadPhaseLayerFilter broadphase_layer_filter = GetDefaultBroadPhaseLayerFilter(body.GetObjectLayer());
 		DefaultBroadPhaseLayerFilter broadphase_layer_filter = GetDefaultBroadPhaseLayerFilter(body.GetObjectLayer());
@@ -1933,11 +2019,20 @@ void PhysicsSystem::JobFindCCDContacts(const PhysicsUpdateContext *ioContext, Ph
 		// Create shape filter
 		// Create shape filter
 		SimShapeFilterWrapper shape_filter(mSimShapeFilter, &body);
 		SimShapeFilterWrapper shape_filter(mSimShapeFilter, &body);
 
 
+	#ifdef JPH_TRACK_SIMULATION_STATS
+		uint64 start_tick = GetProcessorTickCount();
+	#endif
+
 		// Check if we collide with any other body. Note that we use the non-locking interface as we know the broadphase cannot be modified at this point.
 		// Check if we collide with any other body. Note that we use the non-locking interface as we know the broadphase cannot be modified at this point.
 		RShapeCast shape_cast(body.GetShape(), Vec3::sOne(), body.GetCenterOfMassTransform(), ccd_body.mDeltaPosition);
 		RShapeCast shape_cast(body.GetShape(), Vec3::sOne(), body.GetCenterOfMassTransform(), ccd_body.mDeltaPosition);
 		CCDBroadPhaseCollector bp_collector(ccd_body, body, shape_cast, settings, shape_filter, np_collector, mBodyManager, ioStep, ioContext->mStepDeltaTime);
 		CCDBroadPhaseCollector bp_collector(ccd_body, body, shape_cast, settings, shape_filter, np_collector, mBodyManager, ioStep, ioContext->mStepDeltaTime);
 		mBroadPhase->CastAABoxNoLock({ shape_cast.mShapeWorldBounds, shape_cast.mDirection }, bp_collector, broadphase_layer_filter, object_layer_filter);
 		mBroadPhase->CastAABoxNoLock({ shape_cast.mShapeWorldBounds, shape_cast.mDirection }, bp_collector, broadphase_layer_filter, object_layer_filter);
 
 
+	#ifdef JPH_TRACK_SIMULATION_STATS
+		uint64 num_ticks = GetProcessorTickCount() - start_tick;
+		body.GetMotionPropertiesUnchecked()->GetSimulationStats().mCCDTicks.fetch_add(num_ticks, memory_order_relaxed);
+	#endif
+
 		// Check if there was a hit
 		// Check if there was a hit
 		if (ccd_body.mFractionPlusSlop < 1.0f)
 		if (ccd_body.mFractionPlusSlop < 1.0f)
 		{
 		{
@@ -2402,20 +2497,31 @@ void PhysicsSystem::JobSolvePositionConstraints(PhysicsUpdateContext *ioContext,
 			switch (mLargeIslandSplitter.FetchNextBatch(split_island_index, constraints_begin, constraints_end, contacts_begin, contacts_end, first_iteration))
 			switch (mLargeIslandSplitter.FetchNextBatch(split_island_index, constraints_begin, constraints_end, contacts_begin, contacts_end, first_iteration))
 			{
 			{
 			case LargeIslandSplitter::EStatus::BatchRetrieved:
 			case LargeIslandSplitter::EStatus::BatchRetrieved:
-				// Solve the batch
-				ConstraintManager::sSolvePositionConstraints(active_constraints, constraints_begin, constraints_end, delta_time, baumgarte);
-				mContactManager.SolvePositionConstraints(contacts_begin, contacts_end);
+				{
+				#ifdef JPH_TRACK_SIMULATION_STATS
+					uint64 start_tick = GetProcessorTickCount();
+				#endif
 
 
-				// Mark the batch as processed
-				bool last_iteration, final_batch;
-				mLargeIslandSplitter.MarkBatchProcessed(split_island_index, constraints_begin, constraints_end, contacts_begin, contacts_end, last_iteration, final_batch);
+					// Solve the batch
+					ConstraintManager::sSolvePositionConstraints(active_constraints, constraints_begin, constraints_end, delta_time, baumgarte);
+					mContactManager.SolvePositionConstraints(contacts_begin, contacts_end);
 
 
-				// The final batch will update all bounds and check sleeping
-				if (final_batch)
-					CheckSleepAndUpdateBounds(mLargeIslandSplitter.GetIslandIndex(split_island_index), ioContext, ioStep, bodies_to_sleep);
+					// Mark the batch as processed
+					bool last_iteration, final_batch;
+					mLargeIslandSplitter.MarkBatchProcessed(split_island_index, constraints_begin, constraints_end, contacts_begin, contacts_end, last_iteration, final_batch);
 
 
-				// We processed work, loop again
-				continue;
+					// The final batch will update all bounds and check sleeping
+					if (final_batch)
+						CheckSleepAndUpdateBounds(mLargeIslandSplitter.GetIslandIndex(split_island_index), ioContext, ioStep, bodies_to_sleep);
+
+				#ifdef JPH_TRACK_SIMULATION_STATS
+					uint64 num_ticks = GetProcessorTickCount() - start_tick;
+					mIslandBuilder.GetIslandStats(mLargeIslandSplitter.GetIslandIndex(split_island_index)).mPositionConstraintTicks.fetch_add(num_ticks, memory_order_relaxed);
+				#endif
+
+					// We processed work, loop again
+					continue;
+				}
 
 
 			case LargeIslandSplitter::EStatus::WaitingForBatch:
 			case LargeIslandSplitter::EStatus::WaitingForBatch:
 				break;
 				break;
@@ -2451,6 +2557,10 @@ void PhysicsSystem::JobSolvePositionConstraints(PhysicsUpdateContext *ioContext,
 				&& num_items >= LargeIslandSplitter::cLargeIslandTreshold)
 				&& num_items >= LargeIslandSplitter::cLargeIslandTreshold)
 				continue;
 				continue;
 
 
+		#ifdef JPH_TRACK_SIMULATION_STATS
+			uint64 start_tick = GetProcessorTickCount();
+		#endif
+
 			// Check if this island needs solving
 			// Check if this island needs solving
 			if (num_items > 0)
 			if (num_items > 0)
 			{
 			{
@@ -2468,6 +2578,12 @@ void PhysicsSystem::JobSolvePositionConstraints(PhysicsUpdateContext *ioContext,
 			// After solving we will update all bounds and check sleeping
 			// After solving we will update all bounds and check sleeping
 			CheckSleepAndUpdateBounds(island_idx, ioContext, ioStep, bodies_to_sleep);
 			CheckSleepAndUpdateBounds(island_idx, ioContext, ioStep, bodies_to_sleep);
 
 
+		#ifdef JPH_TRACK_SIMULATION_STATS
+			// Average the total ticks spent over the number of bodies and assign each the average number of ticks per body
+			uint64 num_ticks = GetProcessorTickCount() - start_tick;
+			mIslandBuilder.GetIslandStats(island_idx).mPositionConstraintTicks.fetch_add(num_ticks, memory_order_relaxed);
+		#endif
+
 			// We processed work, loop again
 			// We processed work, loop again
 			continue;
 			continue;
 		}
 		}

+ 10 - 0
Jolt/Physics/PhysicsSystem.h

@@ -255,6 +255,11 @@ public:
 	void						ReportBroadphaseStats()										{ mBroadPhase->ReportStats(); }
 	void						ReportBroadphaseStats()										{ mBroadPhase->ReportStats(); }
 #endif // JPH_TRACK_BROADPHASE_STATS
 #endif // JPH_TRACK_BROADPHASE_STATS
 
 
+#if defined(JPH_TRACK_SIMULATION_STATS) && defined(JPH_PROFILE_ENABLED)
+	/// Dump the per body simulation stats to the TTY
+	void						ReportSimulationStats()										{ mBodyManager.ReportSimulationStats(); }
+#endif
+
 private:
 private:
 	using CCDBody = PhysicsUpdateContext::Step::CCDBody;
 	using CCDBody = PhysicsUpdateContext::Step::CCDBody;
 
 
@@ -283,6 +288,11 @@ private:
 	/// Tries to spawn a new FindCollisions job if max concurrency hasn't been reached yet
 	/// Tries to spawn a new FindCollisions job if max concurrency hasn't been reached yet
 	void						TrySpawnJobFindCollisions(PhysicsUpdateContext::Step *ioStep) const;
 	void						TrySpawnJobFindCollisions(PhysicsUpdateContext::Step *ioStep) const;
 
 
+#ifdef JPH_TRACK_SIMULATION_STATS
+	/// Gather stats from the islands and distribute them over the bodies
+	void						GatherIslandStats();
+#endif
+
 	using ContactAllocator = ContactConstraintManager::ContactAllocator;
 	using ContactAllocator = ContactConstraintManager::ContactAllocator;
 
 
 	/// Process narrow phase for a single body pair
 	/// Process narrow phase for a single body pair

+ 6 - 0
Samples/SamplesApp.cpp

@@ -2170,6 +2170,12 @@ bool SamplesApp::UpdateFrame(float inDeltaTime)
 				mPlaybackMode = shift? EPlaybackMode::FastForward : EPlaybackMode::StepForward;
 				mPlaybackMode = shift? EPlaybackMode::FastForward : EPlaybackMode::StepForward;
 			}
 			}
 			break;
 			break;
+
+	#if defined(JPH_TRACK_SIMULATION_STATS) && defined(JPH_PROFILE_ENABLED)
+		case EKey::Y:
+			mPhysicsSystem->ReportSimulationStats();
+			break;
+	#endif
 		}
 		}
 
 
 	// Stop recording if record state is turned off
 	// Stop recording if record state is turned off