Parcourir la source

Added scene to test determinism of CharacterVirtual (#1478)

This scene has 100 characters walking on a mesh with some stairs and a wall to exercise most code paths of the character. It hashes all contact callbacks and the final character positions to detect any non-determinism issues.

See #1467
Jorrit Rouwe il y a 6 mois
Parent
commit
2af1bf9434

+ 31 - 0
.github/workflows/determinism_check.yml

@@ -4,6 +4,7 @@ env:
   CONVEX_VS_MESH_HASH: '0x610d538e15420778'
   RAGDOLL_HASH: '0x275057ded572c916'
   PYRAMID_HASH: '0x198b8eeaee57e29a'
+  CHARACTER_VIRTUAL_HASH: '0x8fbe7347b302ac43'
   EMSCRIPTEN_VERSION: 3.1.73
   UBUNTU_CLANG_VERSION: clang++-18
   UBUNTU_GCC_VERSION: g++-14
@@ -49,6 +50,9 @@ jobs:
     - name: Test Pyramid
       working-directory: ${{github.workspace}}/Build/Linux_Distribution
       run: ./PerformanceTest -q=LinearCast -t=max -s=Pyramid -validate_hash=${PYRAMID_HASH}
+    - name: Test CharacterVirtual
+      working-directory: ${{github.workspace}}/Build/Linux_Distribution
+      run: ./PerformanceTest -q=Discrete -t=max -s=CharacterVirtual -validate_hash=${CHARACTER_VIRTUAL_HASH}
 
   linux_gcc:
     runs-on: ubuntu-latest
@@ -74,6 +78,9 @@ jobs:
     - name: Test Pyramid
       working-directory: ${{github.workspace}}/Build/Linux_Distribution
       run: ./PerformanceTest -q=LinearCast -t=max -s=Pyramid -validate_hash=${PYRAMID_HASH}
+    - name: Test CharacterVirtual
+      working-directory: ${{github.workspace}}/Build/Linux_Distribution
+      run: ./PerformanceTest -q=Discrete -t=max -s=CharacterVirtual -validate_hash=${CHARACTER_VIRTUAL_HASH}
 
   msvc_cl:
     runs-on: windows-latest
@@ -101,6 +108,9 @@ jobs:
     - name: Test Pyramid
       working-directory: ${{github.workspace}}/Build/VS2022_CL/Distribution
       run: ./PerformanceTest -q=LinearCast -t=max -s=Pyramid "-validate_hash=$env:PYRAMID_HASH"
+    - name: Test CharacterVirtual
+      working-directory: ${{github.workspace}}/Build/VS2022_CL/Distribution
+      run: ./PerformanceTest -q=Discrete -t=max -s=CharacterVirtual "-validate_hash=$env:CHARACTER_VIRTUAL_HASH"
 
   msvc_cl_32:
     runs-on: windows-latest
@@ -128,6 +138,9 @@ jobs:
     - name: Test Pyramid
       working-directory: ${{github.workspace}}/Build/VS2022_CL_32BIT/Distribution
       run: ./PerformanceTest -q=LinearCast -t=max -s=Pyramid "-validate_hash=$env:PYRAMID_HASH"
+    - name: Test CharacterVirtual
+      working-directory: ${{github.workspace}}/Build/VS2022_CL_32BIT/Distribution
+      run: ./PerformanceTest -q=Discrete -t=max -s=CharacterVirtual "-validate_hash=$env:CHARACTER_VIRTUAL_HASH"
 
   macos:
     runs-on: macos-latest
@@ -153,6 +166,9 @@ jobs:
     - name: Test Pyramid
       working-directory: ${{github.workspace}}/Build/Linux_Distribution
       run: ./PerformanceTest -q=LinearCast -t=max -s=Pyramid -validate_hash=${PYRAMID_HASH}
+    - name: Test CharacterVirtual
+      working-directory: ${{github.workspace}}/Build/Linux_Distribution
+      run: ./PerformanceTest -q=Discrete -t=max -s=CharacterVirtual -validate_hash=${CHARACTER_VIRTUAL_HASH}
 
   arm_clang:
     runs-on: ubuntu-latest
@@ -181,6 +197,9 @@ jobs:
     - name: Test Pyramid
       working-directory: ${{github.workspace}}/Build/Linux_Distribution
       run: qemu-aarch64 -L /usr/aarch64-linux-gnu/ ./PerformanceTest -q=LinearCast -t=max -s=Pyramid -validate_hash=${PYRAMID_HASH}
+    - name: Test CharacterVirtual
+      working-directory: ${{github.workspace}}/Build/Linux_Distribution
+      run: qemu-aarch64 -L /usr/aarch64-linux-gnu/ ./PerformanceTest -q=Discrete -t=max -s=CharacterVirtual -validate_hash=${CHARACTER_VIRTUAL_HASH}
 
   arm_clang_32:
     runs-on: ubuntu-latest
@@ -209,6 +228,9 @@ jobs:
     - name: Test Pyramid
       working-directory: ${{github.workspace}}/Build/Linux_Distribution
       run: qemu-arm -L /usr/arm-linux-gnueabihf/ ./PerformanceTest -q=LinearCast -t=max -s=Pyramid -validate_hash=${PYRAMID_HASH}
+    - name: Test CharacterVirtual
+      working-directory: ${{github.workspace}}/Build/Linux_Distribution
+      run: qemu-arm -L /usr/arm-linux-gnueabihf/ ./PerformanceTest -q=Discrete -t=max -s=CharacterVirtual -validate_hash=${CHARACTER_VIRTUAL_HASH}
 
   arm_gcc:
     runs-on: ubuntu-latest
@@ -237,6 +259,9 @@ jobs:
     - name: Test Pyramid
       working-directory: ${{github.workspace}}/Build/Linux_Distribution
       run: qemu-aarch64 -L /usr/aarch64-linux-gnu/ ./PerformanceTest -q=LinearCast -t=max -s=Pyramid -validate_hash=${PYRAMID_HASH}
+    - name: Test CharacterVirtual
+      working-directory: ${{github.workspace}}/Build/Linux_Distribution
+      run: qemu-aarch64 -L /usr/aarch64-linux-gnu/ ./PerformanceTest -q=Discrete -t=max -s=CharacterVirtual -validate_hash=${CHARACTER_VIRTUAL_HASH}
 
   riscv_gcc:
     runs-on: ubuntu-latest
@@ -265,6 +290,9 @@ jobs:
     - name: Test Pyramid
       working-directory: ${{github.workspace}}/Build/Linux_Distribution
       run: qemu-riscv64 -L /usr/riscv64-linux-gnu/ ./PerformanceTest -q=LinearCast -t=max -s=Pyramid -validate_hash=${PYRAMID_HASH}
+    - name: Test CharacterVirtual
+      working-directory: ${{github.workspace}}/Build/Linux_Distribution
+      run: qemu-riscv64 -L /usr/riscv64-linux-gnu/ ./PerformanceTest -q=Discrete -t=max -s=CharacterVirtual -validate_hash=${CHARACTER_VIRTUAL_HASH}
 
   powerpcle_gcc:
     runs-on: ubuntu-latest
@@ -358,3 +386,6 @@ jobs:
     - name: Test Pyramid
       working-directory: ${{github.workspace}}/Build/WASM_Distribution
       run: node PerformanceTest.js -q=LinearCast -t=max -s=Pyramid -validate_hash=${PYRAMID_HASH}
+    - name: Test CharacterVirtual
+      working-directory: ${{github.workspace}}/Build/WASM_Distribution
+      run: node PerformanceTest.js -q=Discrete -t=max -s=CharacterVirtual -validate_hash=${CHARACTER_VIRTUAL_HASH}

+ 267 - 0
PerformanceTest/CharacterVirtualScene.h

@@ -0,0 +1,267 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+// Jolt includes
+#include <Jolt/Physics/Collision/Shape/BoxShape.h>
+#include <Jolt/Physics/Collision/Shape/MeshShape.h>
+#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
+#include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
+#include <Jolt/Physics/Character/CharacterVirtual.h>
+#include <Jolt/Physics/Body/BodyCreationSettings.h>
+
+// Local includes
+#include "PerformanceTestScene.h"
+#include "Layers.h"
+
+// A scene that drops a number of virtual characters on a scene and simulates them
+class CharacterVirtualScene : public PerformanceTestScene, public CharacterContactListener
+{
+public:
+	virtual const char *	GetName() const override
+	{
+		return "CharacterVirtual";
+	}
+
+	virtual bool			Load(const String &inAssetPath) override
+	{
+		const int n = 100;
+		const float cell_size = 0.5f;
+		const float max_height = 2.0f;
+		float center = n * cell_size / 2;
+
+		// Create vertices
+		const int num_vertices = (n + 1) * (n + 1);
+		VertexList vertices;
+		vertices.resize(num_vertices);
+		for (int x = 0; x <= n; ++x)
+			for (int z = 0; z <= n; ++z)
+			{
+				float height = Sin(float(x) * 20.0f / n) * Cos(float(z) * 20.0f / n);
+				vertices[z * (n + 1) + x] = Float3(cell_size * x, max_height * height, cell_size * z);
+			}
+
+		// Create regular grid of triangles
+		const int num_triangles = n * n * 2;
+		IndexedTriangleList indices;
+		indices.resize(num_triangles);
+		IndexedTriangle *next = indices.data();
+		for (int x = 0; x < n; ++x)
+			for (int z = 0; z < n; ++z)
+			{
+				int start = (n + 1) * z + x;
+
+				next->mIdx[0] = start;
+				next->mIdx[1] = start + n + 1;
+				next->mIdx[2] = start + 1;
+				next++;
+
+				next->mIdx[0] = start + 1;
+				next->mIdx[1] = start + n + 1;
+				next->mIdx[2] = start + n + 2;
+				next++;
+			}
+
+		// Create mesh
+		BodyCreationSettings mesh(new MeshShapeSettings(vertices, indices), RVec3(Real(-center), 0, Real(-center)), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING);
+		mWorld.push_back(mesh);
+
+		// Create pyramid stairs
+		for (int i = 0; i < 10; ++i)
+		{
+			float width = 4.0f - 0.4f * i;
+			BodyCreationSettings step(new BoxShape(Vec3(width, 0.5f * cStairsStepHeight, width)), RVec3(-4.0_r, -1.0_r + Real(i * cStairsStepHeight), 0), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING);
+			mWorld.push_back(step);
+		}
+
+		// Create wall consisting of vertical pillars
+		Ref<Shape> wall = new BoxShape(Vec3(0.1f, 2.5f, 0.1f), 0.0f);
+		for (int z = 0; z < 10; ++z)
+		{
+			BodyCreationSettings bcs(wall, RVec3(2.0_r, 1.0_r, 2.0_r + 0.2_r * z), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING);
+			mWorld.push_back(bcs);
+		}
+
+		// Create some dynamic boxes
+		Ref<Shape> box = new BoxShape(Vec3::sReplicate(0.25f));
+		for (int x = 0; x < 10; ++x)
+			for (int z = 0; z < 10; ++z)
+			{
+				BodyCreationSettings bcs(box, RVec3(4.0_r * x - 20.0_r, 5.0_r, 4.0_r * z - 20.0_r), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
+				bcs.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
+				bcs.mMassPropertiesOverride.mMass = 1.0f;
+				mWorld.push_back(bcs);
+			}
+
+		return true;
+	}
+
+	virtual void			StartTest(PhysicsSystem &inPhysicsSystem, EMotionQuality inMotionQuality) override
+	{
+		// Construct bodies
+		BodyInterface &bi = inPhysicsSystem.GetBodyInterface();
+		for (BodyCreationSettings &bcs : mWorld)
+			if (bcs.mMotionType == EMotionType::Dynamic)
+			{
+				bcs.mMotionQuality = inMotionQuality;
+				bi.CreateAndAddBody(bcs, EActivation::Activate);
+			}
+			else
+				bi.CreateAndAddBody(bcs, EActivation::DontActivate);
+
+		// Construct characters
+		CharacterID::sSetNextCharacterID();
+		RefConst<Shape> standing_shape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cCharacterHeightStanding, cCharacterRadiusStanding)).Create().Get();
+		RefConst<Shape> inner_standing_shape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cInnerShapeFraction * cCharacterHeightStanding, cInnerShapeFraction * cCharacterRadiusStanding)).Create().Get();
+		for (int y = 0; y < cNumCharactersY; ++y)
+			for (int x = 0; x < cNumCharactersX; ++x)
+			{
+				Ref<CharacterVirtualSettings> settings = new CharacterVirtualSettings();
+				settings->mShape = standing_shape;
+				settings->mSupportingVolume = Plane(Vec3::sAxisY(), -cCharacterRadiusStanding); // Accept contacts that touch the lower sphere of the capsule
+				settings->mInnerBodyShape = inner_standing_shape;
+				settings->mInnerBodyLayer = Layers::MOVING;
+				Ref<CharacterVirtual> character = new CharacterVirtual(settings, RVec3(4.0_r * x - 20.0_r, 2.0_r, 4.0_r * y - 20.0_r), Quat::sIdentity(), 0, &inPhysicsSystem);
+				character->SetCharacterVsCharacterCollision(&mCharacterVsCharacterCollision);
+				character->SetListener(this);
+				mCharacters.push_back(character);
+				mCharacterVsCharacterCollision.Add(character);
+			}
+
+		// Start at time 0
+		mTime = 0.0f;
+		mHash = HashBytes(nullptr, 0);
+	}
+
+	virtual void			UpdateTest(PhysicsSystem &inPhysicsSystem, TempAllocator &ioTempAllocator, float inDeltaTime) override
+	{
+		// Change direction every 2 seconds
+		mTime += inDeltaTime;
+		uint64 count = uint64(mTime / 2.0f) * cNumCharactersX * cNumCharactersY;
+
+		for (CharacterVirtual *ch : mCharacters)
+		{
+			// Calculate new vertical velocity
+			Vec3 new_velocity;
+			if (ch->GetGroundState() == CharacterVirtual::EGroundState::OnGround	// If on ground
+				&& ch->GetLinearVelocity().GetY() < 0.1f)						// And not moving away from ground
+				new_velocity = Vec3::sZero();
+			else
+				new_velocity = ch->GetLinearVelocity() * Vec3(0, 1, 0);
+			new_velocity += inPhysicsSystem.GetGravity() * inDeltaTime;
+
+			// Deterministic random input
+			uint64 hash = Hash<uint64> {} (count);
+			int x = int(hash % 10);
+			int y = int((hash / 10) % 10);
+			int speed = int((hash / 100) % 10);
+
+			// Determine target position
+			RVec3 target = RVec3(4.0_r * x - 20.0_r, 5.0_r, 4.0_r * y - 20.0_r);
+
+			// Determine new character velocity
+			Vec3 direction = Vec3(target - ch->GetPosition()).NormalizedOr(Vec3::sZero());
+			direction.SetY(0);
+			new_velocity += (5.0f + 0.5f * speed) * direction;
+			ch->SetLinearVelocity(new_velocity);
+
+			// Update the character position
+			CharacterVirtual::ExtendedUpdateSettings update_settings;
+			ch->ExtendedUpdate(inDeltaTime,
+				inPhysicsSystem.GetGravity(),
+				update_settings,
+				inPhysicsSystem.GetDefaultBroadPhaseLayerFilter(Layers::MOVING),
+				inPhysicsSystem.GetDefaultLayerFilter(Layers::MOVING),
+				{ },
+				{ },
+				ioTempAllocator);
+
+			++count;
+		}
+	}
+
+	virtual void			UpdateHash(uint64 &ioHash) const override
+	{
+		// Hash the contact callback hash
+		HashCombine(ioHash, mHash);
+
+		// Hash the state of all characters
+		for (const CharacterVirtual *ch : mCharacters)
+			HashCombine(ioHash, ch->GetPosition());
+	}
+
+	virtual void			StopTest(PhysicsSystem &inPhysicsSystem) override
+	{
+		for (const CharacterVirtual *ch : mCharacters)
+			mCharacterVsCharacterCollision.Remove(ch);
+		mCharacters.clear();
+	}
+
+	// See: CharacterContactListener
+	virtual void			OnContactAdded(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override
+	{
+		HashCombine(mHash, 1);
+		HashCombine(mHash, inCharacter->GetID());
+		HashCombine(mHash, inBodyID2);
+		HashCombine(mHash, inSubShapeID2.GetValue());
+		HashCombine(mHash, inContactPosition);
+		HashCombine(mHash, inContactNormal);
+	}
+	virtual void			OnContactPersisted(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override
+	{
+		HashCombine(mHash, 2);
+		HashCombine(mHash, inCharacter->GetID());
+		HashCombine(mHash, inBodyID2);
+		HashCombine(mHash, inSubShapeID2.GetValue());
+		HashCombine(mHash, inContactPosition);
+		HashCombine(mHash, inContactNormal);
+	}
+	virtual void			OnContactRemoved(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2) override
+	{
+		HashCombine(mHash, 3);
+		HashCombine(mHash, inCharacter->GetID());
+		HashCombine(mHash, inBodyID2);
+		HashCombine(mHash, inSubShapeID2.GetValue());
+	}
+	virtual void			OnCharacterContactAdded(const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override
+	{
+		HashCombine(mHash, 4);
+		HashCombine(mHash, inCharacter->GetID());
+		HashCombine(mHash, inOtherCharacter->GetID());
+		HashCombine(mHash, inSubShapeID2.GetValue());
+		HashCombine(mHash, inContactPosition);
+		HashCombine(mHash, inContactNormal);
+	}
+	virtual void			OnCharacterContactPersisted(const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override
+	{
+		HashCombine(mHash, 5);
+		HashCombine(mHash, inCharacter->GetID());
+		HashCombine(mHash, inOtherCharacter->GetID());
+		HashCombine(mHash, inSubShapeID2.GetValue());
+		HashCombine(mHash, inContactPosition);
+		HashCombine(mHash, inContactNormal);
+	}
+	virtual void			OnCharacterContactRemoved(const CharacterVirtual *inCharacter, const CharacterID &inOtherCharacterID, const SubShapeID &inSubShapeID2) override
+	{
+		HashCombine(mHash, 6);
+		HashCombine(mHash, inCharacter->GetID());
+		HashCombine(mHash, inOtherCharacterID);
+		HashCombine(mHash, inSubShapeID2.GetValue());
+	}
+
+private:
+	static constexpr int	cNumCharactersX = 10;
+	static constexpr int	cNumCharactersY = 10;
+	static constexpr float	cCharacterHeightStanding = 1.35f;
+	static constexpr float	cCharacterRadiusStanding = 0.3f;
+	static constexpr float	cInnerShapeFraction = 0.9f;
+	static constexpr float	cStairsStepHeight = 0.3f;
+
+	float					mTime = 0.0f;
+	uint64					mHash = 0;
+	Array<BodyCreationSettings> mWorld;
+	Array<Ref<CharacterVirtual>> mCharacters;
+	CharacterVsCharacterCollisionSimple mCharacterVsCharacterCollision;
+};

+ 1 - 0
PerformanceTest/PerformanceTest.cmake

@@ -9,6 +9,7 @@ set(PERFORMANCE_TEST_SRC_FILES
 	${PERFORMANCE_TEST_ROOT}/PerformanceTestScene.h
 	${PERFORMANCE_TEST_ROOT}/RagdollScene.h
 	${PERFORMANCE_TEST_ROOT}/ConvexVsMeshScene.h
+	${PERFORMANCE_TEST_ROOT}/CharacterVirtualScene.h
 	${PERFORMANCE_TEST_ROOT}/LargeMeshScene.h
 	${PERFORMANCE_TEST_ROOT}/Layers.h
 )

+ 9 - 0
PerformanceTest/PerformanceTest.cpp

@@ -45,6 +45,7 @@ JPH_SUPPRESS_WARNINGS
 #include "ConvexVsMeshScene.h"
 #include "PyramidScene.h"
 #include "LargeMeshScene.h"
+#include "CharacterVirtualScene.h"
 
 // Time step for physics
 constexpr float cDeltaTime = 1.0f / 60.0f;
@@ -116,6 +117,8 @@ int main(int argc, char** argv)
 				scene = unique_ptr<PerformanceTestScene>(new PyramidScene);
 			else if (strcmp(arg + 3, "LargeMesh") == 0)
 				scene = unique_ptr<PerformanceTestScene>(new LargeMeshScene);
+			else if (strcmp(arg + 3, "CharacterVirtual") == 0)
+				scene = unique_ptr<PerformanceTestScene>(new CharacterVirtualScene);
 			else
 			{
 				Trace("Invalid scene");
@@ -367,6 +370,9 @@ int main(int argc, char** argv)
 					// Start measuring
 					chrono::high_resolution_clock::time_point clock_start = chrono::high_resolution_clock::now();
 
+					// Update the test
+					scene->UpdateTest(physics_system, temp_allocator, cDeltaTime);
+
 					// Do a physics step
 					physics_system.Update(cDeltaTime, 1, &temp_allocator, &job_system);
 
@@ -454,6 +460,9 @@ int main(int argc, char** argv)
 					hash = HashBytes(&rot, sizeof(Quat), hash);
 				}
 
+				// Let the scene hash its own state
+				scene->UpdateHash(hash);
+
 				// Convert hash to string
 				stringstream hash_stream;
 				hash_stream << "0x" << hex << hash << dec;

+ 6 - 0
PerformanceTest/PerformanceTestScene.h

@@ -20,6 +20,12 @@ public:
 	// Start a new test by adding objects to inPhysicsSystem
 	virtual void			StartTest(PhysicsSystem &inPhysicsSystem, EMotionQuality inMotionQuality) = 0;
 
+	// Step the test
+	virtual void			UpdateTest([[maybe_unused]] PhysicsSystem &inPhysicsSystem, [[maybe_unused]] TempAllocator &ioTempAllocator, [[maybe_unused]] float inDeltaTime) { }
+
+	// Update the hash with the state of the scene
+	virtual void			UpdateHash([[maybe_unused]] uint64 &ioHash) const	{ }
+
 	// Stop a test and remove objects from inPhysicsSystem
 	virtual void			StopTest(PhysicsSystem &inPhysicsSystem)			{ }
 };