// Copyright Epic Games, Inc. All Rights Reserved. #include "HeadlessChaosTestUtility.h" #include "Chaos/ParticleHandle.h" #include "Chaos/ErrorReporter.h" #include "PhysicsProxy/SingleParticlePhysicsProxy.h" #include "PhysicsProxy/GeometryCollectionPhysicsProxy.h" #include "Chaos/Utilities.h" #include "PBDRigidsSolver.h" #include "ChaosSolversModule.h" #include "Modules/ModuleManager.h" #include "Chaos/ChaosEngineInterface.h" #include "Chaos/ChaosScene.h" #include "SQAccelerator.h" #include "CollisionQueryFilterCallbackCore.h" #include "BodyInstanceCore.h" namespace Chaos { extern CHAOS_API float AsyncInterpolationMultiplier; } namespace ChaosTest { using namespace Chaos; using namespace ChaosInterface; // Returns true on raycast if we hit payload bounds. struct FSimpleRaycastVisitor: ISpatialVisitor { using FPayload = FAccelerationStructureHandle; FVec3 Start; bool bHit; bool bQueryGameThread; // Query game thread or physics thread data? bool bUseQueryFilter; FCollisionFilterData FilterData; FSimpleRaycastVisitor(const FVec3& InStart, bool bInQueryGameThread) : Start(InStart) , bHit(false) , bQueryGameThread(bInQueryGameThread) { } FSimpleRaycastVisitor(const FVec3& InStart, FCollisionFilterData& InFilterData, bool bInQueryGameThread) : Start(InStart) , bHit(false) , bQueryGameThread(bInQueryGameThread) , bUseQueryFilter(true) , FilterData(InFilterData) { } virtual const void* GetQueryData() const override { if (bUseQueryFilter) { return &FilterData; } return nullptr; } enum class SQType { Raycast, Sweep, Overlap }; bool VisitRaycast(const TSpatialVisitorData& Data, FQueryFastData& CurData) { FReal OutTime = 0; FVec3 OutPos; FVec3 OutNorm; int32 FaceIdx; if (Data.Bounds.Raycast(Start, CurData.Dir, CurData.CurrentLength, 0, OutTime, OutPos, OutNorm, FaceIdx)) { if (bQueryGameThread) { FTransform ParticleTransform(Data.Payload.GetExternalGeometryParticle_ExternalThread()->R(), Data.Payload.GetExternalGeometryParticle_ExternalThread()->GetX()); const FVec3 DirLocal = ParticleTransform.InverseTransformVectorNoScale(CurData.Dir); const FVec3 StartLocal = ParticleTransform.InverseTransformPositionNoScale(Start); bHit = Data.Payload.GetExternalGeometryParticle_ExternalThread()->GetGeometry()->Raycast(StartLocal, DirLocal, CurData.CurrentLength, 0, OutTime, OutPos, OutNorm, FaceIdx); } else { FTransform ParticleTransform(Data.Payload.GetGeometryParticleHandle_PhysicsThread()->GetR(), Data.Payload.GetGeometryParticleHandle_PhysicsThread()->GetX()); const FVec3 DirLocal = ParticleTransform.InverseTransformVectorNoScale(CurData.Dir); const FVec3 StartLocal = ParticleTransform.InverseTransformPositionNoScale(Start); bHit = Data.Payload.GetGeometryParticleHandle_PhysicsThread()->GetGeometry()->Raycast(StartLocal, DirLocal, CurData.CurrentLength, 0, OutTime, OutPos, OutNorm, FaceIdx); } if (bHit) { return false; } } return true; } bool VisitSweep(const TSpatialVisitorData& Data, FQueryFastData& CurData) { check(false); return false; } bool VisitOverlap(const TSpatialVisitorData& Data) { check(false); return false; } virtual bool Overlap(const TSpatialVisitorData& Instance) override { check(false); return false; } virtual bool Raycast(const TSpatialVisitorData& Instance, FQueryFastData& CurData) override { return VisitRaycast(Instance, CurData); } virtual bool Sweep(const TSpatialVisitorData& Instance, FQueryFastData& CurData) override { check(false); return false; } }; FSQHitBuffer InSphereHelper(const FChaosScene& Scene, const FTransform& InTM, const FReal Radius) { FChaosSQAccelerator SQAccelerator(*Scene.GetSpacialAcceleration()); FSQHitBuffer HitBuffer; FOverlapAllQueryCallback QueryCallback; SQAccelerator.Overlap(Chaos::FSphere(FVec3(0), Radius), InTM, HitBuffer, FChaosQueryFilterData(), QueryCallback, FQueryDebugParams()); return HitBuffer; } GTEST_TEST(EngineInterface, CreateAndReleaseActor) { FChaosScene Scene(nullptr, /*AsyncDt=*/-1); FActorCreationParams Params; Params.Scene = &Scene; FPhysicsActorHandle Proxy = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy); auto& Particle = Proxy->GetGameThreadAPI(); EXPECT_NE(Proxy, nullptr); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Proxy->GetGameThreadAPI().SetGeometry(Sphere); } FChaosEngineInterface::ReleaseActor(Proxy, &Scene); EXPECT_EQ(Proxy, nullptr); } GTEST_TEST(EngineInterface, CreateMoveAndReleaseInScene) { FChaosScene Scene(nullptr, /*AsyncDt=*/-1); FActorCreationParams Params; Params.Scene = &Scene; FPhysicsActorHandle Proxy = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy); auto& Particle = Proxy->GetGameThreadAPI(); EXPECT_NE(Proxy, nullptr); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle.SetGeometry(Sphere); } TArray Proxys = { Proxy }; Scene.AddActorsToScene_AssumesLocked(Proxys); //make sure acceleration structure has new actor right away { const auto HitBuffer = InSphereHelper(Scene, FTransform::Identity, 3); EXPECT_EQ(HitBuffer.GetNumHits(), 1); } //make sure acceleration structure sees moved actor right away const FTransform MovedTM(FQuat::Identity, FVec3(100, 0, 0)); FChaosEngineInterface::SetGlobalPose_AssumesLocked(Proxy, MovedTM); { const auto HitBuffer = InSphereHelper(Scene, FTransform::Identity, 3); EXPECT_EQ(HitBuffer.GetNumHits(), 0); const auto HitBuffer2 = InSphereHelper(Scene, MovedTM, 3); EXPECT_EQ(HitBuffer2.GetNumHits(), 1); } //move actor back and acceleration structure sees it right away FChaosEngineInterface::SetGlobalPose_AssumesLocked(Proxy, FTransform::Identity); { const auto HitBuffer = InSphereHelper(Scene, FTransform::Identity, 3); EXPECT_EQ(HitBuffer.GetNumHits(), 1); } FChaosEngineInterface::ReleaseActor(Proxy, &Scene); EXPECT_EQ(Proxy, nullptr); //make sure acceleration structure no longer has actor { const auto HitBuffer = InSphereHelper(Scene, FTransform::Identity, 3); EXPECT_EQ(HitBuffer.GetNumHits(), 0); } } template void AdvanceSolverNoPushHelper(TSolver* Solver, FReal Dt) { Solver->AdvanceSolverBy(Dt); } GTEST_TEST(EngineInterface, AccelerationStructureHasSyncTimestamp) { //make sure acceleration structure has appropriate sync time FChaosScene Scene(nullptr, /*AsyncDt=*/-1); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 0); //timestamp of 0 because we flush when scene is created FReal TotalDt = 0; for (int Step = 1; Step < 10; ++Step) { FVec3 Grav(0,0,-1); Scene.SetUpForFrame(&Grav, 1,0,99999,99999,10,false); Scene.StartFrame(); Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration(); //make sure we get a new tree every step Scene.EndFrame(); EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), Step - 1); } } GTEST_TEST(EngineInterface, AccelerationStructureHasSyncTimestamp_MultiFrameDelay) { //make sure acceleration structure has appropriate sync time when PT falls behind GT FChaosScene Scene(nullptr, /*AsyncDt=*/-1); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); Scene.GetSolver()->SetStealAdvanceTasks_ForTesting(true); // prevents execution on StartFrame so we can execute task manually. EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 0); //timestamp of 0 because we flush when scene is created FVec3 Grav(0, 0, -1); Scene.SetUpForFrame(&Grav, 1, 0, 99999, 99999, 10, false); // Game thread enqueues second solver task before first completes (we did not execute advance task) Scene.StartFrame(); Scene.EndFrame(); Scene.StartFrame(); // Execute first enqueued advance task Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting(); Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration(); Scene.EndFrame(); // Still timestamp 0, as we have only processed first PT step.. EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 0); Scene.StartFrame(); // PT catches up during this frame Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting(); Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting(); Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration(); Scene.EndFrame(); // New structure should be at 2, 3 steps have been processed, PT/GT are in sync. EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 2); } GTEST_TEST(EngineInterface, AccelerationStructureHasSyncTimestamp_MultiFrameDelay2) { //make sure acceleration structure has appropriate sync time when PT falls behind GT FChaosScene Scene(nullptr, /*AsyncDt=*/-1); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); Scene.GetSolver()->SetStealAdvanceTasks_ForTesting(true); // prevents execution on StartFrame so we can execute task manually. EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 0); //timestamp of 0 because we flush when scene is created FVec3 Grav(0,0,-1); Scene.SetUpForFrame(&Grav, 1,0,99999,99999,10,false); // PT not finished yet (we didn't execute solver task), should still be 0. Scene.StartFrame(); Scene.EndFrame(); EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 0); // PT not finished yet (we didn't execute solver task), should still be 0. Scene.StartFrame(); Scene.EndFrame(); EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 0); // First PT task finished this frame, we are two behind, still at 0 as structure is from first GT input (timestamp 0). Scene.StartFrame(); Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting(); Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration(); Scene.EndFrame(); EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 0); Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting(); Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting(); Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration(); // Remaining two PT tasks finish, we are caught up, GT is still time 0 as EndFrame has not updated our structure. EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 0); // Popping acceleration structures from physics thread will give us timestamp of 2. (3 total GT inputs processed) Scene.CopySolverAccelerationStructure(); EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 2); // PT task this frame finishes before EndFrame, putting us at 3, in sync with GT. Scene.StartFrame(); Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting(); Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration(); Scene.EndFrame(); EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 3); } GTEST_TEST(EngineInterface, PullFromPhysicsState_MultiFrameDelay) { // This test is designed to verify pulldata is being timestamped correctly, and that we will not write to a deleted GT Proxy // in this case. FChaosScene Scene(nullptr, /*AsyncDt=*/-1); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); Scene.GetSolver()->SetStealAdvanceTasks_ForTesting(true); // prevents execution on StartFrame so we can execute task manually. FVec3 Grav(0,0,-1); Scene.SetUpForFrame(&Grav, 1,0,99999,99999,10,false); FActorCreationParams Params; Params.Scene = &Scene; Params.bSimulatePhysics = true; Params.bEnableGravity = true; Params.bStartAwake = true; // Create two Proxys, one to remove for test, the other to ensure we have > 0 proxies to hit the pull physics data path. FPhysicsActorHandle Proxy = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy); auto& Particle = Proxy->GetGameThreadAPI(); EXPECT_NE(Proxy, nullptr); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle.SetGeometry(Sphere); } FPhysicsActorHandle Proxy2 = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy2); auto& Particle2 = Proxy2->GetGameThreadAPI(); EXPECT_NE(Proxy2, nullptr); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle2.SetGeometry(Sphere); } TArray Proxys = { Proxy, Proxy2 }; Scene.AddActorsToScene_AssumesLocked(Proxys); // verify external timestamps are as expected. auto& MarshallingManager = Scene.GetSolver()->GetMarshallingManager(); EXPECT_EQ(MarshallingManager.GetExternalTimestamp_External(), 0); // Execute a frame such that Proxys should be initialized in physics thread and game thread. Scene.StartFrame(); EXPECT_EQ(MarshallingManager.GetExternalTimestamp_External(), 1); Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting(); Scene.EndFrame(); // run GT frame, no PT task executed. Scene.StartFrame(); EXPECT_EQ(MarshallingManager.GetExternalTimestamp_External(), 2); Scene.EndFrame(); // enqueue another frame. Scene.StartFrame(); EXPECT_EQ(MarshallingManager.GetExternalTimestamp_External(), 3); // Remove Proxy, is stamped with external time 3. PT needs to run 3 frames before this will be removed, // as we are two PT tasks behind, and this has not been enqueued yet. auto StaleProxy = Proxy; FChaosEngineInterface::ReleaseActor(Proxy, &Scene); EXPECT_EQ(Proxy, nullptr); EXPECT_EQ(StaleProxy->GetSyncTimestamp()->bDeleted, true); // Run PT task for internal timestamp 1. Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting(); // Proxy should not get touched in Pull, as timestamp from removal should be greater than pulldata timestamp. // (if it was touched we'd crash as it is now deleted). Scene.EndFrame(); Scene.StartFrame(); EXPECT_EQ(MarshallingManager.GetExternalTimestamp_External(), 4); EXPECT_EQ(StaleProxy->GetSyncTimestamp()->bDeleted, true); // run pt task for internal timestamp 3. Proxy still not removed on PT. Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting(); EXPECT_EQ(Scene.GetSolver()->GetEvolution()->GetParticles().GetAllParticlesView().Num(), 2); // none have been removed on pt, still 2 Proxys. // Proxy should not get touched in pull, as timestamp from removal is less than pulldata timestamp (3 < 4) // If this crashes in pull, that means this test has regressed. (Pulldata timestamp is likely wrong). Scene.EndFrame(); Scene.StartFrame(); EXPECT_EQ(MarshallingManager.GetExternalTimestamp_External(), 5); EXPECT_EQ(StaleProxy->GetSyncTimestamp()->bDeleted, true); EXPECT_EQ(Scene.GetSolver()->GetEvolution()->GetParticles().GetAllParticlesView().Num(), 2); // Proxys not yet removed on pt, still 2. // This is PT task that should remove Proxy (internal timestamp 4, matching stamp on removed Proxy's dirty data). Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting(); EXPECT_EQ(Scene.GetSolver()->GetEvolution()->GetParticles().GetAllParticlesView().Num(), 1); // one Proxy removed on pt, one remaining. // This PT task catches up to gamethread. Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting(); Scene.EndFrame(); } GTEST_TEST(EngineInterface, UpdatingAccelerationStructurePrePreFilterOnShapeFilterChange) { const float PhysicsTimestep = 1; // 1 second FChaosScene Scene(nullptr, PhysicsTimestep); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); float DeltaSeconds = PhysicsTimestep; FVec3 Grav(0, 0, -1); Scene.SetUpForFrame(&Grav, DeltaSeconds, 0, 9999, 9999, 9999, false); // Raycast params, aimed to hit our particle at (0,0,0) const FVector Start(0, 0, -5); const FVector Dir(0, 0, 1); const float Length = 50; // Init kinematic particle, sphere radius 3 FActorCreationParams Params; Params.Scene = &Scene; Params.bSimulatePhysics = false; Params.bEnableGravity = true; Params.bStartAwake = true; FPhysicsActorHandle Proxy = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy); auto& Particle = Proxy->GetGameThreadAPI(); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle.SetGeometry(Sphere); } TArray Proxys = { Proxy }; Scene.AddActorsToScene_AssumesLocked(Proxys); // Execute a whole frame such that particle is initialized on physics thread Scene.StartFrame(); Scene.EndFrame(); // Make query filter that will allow query against particle that blocks/touches all channels. // Filter will fail against particle that has no query allowed (default query filter). FCollisionFilterData QueryFilter; QueryFilter.Word0 = 1; // Setting to non-zero to set query type that will filter // This is setting a somewhat arbritrary trace channels. It's very hard to make sense of these bitfields at this level of API. // Below particle uses a filter that touches/blocks anything, so these bits are enough to make filter pass. QueryFilter.Word3 = 7 << 21; // Get collision data off shape for (const TUniquePtr& Shape : Particle.ShapesArray()) { const FCollisionData& CollisionData = Shape->GetCollisionData(); EXPECT_EQ(CollisionData.QueryData.Word0, 0); // ensure query filter is defaulted to 0 (no query allowed at all) // Verify query is filtered out with default collision data on shape bool bFiltered = PrePreQueryFilterImp(QueryFilter, CollisionData.QueryData); EXPECT_EQ(bFiltered, true); } // Query against particle on game thread, should fail to hit due to particle filter being defaulted, no touch/block set. { bool bQueryGameThread = true; FSimpleRaycastVisitor Visitor(Start, QueryFilter, bQueryGameThread); Scene.GetSpacialAcceleration()->Raycast(Start, Dir, Length, Visitor); EXPECT_EQ(Visitor.bHit, false); } // Change filter data on game thread to contain touch/block on all channels. FCollisionFilterData NewParticleQueryFilter; NewParticleQueryFilter.Word1 = TNumericLimits::Max(); NewParticleQueryFilter.Word2 = TNumericLimits::Max(); for (const TUniquePtr& Shape : Particle.ShapesArray()) { const FCollisionData& CollisionData = Shape->GetCollisionData(); // Update filter FCollisionData NewCollisionData = CollisionData; NewCollisionData.QueryData = NewParticleQueryFilter; Shape->SetCollisionData(NewCollisionData); // Filter with new data, ensuring we pass and are not filtered out. bool bFiltered = PrePreQueryFilterImp(QueryFilter, NewCollisionData.QueryData); EXPECT_EQ(bFiltered, false); } // Update particle in GT accel structure so cached PrePreFilter updates Scene.UpdateActorInAccelerationStructure(Proxy); // Query against particle on game thread, should hit with new filter. { bool bQueryGameThread = true; FSimpleRaycastVisitor Visitor(Start, QueryFilter, bQueryGameThread); Scene.GetSpacialAcceleration()->Raycast(Start, Dir, Length, Visitor); EXPECT_EQ(Visitor.bHit, true); } // Tick to push to physics thread Scene.StartFrame(); Scene.EndFrame(); // Query particle on physics thread, expected to hit with new filter. // If this fails it means we did not update cached filter data in acceleration structure entry. { bool bQueryGameThread = false; FSimpleRaycastVisitor Visitor(Start, QueryFilter, bQueryGameThread); Scene.GetSolver()->GetEvolution()->GetSpatialAcceleration()->Raycast(Start, Dir, Length, Visitor); EXPECT_EQ(Visitor.bHit, true); } } // Disabled until we move fix with kineamtic bounds update on PushToPhysicsState into this branch. Might also need to remove bounds computation in ApplyKinematicTarget. GTEST_TEST(EngineInterface, DISABLED_KinematicTargetsPassingGTWrongAccelBoundsBeforeHittingTarget) { // This test is designed to catch an edge case with kinematic targets (or other things interpolated over multiple physics steps), and acceleration structure bounds. // Timestep is setup such that 1 GT frame = 10 physics steps, we have to make sure that if a non-final step gives an acceleration structure to game thread, in which // kinematic has not reached target yet, that the bounds in structure representing interpolated position do not make it to game thread, otherwise game thread has // position at target, but bounds that don't match. const float PhysicsTimestep = 1; // 1 second // Setup solver so we can manually execute each physics step. FChaosScene Scene(nullptr, PhysicsTimestep); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); Scene.GetSolver()->SetStealAdvanceTasks_ForTesting(true); // In this test we have a 10s Dt, split into 10 physics steps of 1s. const int32 PhysicsStepsInFrame = 10; float DeltaSeconds = PhysicsTimestep * PhysicsStepsInFrame; FVec3 Grav(0, 0, -1); Scene.SetUpForFrame(&Grav, DeltaSeconds, 0, 9999, 9999, 9999, false); // Raycast params, aimed to hit our kinematic target (10,0,0) const FVector Start(10, 0, -5); const FVector Dir(0, 0,1); const float Length = 50; // Init kinematic particle, sphere radius 3 FActorCreationParams Params; Params.Scene = &Scene; Params.bSimulatePhysics = false; Params.bEnableGravity = true; Params.bStartAwake = true; FPhysicsActorHandle Proxy = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy); auto& Particle = Proxy->GetGameThreadAPI(); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle.SetGeometry(Sphere); } TArray Proxys = { Proxy }; Scene.AddActorsToScene_AssumesLocked(Proxys); // Execute a whole frame such that particle is initialized on physics thread Scene.StartFrame(); for (int32 PhysicsTicks = 0; PhysicsTicks < PhysicsStepsInFrame; ++PhysicsTicks) { // Tick each physics step generated from game thread input Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting(); } Scene.EndFrame(); // Set kinematic target to (10,0,0) on game thread FTransform Target(FVector(10, 0, 0)); FChaosEngineInterface::SetKinematicTarget_AssumesLocked(Proxy, Target); // Confirm particle is at target on game thread with raycast. { FSimpleRaycastVisitor Visitor(Start, true); Scene.GetSpacialAcceleration()->Raycast(Start, Dir, Length, Visitor); EXPECT_EQ(Visitor.bHit, true); } // Tick game thread again, this enqueues 10 physics steps, kinematic will interpolate // to target on physics thread over duration of these 10 steps. Scene.StartFrame(); for (int32 PhysicsTick = 0; PhysicsTick < PhysicsStepsInFrame; ++PhysicsTick) { Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting(); if (PhysicsTick == 2) { // On this arbritrary tick, copy acceleration structure to game thread, // at this point we have sim'd only some of the physics steps for this frame. // kinematic target is still interpolating, has not reached target of (10,0,0) yet. // When this was broken this would give game thread a structure with // the bounds of interpolated position (which is wrong because game thread particle is at target!) Scene.CopySolverAccelerationStructure(); // Verify the game thread particle can still be queried at target (verifying bounds and particle position are still correct) { FSimpleRaycastVisitor Visitor(Start, true); Scene.GetSpacialAcceleration()->Raycast(Start, Dir, Length, Visitor); EXPECT_EQ(Visitor.bHit, true); } } } // Finish frame Scene.EndFrame(); // Verify can still query game thread particle at our target. { FSimpleRaycastVisitor Visitor(Start, true); Scene.GetSpacialAcceleration()->Raycast(Start, Dir, Length, Visitor); EXPECT_EQ(Visitor.bHit, true); } } GTEST_TEST(EngineInterface, CreateActorPostFlush) { FChaosScene Scene(nullptr, /*AsyncDt=*/-1); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); FActorCreationParams Params; Params.Scene = &Scene; FPhysicsActorHandle Proxy = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy); auto& Particle = Proxy->GetGameThreadAPI(); EXPECT_NE(Proxy, nullptr); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle.SetGeometry(Sphere); } //tick solver but don't call EndFrame (want to flush and swap manually) { FVec3 Grav(0,0,-1); Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false); Scene.StartFrame(); } //make sure acceleration structure is built Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration(); //create actor after structure is finished, but before swap happens TArray Proxys = { Proxy }; Scene.AddActorsToScene_AssumesLocked(Proxys); Scene.CopySolverAccelerationStructure(); //trigger swap manually and see pending changes apply { const auto HitBuffer = InSphereHelper(Scene, FTransform::Identity, 3); EXPECT_EQ(HitBuffer.GetNumHits(), 1); } } GTEST_TEST(EngineInterface, MoveActorPostFlush) { FChaosScene Scene(nullptr, /*AsyncDt=*/-1); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); FActorCreationParams Params; Params.Scene = &Scene; FPhysicsActorHandle Proxy = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy); auto& Particle = Proxy->GetGameThreadAPI(); EXPECT_NE(Proxy, nullptr); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle.SetGeometry(Sphere); } //create actor before structure is ticked TArray Proxys = { Proxy }; Scene.AddActorsToScene_AssumesLocked(Proxys); //tick solver so that Proxy is created, but don't call EndFrame (want to flush and swap manually) { FVec3 Grav(0,0,-1); Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false); Scene.StartFrame(); } //make sure acceleration structure is built Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration(); //move object to get hit (shows pending move is applied) FChaosEngineInterface::SetGlobalPose_AssumesLocked(Proxy, FTransform(FRotation3::FromIdentity(), FVec3(100, 0, 0))); Scene.CopySolverAccelerationStructure(); //trigger swap manually and see pending changes apply { TRigidTransform OverlapTM(FVec3(100, 0, 0), FRotation3::FromIdentity()); const auto HitBuffer = InSphereHelper(Scene, OverlapTM, 3); EXPECT_EQ(HitBuffer.GetNumHits(), 1); } } GTEST_TEST(EngineInterface, RemoveActorPostFlush) { FChaosScene Scene(nullptr, /*AsyncDt=*/-1); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); FActorCreationParams Params; Params.Scene = &Scene; FPhysicsActorHandle Proxy = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy); auto& Particle = Proxy->GetGameThreadAPI(); EXPECT_NE(Proxy, nullptr); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle.SetGeometry(Sphere); } //create actor before structure is ticked TArray Proxys = { Proxy }; Scene.AddActorsToScene_AssumesLocked(Proxys); //tick solver so that Proxy is created, but don't call EndFrame (want to flush and swap manually) { FVec3 Grav(0,0,-1); Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false); Scene.StartFrame(); } //make sure acceleration structure is built Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration(); //delete object to get no hit FChaosEngineInterface::ReleaseActor(Proxy, &Scene); Scene.CopySolverAccelerationStructure(); //trigger swap manually and see pending changes apply { const auto HitBuffer = InSphereHelper(Scene, FTransform::Identity, 3); EXPECT_EQ(HitBuffer.GetNumHits(), 0); } } GTEST_TEST(EngineInterface, RemoveActorPostFlush0Dt) { FChaosScene Scene(nullptr, /*AsyncDt=*/-1); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); FActorCreationParams Params; Params.Scene = &Scene; FPhysicsActorHandle Proxy = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy); auto& Particle = Proxy->GetGameThreadAPI(); EXPECT_NE(Proxy, nullptr); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle.SetGeometry(Sphere); } //create actor before structure is ticked TArray Proxys = { Proxy }; Scene.AddActorsToScene_AssumesLocked(Proxys); //tick solver so that Proxy is created, but don't call EndFrame (want to flush and swap manually) { //use 0 dt to make sure pending operations are not sensitive to 0 dt FVec3 Grav(0,0,-1); Scene.SetUpForFrame(&Grav,0,0,99999,99999,10,false); Scene.StartFrame(); } //make sure acceleration structure is built Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration(); //delete object to get no hit FChaosEngineInterface::ReleaseActor(Proxy, &Scene); Scene.CopySolverAccelerationStructure(); //trigger swap manually and see pending changes apply { const auto HitBuffer = InSphereHelper(Scene, FTransform::Identity, 3); EXPECT_EQ(HitBuffer.GetNumHits(), 0); } } GTEST_TEST(EngineInterface, CreateAndRemoveActorPostFlush) { FChaosScene Scene(nullptr, /*AsyncDt=*/-1); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); FActorCreationParams Params; Params.Scene = &Scene; FPhysicsActorHandle Proxy = nullptr; //tick solver, but don't call EndFrame (want to flush and swap manually) { FVec3 Grav(0,0,-1); Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false); Scene.StartFrame(); } //make sure acceleration structure is built Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration(); FChaosEngineInterface::CreateActor(Params, Proxy); auto& Particle = Proxy->GetGameThreadAPI(); EXPECT_NE(Proxy, nullptr); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle.SetGeometry(Sphere); } //create actor after flush TArray Proxys = { Proxy }; Scene.AddActorsToScene_AssumesLocked(Proxys); //delete object right away to get no hit FChaosEngineInterface::ReleaseActor(Proxy, &Scene); Scene.CopySolverAccelerationStructure(); //trigger swap manually and see pending changes apply { const auto HitBuffer = InSphereHelper(Scene, FTransform::Identity, 3); EXPECT_EQ(HitBuffer.GetNumHits(), 0); } } GTEST_TEST(EngineInterface, CreateDelayed) { for (int Delay = 0; Delay < 4; ++Delay) { FChaosScene Scene(nullptr, /*AsyncDt=*/-1); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); Scene.GetSolver()->GetMarshallingManager().SetTickDelay_External(Delay); FActorCreationParams Params; Params.Scene = &Scene; FPhysicsActorHandle Proxy = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy); auto& Particle = Proxy->GetGameThreadAPI(); EXPECT_NE(Proxy, nullptr); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle.SetGeometry(Sphere); } //create actor after flush TArray Proxys = { Proxy }; Scene.AddActorsToScene_AssumesLocked(Proxys); for (int Repeat = 0; Repeat < Delay; ++Repeat) { //tick solver { FVec3 Grav(0,0,-1); Scene.SetUpForFrame(&Grav,1,0,99999,99999,1,false); Scene.StartFrame(); Scene.EndFrame(); } //make sure sim hasn't seen it yet { FPBDRigidsEvolution* Evolution = Scene.GetSolver()->GetEvolution(); const auto& SOA = Evolution->GetParticles(); EXPECT_EQ(SOA.GetAllParticlesView().Num(), 0); } //make sure external thread knows about it { const auto HitBuffer = InSphereHelper(Scene, FTransform::Identity, 3); EXPECT_EQ(HitBuffer.GetNumHits(), 1); } } //tick solver one last time { FVec3 Grav(0,0,-1); Scene.SetUpForFrame(&Grav,1,0,99999,99999,1,false); Scene.StartFrame(); Scene.EndFrame(); } //now sim knows about it { FPBDRigidsEvolution* Evolution = Scene.GetSolver()->GetEvolution(); const auto& SOA = Evolution->GetParticles(); EXPECT_EQ(SOA.GetAllParticlesView().Num(), 1); } Particle.SetX(FVec3(5, 0, 0)); for (int Repeat = 0; Repeat < Delay; ++Repeat) { //tick solver { FVec3 Grav(0,0,-1); Scene.SetUpForFrame(&Grav,1,0,99999,99999,1,false); Scene.StartFrame(); Scene.EndFrame(); } //make sure sim hasn't seen new X yet { FPBDRigidsEvolution* Evolution = Scene.GetSolver()->GetEvolution(); const auto& SOA = Evolution->GetParticles(); const auto& InternalProxy = *SOA.GetAllParticlesView().Begin(); EXPECT_EQ(InternalProxy.GetX()[0], 0); } } //tick solver one last time { FVec3 Grav(0,0,-1); Scene.SetUpForFrame(&Grav,1,0,99999,99999,1,false); Scene.StartFrame(); Scene.EndFrame(); } //now sim knows about new X { FPBDRigidsEvolution* Evolution = Scene.GetSolver()->GetEvolution(); const auto& SOA = Evolution->GetParticles(); const auto& InternalProxy = *SOA.GetAllParticlesView().Begin(); EXPECT_EQ(InternalProxy.GetX()[0], 5); } //make sure commands are also deferred int Count = 0; int ExternalCount = 0; TUniqueFunction Lambda = [&]() { ++Count; EXPECT_EQ(Count, 1); //only hit once on internal thread EXPECT_EQ(ExternalCount, Delay); //internal hits with expected delay }; Scene.GetSolver()->EnqueueCommandImmediate(Lambda); for (int Repeat = 0; Repeat < Delay + 1; ++Repeat) { //tick solver FVec3 Grav(0,0,-1); Scene.SetUpForFrame(&Grav,1,0,99999,99999,1,false); Scene.StartFrame(); Scene.EndFrame(); ++ExternalCount; } } } GTEST_TEST(EngineInterface, RemoveDelayed) { for (int Delay = 0; Delay < 4; ++Delay) { FChaosScene Scene(nullptr, /*AsyncDt=*/-1); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); Scene.GetSolver()->GetMarshallingManager().SetTickDelay_External(Delay); FActorCreationParams Params; Params.Scene = &Scene; Params.bSimulatePhysics = true; //simulate so that sync body is triggered Params.bStartAwake = true; FPhysicsActorHandle Proxy = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy); auto& Particle = Proxy->GetGameThreadAPI(); EXPECT_NE(Proxy, nullptr); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle.SetGeometry(Sphere); Particle.SetV(FVec3(0, 0, -1)); } //make second simulating Proxy that we don't delete. Needed to trigger a sync //this is because some data is cleaned up on GT immediately FPhysicsActorHandle Proxy2 = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy2); auto& Particle2 = Proxy2->GetGameThreadAPI(); EXPECT_NE(Proxy2, nullptr); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle2.SetGeometry(Sphere); Particle2.SetV(FVec3(0, -1, 0)); } //create actor TArray Proxys = { Proxy, Proxy2 }; Scene.AddActorsToScene_AssumesLocked(Proxys); //tick until it's being synced from sim for (int Repeat = 0; Repeat < Delay; ++Repeat) { { FVec3 Grav(0,0,0); Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false); Scene.StartFrame(); Scene.EndFrame(); } } //x starts at 0 EXPECT_NEAR(Particle.X()[2], 0, 1e-4); EXPECT_NEAR(Particle2.X()[1], 0, 1e-4); //tick solver and see new position synced from sim { FVec3 Grav(0,0,0); Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false); Scene.StartFrame(); Scene.EndFrame(); EXPECT_NEAR(Particle.X()[2], -1, 1e-4); EXPECT_NEAR(Particle2.X()[1], -1, 1e-4); } //tick solver and delete in between solver finishing and sync { FVec3 Grav(0,0,0); Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false); Scene.StartFrame(); //delete Proxy FChaosEngineInterface::ReleaseActor(Proxy, &Scene); Scene.EndFrame(); EXPECT_NEAR(Particle2.X()[1], -2, 1e-4); //other Proxy keeps moving } //tick again and don't crash for (int Repeat = 0; Repeat < Delay + 1; ++Repeat) { { FVec3 Grav(0,0,0); Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false); Scene.StartFrame(); Scene.EndFrame(); EXPECT_NEAR(Particle2.X()[1], -3 - Repeat, 1e-4); //other Proxy keeps moving } } } } GTEST_TEST(EngineInterface, MoveDelayed) { for (int Delay = 0; Delay < 4; ++Delay) { FChaosScene Scene(nullptr, /*AsyncDt=*/-1); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); Scene.GetSolver()->GetMarshallingManager().SetTickDelay_External(Delay); FActorCreationParams Params; Params.Scene = &Scene; Params.bSimulatePhysics = true; //simulated so that gt conflicts with sim thread Params.bStartAwake = true; FPhysicsActorHandle Proxy = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy); auto& Particle = Proxy->GetGameThreadAPI(); EXPECT_NE(Proxy, nullptr); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle.SetGeometry(Sphere); Particle.SetV(FVec3(0, 0, -1)); } //create actor TArray Proxys = { Proxy }; Scene.AddActorsToScene_AssumesLocked(Proxys); //tick until it's being synced from sim for (int Repeat = 0; Repeat < Delay; ++Repeat) { { FVec3 Grav(0,0,0); Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false); Scene.StartFrame(); Scene.EndFrame(); } } //x starts at 0 EXPECT_NEAR(Particle.X()[2], 0, 1e-4); //tick solver and see new position synced from sim { FVec3 Grav(0,0,0); Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false); Scene.StartFrame(); Scene.EndFrame(); EXPECT_NEAR(Particle.X()[2], -1, 1e-4); } //set new x position and make sure we see it right away even though there's delay FChaosEngineInterface::SetGlobalPose_AssumesLocked(Proxy, FTransform(FQuat::Identity, FVec3(0, 0, 10))); for (int Repeat = 0; Repeat < Delay; ++Repeat) { { FVec3 Grav(0,0,0); Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false); Scene.StartFrame(); Scene.EndFrame(); EXPECT_NEAR(Particle.X()[2], 10, 1e-4); //until we catch up, just use GT data } } //tick solver one last time, should see sim results from the place we teleported to { FVec3 Grav(0,0,0); Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false); Scene.StartFrame(); Scene.EndFrame(); EXPECT_NEAR(Particle.X()[2], 9, 1e-4); } //set x after sim but before EndFrame, make sure to see gt position since it was written after { FVec3 Grav(0,0,0); Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false); Scene.StartFrame(); FChaosEngineInterface::SetGlobalPose_AssumesLocked(Proxy, FTransform(FQuat::Identity, FVec3(0, 0, 100))); Scene.EndFrame(); EXPECT_NEAR(Particle.X()[2], 100, 1e-4); } for (int Repeat = 0; Repeat < Delay; ++Repeat) { { FVec3 Grav(0,0,0); Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false); Scene.StartFrame(); Scene.EndFrame(); EXPECT_NEAR(Particle.X()[2], 100, 1e-4); //until we catch up, just use GT data } } //tick solver one last time, should see sim results from the place we teleported to { FVec3 Grav(0,0,0); Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false); Scene.StartFrame(); Scene.EndFrame(); EXPECT_NEAR(Particle.X()[2], 99, 1e-4); } } } GTEST_TEST(EngineInterface, SimRoundTrip) { FChaosScene Scene(nullptr, /*AsyncDt=*/-1); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); FActorCreationParams Params; Params.Scene = &Scene; FPhysicsActorHandle Proxy = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy); auto& Particle = Proxy->GetGameThreadAPI(); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle.SetGeometry(Sphere); } TArray Proxys = { Proxy }; Scene.AddActorsToScene_AssumesLocked(Proxys); Particle.SetObjectState(EObjectStateType::Dynamic); Particle.AddForce(FVec3(0, 0, 10) * Particle.M()); FVec3 Grav(0,0,0); Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false); Scene.StartFrame(); Scene.EndFrame(); //integration happened and we get results back EXPECT_EQ(Particle.X(), FVec3(0, 0, 10)); EXPECT_EQ(Particle.V(), FVec3(0, 0, 10)); } GTEST_TEST(EngineInterface, SimInterpolated) { //Need to test: //position interpolation //position interpolation from an inactive Proxy (i.e a step function) //position interpolation from an active to an inactive Proxy (i.e a step function but reversed) //interpolation to a deleted Proxy //state change should be a step function (sleep state) //wake events must be collapsed (sleep awake sleep becomes sleep) //collision events must be collapsed //forces are averaged const FReal FixedDT = 1; FChaosScene Scene(nullptr, FixedDT); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); FActorCreationParams Params; Params.Scene = &Scene; FPhysicsActorHandle Proxy = nullptr; FPhysicsActorHandle Proxy2 = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy); auto& Particle = Proxy->GetGameThreadAPI(); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle.SetGeometry(Sphere); } Params.bSimulatePhysics = true; FChaosEngineInterface::CreateActor(Params, Proxy2); auto& Particle2 = Proxy2->GetGameThreadAPI(); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle2.SetGeometry(Sphere); } TArray Proxys = { Proxy, Proxy2 }; Scene.AddActorsToScene_AssumesLocked(Proxys); Particle.SetObjectState(EObjectStateType::Dynamic); const FReal ZVel = 10; const FReal ZStart = 100; const FVec3 ConstantForce(0, 0, 1 * Particle2.M()); Particle.SetV(FVec3(0, 0, ZVel)); Particle.SetX(FVec3(0, 0, ZStart)); const int32 NumGTSteps = 24; const int32 NumPTSteps = 24 / 4; struct FCallback : public TSimCallbackObject { virtual void OnPreSimulate_Internal() override { EXPECT_EQ(GetConsumerInput_Internal(), nullptr); //no inputs passed in //we expect the dt to be 1 EXPECT_EQ(GetDeltaTime_Internal(), 1); EXPECT_EQ(GetSimTime_Internal(), Count); Count++; } int32 Count = 0; int32 NumPTSteps; }; auto Callback = Scene.GetSolver()->CreateAndRegisterSimCallbackObject_External(); Callback->NumPTSteps = NumPTSteps; FReal Time = 0; const FReal GTDt = FixedDT * 0.25f; for (int32 Step = 0; Step < NumGTSteps; Step++) { //set force every external frame Particle2.AddForce(ConstantForce); FVec3 Grav(0, 0, 0); Scene.SetUpForFrame(&Grav, GTDt, 0, 99999, 99999, 1, false); Scene.StartFrame(); Scene.EndFrame(); Time += GTDt; const FReal InterpolatedTime = Time - FixedDT * Chaos::AsyncInterpolationMultiplier; const FReal ExpectedVFromForce = Time; if (InterpolatedTime < 0) { //not enough time to interpolate so just take initial value EXPECT_NEAR(Particle.X()[2], ZStart, 1e-2); EXPECT_NEAR(Particle2.V()[2], 0, 1e-2); } else { //interpolated EXPECT_NEAR(Particle.X()[2], ZStart + ZVel * InterpolatedTime, 1e-2); EXPECT_NEAR(Particle2.V()[2], InterpolatedTime, 1e-2); } } EXPECT_EQ(Callback->Count, NumPTSteps); const FReal LastInterpolatedTime = NumGTSteps * GTDt - FixedDT * Chaos::AsyncInterpolationMultiplier; EXPECT_NEAR(Particle.X()[2], ZStart + ZVel * LastInterpolatedTime, 1e-2); EXPECT_NEAR(Particle.V()[2], ZVel, 1e-2); } void ExpectVectorEqual(const FVec3& V0, const FVec3& V1) { EXPECT_EQ(V0.X, V1.X); EXPECT_EQ(V0.Y, V1.Y); EXPECT_EQ(V0.Z, V1.Z); } void TestKinematicTarget(const bool bInUpdateKinematicFromSimulation) { // Need to test: // GT particle position is immediately updated after calling SetKinematicTarget_AssumesLocked // GT particle positions and velocities are correctly updated // PT particle positions and velocities are correctly updated // Velocity becomes zero if no KinematicTarget is set in the current frame // Particle positions and velocities are correct after SetKinematicTarget_AssumesLocked, SetKinematicTarget_AssumesLocked // Velocity is zero if only SetGlobalPose_AssumesLocked is called (Teleport) // Particle positions and velocities are correct after SetGlobalPose_AssumesLocked, SetKinematicTarget_AssumesLocked (Teleport) // Particle positions and velocities are correct after SetKinematicTarget_AssumesLocked, SetGlobalPose_AssumesLocked (Teleport, KinematicTarget is cleared) FChaosScene Scene(nullptr, /*AsyncDt=*/-1); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); FActorCreationParams Params; Params.Scene = &Scene; FPhysicsActorHandle Proxy = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy); auto& Particle = Proxy->GetGameThreadAPI(); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle.SetGeometry(Sphere); } TArray Proxys = { Proxy }; Scene.AddActorsToScene_AssumesLocked(Proxys); Particle.SetObjectState(EObjectStateType::Kinematic); Particle.SetUpdateKinematicFromSimulation(bInUpdateKinematicFromSimulation); struct FDummyInput : FSimCallbackInput { FSingleParticlePhysicsProxy* Proxy; FVec3 CorrectX; FVec3 CorrectV; bool bKinematicWritebackEnabled; void Reset() {} }; struct FCallback : public TSimCallbackObject { virtual void OnPreSimulate_Internal() override { const FVec3 ExpectedX = GetConsumerInput_Internal()->CorrectX; const FVec3 ExpectedV = GetConsumerInput_Internal()->CorrectV; auto Handle = GetConsumerInput_Internal()->Proxy->GetPhysicsThreadAPI(); ExpectVectorEqual(Handle->X(), ExpectedX); ExpectVectorEqual(Handle->V(), ExpectedV); } }; auto Callback = Scene.GetSolver()->CreateAndRegisterSimCallbackObject_External(); Callback->GetProducerInputData_External()->Proxy = Proxy; Callback->GetProducerInputData_External()->bKinematicWritebackEnabled = bInUpdateKinematicFromSimulation; FVec3 Grav(0, 0, 0); float Dt = 1; auto AdvanceFrameAndRunTest = [&](const FVec3 &CorrectX, const FVec3 &CorrectV) { Scene.SetUpForFrame(&Grav, Dt, 0, 99999, 99999, 10, false); Scene.StartFrame(); Scene.EndFrame(); // Test X and V on GT // NOTE: GT velocity will not be updated if kinematic writeback from the physics thread is disabled ExpectVectorEqual(Particle.X(), CorrectX); if (Callback->GetProducerInputData_External()->bKinematicWritebackEnabled) { ExpectVectorEqual(Particle.V(), CorrectV); } // Test X and V on PT, this is going to be used in OnPreSimulate_Internal in next frame. Callback->GetProducerInputData_External()->CorrectX = CorrectX; Callback->GetProducerInputData_External()->CorrectV = CorrectV; }; // Set initial transform FVec3 CurrentX = FVec3(1, 2, 3); FVec3 CurrentV = FVec3(0, 0, 0); FChaosEngineInterface::SetGlobalPose_AssumesLocked(Proxy, FTransform(CurrentX)); Callback->GetProducerInputData_External()->CorrectX = CurrentX; Callback->GetProducerInputData_External()->CorrectV = CurrentV; AdvanceFrameAndRunTest(CurrentX, CurrentV); // Test SetKinematicTarget_AssumesLocked CurrentX = FVec3(2, 3, 4); CurrentV = FVec3(1, 1, 1); FChaosEngineInterface::SetKinematicTarget_AssumesLocked(Proxy, FTransform(CurrentX)); // Test if position is immediately updated on GT after SetKinematicTarget_AssumesLocked (if we aren't reading data back from PT) if (!bInUpdateKinematicFromSimulation) { ExpectVectorEqual(Particle.X(), CurrentX); } // This will fail when bInUpdateKinematicFromSimulation is false becasuse GT and PT disagree on velocity AdvanceFrameAndRunTest(CurrentX, CurrentV); // Test if velocity becomes zero when no kinematic target is set CurrentX = FVec3(2, 3, 4); CurrentV = FVec3(0, 0, 0); AdvanceFrameAndRunTest(CurrentX, CurrentV); // Test if particle positions and velocities are correct after SetKinematicTarget_AssumesLocked, SetKinematicTarget_AssumesLocked CurrentX = FVec3(0, 0, 0); CurrentV = FVec3(-2, -3, -4); FChaosEngineInterface::SetKinematicTarget_AssumesLocked(Proxy, FTransform(FVec3(1, 2, 3))); FChaosEngineInterface::SetKinematicTarget_AssumesLocked(Proxy, FTransform(CurrentX)); AdvanceFrameAndRunTest(CurrentX, CurrentV); // Test if velocity is zero if only SetGlobalPose_AssumesLocked is called (Teleport) CurrentX = FVec3(0, 0, 0); CurrentV = FVec3(0, 0, 0); FChaosEngineInterface::SetGlobalPose_AssumesLocked(Proxy, FTransform(CurrentX)); Callback->GetProducerInputData_External()->CorrectX = CurrentX; Callback->GetProducerInputData_External()->CorrectV = CurrentV; AdvanceFrameAndRunTest(CurrentX, CurrentV); // Test if particle positions and velocities are correct after SetGlobalPose_AssumesLocked, SetKinematicTarget_AssumesLocked CurrentX = FVec3(-1, -2, -3); CurrentV = FVec3(0, 0, 0); FChaosEngineInterface::SetGlobalPose_AssumesLocked(Proxy, FTransform(CurrentX)); FChaosEngineInterface::SetKinematicTarget_AssumesLocked(Proxy, FTransform(CurrentX)); Callback->GetProducerInputData_External()->CorrectX = CurrentX; AdvanceFrameAndRunTest(CurrentX, CurrentV); // Test if particle state to sleeping change after setting a kinematic target it's position and velocity should remain the same FChaosEngineInterface::SetKinematicTarget_AssumesLocked(Proxy, FTransform(FVec3(1, 2, 3))); Particle.SetObjectState(EObjectStateType::Sleeping); AdvanceFrameAndRunTest(CurrentX, CurrentV); // Test if particle positions and velocities are correct after SetKinematicTarget_AssumesLocked, SetGlobalPose_AssumesLocked CurrentX = FVec3(3, 2, 1); CurrentV = FVec3(0, 0, 0); FChaosEngineInterface::SetKinematicTarget_AssumesLocked(Proxy, FTransform(CurrentX)); FChaosEngineInterface::SetGlobalPose_AssumesLocked(Proxy, FTransform(CurrentX)); Callback->GetProducerInputData_External()->CorrectX = CurrentX; AdvanceFrameAndRunTest(CurrentX, CurrentV); // Test if the PT positions and velocities are right from previous frame CurrentX = FVec3(3, 2, 1); CurrentV = FVec3(0, 0, 0); AdvanceFrameAndRunTest(CurrentX, CurrentV); } // Test SetKinematicTarget when writeback from PT is enabled GTEST_TEST(EngineInterface, SetKinematicTargetWriteBackEnabled) { TestKinematicTarget(true); } // Test SetKinematicTarget when writeback from PT is disabled GTEST_TEST(EngineInterface, SetKinematicTargetWriteBackDisabled) { TestKinematicTarget(false); } GTEST_TEST(EngineInterface, PerPropertySetOnGT) { //Need to test: //setting transform, velocities, wake state, on external thread means we overwrite results until sim catches up //deleted proxy does not incorrectly update after it's deleted on gt const FReal FixedDT = 1; FChaosScene Scene(nullptr, FixedDT); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); Scene.GetSolver()->EnableAsyncMode(1); //tick 1 dt at a time FActorCreationParams Params; Params.Scene = &Scene; FPhysicsActorHandle Proxy = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy); auto& Particle = Proxy->GetGameThreadAPI(); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle.SetGeometry(Sphere); } TArray Proxys = { Proxy }; Scene.AddActorsToScene_AssumesLocked(Proxys); Particle.SetObjectState(EObjectStateType::Dynamic); const FReal ZVel = 10; const FReal ZStart = 100; Particle.SetV(FVec3(0, 0, ZVel)); Particle.SetX(FVec3(0, 0, ZStart)); const int32 NumGTSteps = 100; const FVec3 TeleportLocation(5, 5, ZStart); FReal Time = 0; const FReal GTDt = FixedDT * 0.5f; const int32 ChangeVelStep = 20; const FReal ChangeVelTime = ChangeVelStep * GTDt; const FReal YVelAfterChange = 10; const int32 TeleportStep = 10; const FReal TeleportTime = TeleportStep * GTDt; bool bHasTeleportedOnGT = false; bool bVelHasChanged = false; bool bWasPutToSleep = false; bool bWasWoken = false; const int32 SleepStep = 50; const int32 WakeStep = 70; const FReal PutToSleepTime = SleepStep * GTDt; const FReal WokenTime = WakeStep * GTDt; FReal SleepZPosition(0); for (int32 Step = 0; Step < NumGTSteps; Step++) { if (Step == TeleportStep) { Particle.SetX(TeleportLocation); bHasTeleportedOnGT = true; } if(Step == ChangeVelStep) { Particle.SetV(FVec3(0, YVelAfterChange, ZVel)); bVelHasChanged = true; } if(Step == SleepStep) { bWasPutToSleep = true; Particle.SetObjectState(EObjectStateType::Sleeping); SleepZPosition = Particle.X()[2]; //record position when gt wants to sleep } if(Step == WakeStep) { bWasWoken = true; Particle.SetV(FVec3(0, YVelAfterChange, ZVel)); Particle.SetObjectState(EObjectStateType::Dynamic); } FVec3 Grav(0, 0, 0); Scene.SetUpForFrame(&Grav, GTDt, 0, 99999, 99999, 10, false); Scene.StartFrame(); Scene.EndFrame(); Time += GTDt; const FReal InterpolatedTime = Time - FixedDT * Chaos::AsyncInterpolationMultiplier; if (InterpolatedTime < 0) { //not enough time to interpolate so just take initial value EXPECT_NEAR(Particle.X()[2], ZStart, 1e-2); } else { //interpolated if(bHasTeleportedOnGT) { EXPECT_NEAR(Particle.X()[0], TeleportLocation[0], 1e-2); //X never changes so as soon as gt teleports we should see it //if we haven't caught up to teleport, we just use the value set on GT for z value if(InterpolatedTime < TeleportTime) { EXPECT_NEAR(Particle.X()[2], TeleportLocation[2], 1e-3); } else { if(!bWasPutToSleep) { //caught up so expect normal movement to marshal back EXPECT_NEAR(Particle.X()[2], TeleportLocation[2] + ZVel * (InterpolatedTime - TeleportTime), 1e-2); } else if(InterpolatedTime < WokenTime) { //currently asleep so position is held constant EXPECT_NEAR(Particle.X()[2], SleepZPosition, 1e-2); if(!bWasWoken) { EXPECT_NEAR(Particle.V()[2], 0, 1e-2); } else { EXPECT_NEAR(Particle.V()[2], ZVel, 1e-2); } } else { //woke back up so position is moving again EXPECT_NEAR(Particle.X()[2], SleepZPosition + ZVel * (InterpolatedTime - WokenTime), 1e-2); EXPECT_NEAR(Particle.V()[2], ZVel, 1e-2); } } } else { EXPECT_NEAR(Particle.X()[2], ZStart + ZVel * InterpolatedTime, 1e-2); } if(bVelHasChanged) { if(!bWasPutToSleep || bWasWoken) { EXPECT_EQ(Particle.V()[1], YVelAfterChange); } else { //asleep so velocity is 0 EXPECT_EQ(Particle.V()[1], 0); } } else { EXPECT_EQ(Particle.V()[1], 0); } if(bWasPutToSleep && !bWasWoken) { EXPECT_EQ(Particle.ObjectState(), EObjectStateType::Sleeping); } else { EXPECT_EQ(Particle.ObjectState(), EObjectStateType::Dynamic); } } } const FReal LastInterpolatedTime = NumGTSteps * GTDt - FixedDT * Chaos::AsyncInterpolationMultiplier; EXPECT_EQ(Particle.V()[2], ZVel); EXPECT_EQ(Particle.V()[1], YVelAfterChange); } GTEST_TEST(EngineInterface, IterationSetOnGT) { //Need to test: //Before set iteration on game thread the iterations are default (8, 2, 1) //After set iteration on game thread the data goes through. FPBDPositionConstraints SinglePositionConstraint; const FReal FixedDT = 1; FChaosScene Scene(nullptr, FixedDT); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); FActorCreationParams Params; Params.Scene = &Scene; FPhysicsActorHandle Proxy = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy); FRigidBodyHandle_External& Particle = Proxy->GetGameThreadAPI(); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle.SetGeometry(Sphere); } TArray Proxys = { Proxy }; Scene.AddActorsToScene_AssumesLocked(Proxys); Particle.SetObjectState(EObjectStateType::Dynamic); Particle.SetGravityEnabled(true); FReal Time = 0; const FReal GTDt = FixedDT * 4; const int32 BigIteration = 100; FVec3 Grav(0, 0, -1); Scene.SetUpForFrame(&Grav, GTDt, 0, 99999, 99999, 10, false); Scene.StartFrame(); Scene.EndFrame(); Chaos::Private::FIterationSettings GroupIterations = Scene.GetSolver()->GetEvolution()->GetIslandGroupManager().GetIslandGroupIterations(0); EXPECT_EQ(GroupIterations.GetNumPositionIterations(), 8); EXPECT_EQ(GroupIterations.GetNumVelocityIterations(), 2); EXPECT_EQ(GroupIterations.GetNumProjectionIterations(), 1); auto FirstHandle = Scene.GetSolver()->GetEvolution()->GetParticleHandles().Handle(0)->CastToRigidParticle(); TArray Dynamics = { FirstHandle }; TArray Positions = { FVec3(1) }; FPBDPositionConstraints PositionConstraints(MoveTemp(Positions), MoveTemp(Dynamics), 1.f); SinglePositionConstraint = PositionConstraints; Scene.GetSolver()->GetEvolution()->AddConstraintContainer(SinglePositionConstraint); Particle.SetPositionSolverIterations(BigIteration); Particle.SetVelocitySolverIterations(BigIteration); Particle.SetProjectionSolverIterations(BigIteration); Scene.StartFrame(); Scene.EndFrame(); Chaos::Private::FIterationSettings GroupIterationsTemp = Scene.GetSolver()->GetEvolution()->GetIslandGroupManager().GetIslandGroupIterations(0); EXPECT_EQ(GroupIterationsTemp.GetNumPositionIterations(), 100); EXPECT_EQ(GroupIterationsTemp.GetNumVelocityIterations(), 100); EXPECT_EQ(GroupIterationsTemp.GetNumProjectionIterations(), 100); } GTEST_TEST(EngineInterface, FlushCommand) { //Need to test: //flushing commands works and sees state changes for both fixed dt and not //sim callback is not called bool bHitOnShutDown = false; { FChaosScene Scene(nullptr, /*AsyncDt=*/-1); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); Scene.GetSolver()->EnableAsyncMode(1); //tick 1 dt at a time FActorCreationParams Params; Params.Scene = &Scene; FPhysicsActorHandle Proxy = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy); auto& Particle = Proxy->GetGameThreadAPI(); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle.SetGeometry(Sphere); } TArray Proxys = { Proxy }; Scene.AddActorsToScene_AssumesLocked(Proxys); Particle.SetX(FVec3(0, 0, 3)); Scene.GetSolver()->EnqueueCommandImmediate([Proxy]() { //sees change immediately EXPECT_EQ(Proxy->GetPhysicsThreadAPI()->X()[2], 3); }); struct FCallback : public TSimCallbackObject<> { virtual void OnPreSimulate_Internal() override { EXPECT_FALSE(true); //this should never hit } }; auto Callback = Scene.GetSolver()->CreateAndRegisterSimCallbackObject_External(); FVec3 Grav(0, 0, 0); Scene.SetUpForFrame(&Grav, 0, 0, 99999, 99999, 10, false); //flush with dt 0 Scene.StartFrame(); Scene.EndFrame(); Scene.GetSolver()->EnqueueCommandImmediate([&bHitOnShutDown]() { //command enqueued and then solver shuts down, so flush must happen bHitOnShutDown = true; }); } EXPECT_TRUE(bHitOnShutDown); } GTEST_TEST(EngineInterface, SimSubstep) { //Need to test: //forces and torques are extrapolated (i.e. held constant for sub-steps) //kinematic targets are interpolated over the sub-step //identical inputs are given to sub-steps const FReal FixedDT = 1; FChaosScene Scene(nullptr, FixedDT); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); FActorCreationParams Params; Params.Scene = &Scene; FPhysicsActorHandle Proxy = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy); auto& Particle = Proxy->GetGameThreadAPI(); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle.SetGeometry(Sphere); } TArray Proxys = { Proxy }; Scene.AddActorsToScene_AssumesLocked(Proxys); Particle.SetObjectState(EObjectStateType::Dynamic); Particle.SetGravityEnabled(true); struct FDummyInput : FSimCallbackInput { int32 ExternalFrame; void Reset() {} }; struct FCallback : public TSimCallbackObject { virtual void OnPreSimulate_Internal() override { EXPECT_EQ(GetConsumerInput_Internal()->ExternalFrame, ExpectedFrame); EXPECT_NEAR(GetSimTime_Internal(), InternalSteps * GetDeltaTime_Internal(), 1e-2); //sim start is changing per sub-step ++InternalSteps; } int32 ExpectedFrame; int32 InternalSteps = 0; }; auto Callback = Scene.GetSolver()->CreateAndRegisterSimCallbackObject_External(); FReal Time = 0; const FReal GTDt = FixedDT * 4; for (int32 Step = 0; Step < 10; Step++) { Callback->ExpectedFrame = Step; Callback->GetProducerInputData_External()->ExternalFrame = Step; //make sure input matches for all sub-steps //set force every external frame Particle.AddForce(FVec3(0, 0, 1 * Particle.M())); //should counteract gravity FVec3 Grav(0, 0, -1); Scene.SetUpForFrame(&Grav, GTDt, 0, 99999, 99999, 10, false); Scene.StartFrame(); Scene.EndFrame(); Time += GTDt; //should have no movement because forces cancel out EXPECT_NEAR(Particle.X()[2], 0, 1e-2); EXPECT_NEAR(Particle.V()[2], 0, 1e-2); } } GTEST_TEST(EngineInterface, SimDestroyedProxy) { //Need to test: //destroyed proxy still valid in callback, but Proxy is nulled out //valid for multiple sub-steps FChaosScene Scene(nullptr, /*AsyncDt=*/-1); Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread); const FReal FixedDT = 1; Scene.GetSolver()->EnableAsyncMode(FixedDT); //tick 1 dt at a time FActorCreationParams Params; Params.Scene = &Scene; FPhysicsActorHandle Proxy = nullptr; FChaosEngineInterface::CreateActor(Params, Proxy); auto& Particle = Proxy->GetGameThreadAPI(); { auto Sphere = MakeImplicitObjectPtr(FVec3(0), 3); Particle.SetGeometry(Sphere); } TArray Proxys = { Proxy }; Scene.AddActorsToScene_AssumesLocked(Proxys); struct FDummyInput : FSimCallbackInput { FSingleParticlePhysicsProxy* Proxy; void Reset() {} }; struct FCallback : public TSimCallbackObject { virtual void OnPreSimulate_Internal() override { EXPECT_EQ(GetConsumerInput_Internal()->Proxy->GetHandle_LowLevel(), nullptr); } }; auto Callback = Scene.GetSolver()->CreateAndRegisterSimCallbackObject_External(); Callback->GetProducerInputData_External()->Proxy = Proxy; Scene.GetSolver()->UnregisterObject(Proxy); FVec3 Grav(0, 0, -1); Scene.SetUpForFrame(&Grav, FixedDT * 3, 0, 99999, 99999, 10, false); Scene.StartFrame(); Scene.EndFrame(); } GTEST_TEST(EngineInterface, OverlapOffsetActor) { FChaosScene Scene(nullptr, /*AsyncDt=*/-1); FActorCreationParams Params; Params.Scene = &Scene; Params.bSimulatePhysics = false; Params.bStatic = true; Params.InitialTM = FTransform::Identity; Params.Scene = &Scene; FPhysicsActorHandle StaticCube = nullptr; FChaosEngineInterface::CreateActor(Params, StaticCube); ASSERT_NE(StaticCube, nullptr); // Add geometry, placing a box at the origin constexpr FReal BoxSize = static_cast(50.0); const FVec3 HalfBoxExtent{ BoxSize }; // We require a union here, although the second geometry isn't used we need the particle to // have more than one shape in its shapes array otherwise the query acceleration will treat // it as a special case and skip bounds checking during the overlap TArray Geoms; Geoms.Emplace(MakeImplicitObjectPtr>(-HalfBoxExtent, HalfBoxExtent)); Geoms.Emplace(MakeImplicitObjectPtr>(-HalfBoxExtent, HalfBoxExtent)); auto& Particle = StaticCube->GetGameThreadAPI(); { Chaos::FImplicitObjectPtr GeomUnion = MakeImplicitObjectPtr(MoveTemp(Geoms)); Particle.SetGeometry(GeomUnion); } TArray Particles{ StaticCube }; Scene.AddActorsToScene_AssumesLocked(Particles); FChaosSQAccelerator SQ{ *Scene.GetSpacialAcceleration() }; FSQHitBuffer HitBuffer; FOverlapAllQueryCallback QueryCallback; // Here we query from a position under the box, but using a shape that has an offset. This tests // a failure case that was previously present where the query system assumed that the QueryTM // was inside the geometry being used to query. const FTransform QueryTM{ FVec3{0.0f, 0.0f, -110.0f} }; constexpr FReal SphereRadius = static_cast(50.0); SQ.Overlap(Chaos::FSphere(FVec3(0.0f, 0.0f, 100.0f), SphereRadius), QueryTM, HitBuffer, FChaosQueryFilterData(), QueryCallback, FQueryDebugParams()); EXPECT_TRUE(HitBuffer.HasBlockingHit()); } GTEST_TEST(EngineInterface, SweepOffsetActor) { FChaosScene Scene(nullptr, /*AsyncDt=*/-1); FActorCreationParams Params; Params.Scene = &Scene; Params.bSimulatePhysics = false; Params.bStatic = true; Params.InitialTM = FTransform::Identity; Params.Scene = &Scene; FPhysicsActorHandle StaticCube = nullptr; FChaosEngineInterface::CreateActor(Params, StaticCube); ASSERT_NE(StaticCube, nullptr); // Add geometry, placing a box at the origin constexpr FReal BoxSize = static_cast(50.0); const FVec3 HalfBoxExtent{ BoxSize }; // We require a union here, although the second geometry isn't used we need the particle to // have more than one shape in its shapes array otherwise the query acceleration will treat // it as a special case and skip bounds checking during the overlap TArray Geoms; Geoms.Emplace(MakeImplicitObjectPtr>(-HalfBoxExtent, HalfBoxExtent)); Geoms.Emplace(MakeImplicitObjectPtr>(-HalfBoxExtent, HalfBoxExtent)); auto& Particle = StaticCube->GetGameThreadAPI(); { Chaos::FImplicitObjectPtr GeomUnion = MakeImplicitObjectPtr(MoveTemp(Geoms)); Particle.SetGeometry(GeomUnion); } TArray Particles{ StaticCube }; Scene.AddActorsToScene_AssumesLocked(Particles); FChaosSQAccelerator SQ{ *Scene.GetSpacialAcceleration() }; FSQHitBuffer HitBuffer; FBlockAllQueryCallback QueryCallback; // Another box of same size that is offset from origin by 200. FVec3 Offset(200.f,0,0); TBox QueryBox(-HalfBoxExtent + Offset, HalfBoxExtent + Offset); // Sweep positions offset box directly above box at origin, should hit box sweeping downward. const FTransform QueryTM{ FVec3{-200.f, 0, 100.0f} }; const FVec3 Dir(0,0,-1); const FReal Length = 200; SQ.Sweep(QueryBox, QueryTM, Dir, Length, HitBuffer, EHitFlags::None, FQueryFilterData(), QueryCallback, FQueryDebugParams()); EXPECT_TRUE(HitBuffer.HasBlockingHit()); } GTEST_TEST(EngineInterface, BodyWithTwoShapes_SweepWithInitialOverlap_MinimalTOIIsReturned) { FChaosScene Scene(nullptr, /*AsyncDt=*/-1); FActorCreationParams Params; Params.Scene = &Scene; Params.bSimulatePhysics = false; Params.bStatic = true; Params.InitialTM = FTransform::Identity; Params.Scene = &Scene; FPhysicsActorHandle Actor0 = nullptr; FChaosEngineInterface::CreateActor(Params, Actor0); ASSERT_NE(Actor0, nullptr); TArray Geoms; Geoms.Emplace(MakeImplicitObjectPtr>(FVec3(-50, -50, -50), FVec3(0, 50, 50))); Geoms.Emplace(MakeImplicitObjectPtr>(FVec3(0, -50, -50), FVec3(50, 50, 50))); FRigidBodyHandle_External& Particle = Actor0->GetGameThreadAPI(); { Chaos::FImplicitObjectPtr GeomUnion = MakeImplicitObjectPtr(MoveTemp(Geoms)); Particle.SetGeometry(GeomUnion); } TArray Particles{ Actor0 }; Scene.AddActorsToScene_AssumesLocked(Particles); FChaosSQAccelerator SQ{ *Scene.GetSpacialAcceleration() }; FSQHitBuffer HitBuffer; FBlockAllQueryCallback QueryCallback; const FReal SphereRadius = 25; const Chaos::FSphere QuerySphere(FVec3::ZeroVector, SphereRadius); // Sweep down over both shapes with an initial overlap, however center over shape 0 more. FTransform QueryTM{ FVec3{-5, 0, 50} }; const FVec3 Dir(0,0,-1); const FReal Length = 200; SQ.Sweep(QuerySphere, QueryTM, Dir, Length, HitBuffer, EHitFlags::MTD, FQueryFilterData(), QueryCallback, FQueryDebugParams()); EXPECT_TRUE(HitBuffer.HasBlockingHit()); const FSweepHit* BlockingHit = HitBuffer.GetBlock(); EXPECT_EQ(BlockingHit->Shape, Particle.ShapesArray()[0].Get()); EXPECT_EQ(BlockingHit->Distance, -25); EXPECT_VECTOR_NEAR(BlockingHit->WorldPosition, FVector(-5, 0, 50), KINDA_SMALL_NUMBER); EXPECT_VECTOR_NEAR(BlockingHit->WorldNormal, FVector(0, 0, 1), KINDA_SMALL_NUMBER); // Now do a second sweep down, this time over shape 1 more. QueryTM = FTransform{ FVec3{5, 0, 50} }; SQ.Sweep(QuerySphere, QueryTM, Dir, Length, HitBuffer, EHitFlags::MTD, FQueryFilterData(), QueryCallback, FQueryDebugParams()); EXPECT_TRUE(HitBuffer.HasBlockingHit()); BlockingHit = HitBuffer.GetBlock(); EXPECT_EQ(BlockingHit->Shape, Particle.ShapesArray()[1].Get()); EXPECT_EQ(BlockingHit->Distance, -25); EXPECT_VECTOR_NEAR(BlockingHit->WorldPosition, FVector(5, 0, 50), KINDA_SMALL_NUMBER); EXPECT_VECTOR_NEAR(BlockingHit->WorldNormal, FVector(0, 0, 1), KINDA_SMALL_NUMBER); } GTEST_TEST(EngineInterface, TwoBodies_SweepWithInitialOverlap_MinimalTOIIsReturned) { FChaosScene Scene(nullptr, /*AsyncDt=*/-1); FActorCreationParams Params; Params.Scene = &Scene; Params.bSimulatePhysics = false; Params.bStatic = true; Params.InitialTM = FTransform::Identity; Params.Scene = &Scene; auto CreateActor = [](const FActorCreationParams& Params, const FImplicitObjectPtr& Geometry) -> FPhysicsActorHandle { FPhysicsActorHandle ActorHandle = nullptr; FChaosEngineInterface::CreateActor(Params, ActorHandle); FRigidBodyHandle_External& Particle = ActorHandle->GetGameThreadAPI(); Particle.SetGeometry(Geometry); return ActorHandle; }; FImplicitObjectPtr Box0 = MakeImplicitObjectPtr>(FVec3(-50, -50, -50), FVec3(0, 50, 50)); FPhysicsActorHandle Actor0 = CreateActor(Params, Box0); ASSERT_NE(Actor0, nullptr); FSingleParticlePhysicsProxy* Actor0Proxy = Actor0->GetGameThreadAPI().GetProxy(); FImplicitObjectPtr Box1 = MakeImplicitObjectPtr>(FVec3(0, -50, -50), FVec3(50, 50, 50)); FPhysicsActorHandle Actor1 = CreateActor(Params, Box1); ASSERT_NE(Actor1, nullptr); FSingleParticlePhysicsProxy* Actor1Proxy = Actor1->GetGameThreadAPI().GetProxy(); TArray Particles{ Actor0, Actor1 }; Scene.AddActorsToScene_AssumesLocked(Particles); FChaosSQAccelerator SQ{ *Scene.GetSpacialAcceleration() }; FSQHitBuffer HitBuffer; FBlockAllQueryCallback QueryCallback; const FReal SphereRadius = 25; const Chaos::FSphere QuerySphere(FVec3::ZeroVector, SphereRadius); // Sweep down over both shapes with an initial overlap, however center over shape 0 more. FTransform QueryTM{ FVec3{-5, 0, 50} }; const FVec3 Dir(0, 0, -1); const FReal Length = 200; SQ.Sweep(QuerySphere, QueryTM, Dir, Length, HitBuffer, EHitFlags::MTD, FQueryFilterData(), QueryCallback, FQueryDebugParams()); EXPECT_TRUE(HitBuffer.HasBlockingHit()); const FSweepHit* BlockingHit = HitBuffer.GetBlock(); EXPECT_EQ(BlockingHit->Actor->GetProxy(), Actor0Proxy); EXPECT_EQ(BlockingHit->Distance, -25); EXPECT_VECTOR_NEAR(BlockingHit->WorldPosition, FVector(-5, 0, 50), KINDA_SMALL_NUMBER); EXPECT_VECTOR_NEAR(BlockingHit->WorldNormal, FVector(0, 0, 1), KINDA_SMALL_NUMBER); // Now do a second sweep down, this time over shape 1 more. QueryTM = FTransform{ FVec3{5, 0, 50} }; SQ.Sweep(QuerySphere, QueryTM, Dir, Length, HitBuffer, EHitFlags::MTD, FQueryFilterData(), QueryCallback, FQueryDebugParams()); EXPECT_TRUE(HitBuffer.HasBlockingHit()); BlockingHit = HitBuffer.GetBlock(); EXPECT_EQ(BlockingHit->Actor->GetProxy(), Actor1Proxy); EXPECT_EQ(BlockingHit->Distance, -25); EXPECT_VECTOR_NEAR(BlockingHit->WorldPosition, FVector(5, 0, 50), KINDA_SMALL_NUMBER); EXPECT_VECTOR_NEAR(BlockingHit->WorldNormal, FVector(0, 0, 1), KINDA_SMALL_NUMBER); } // Disable a moving kinematic particle and switch it to dynamic while disabled. Verify that when // disabled it is not in any active lists, and that when re-enabled it is not duplicated // // There was a (benign) bug where particles were not removed from the MovingKinematics list until the // next call to ApplyKinematicTargets, which means they can be included in collision detection. // GTEST_TEST(EvolutionTests, TestDisableKinematicEnableDynamic) { const FReal Dt = FReal(1.0 / 60.0); FParticleUniqueIndicesMultithreaded UniqueIndices; FPBDRigidsSOAs Particles(UniqueIndices); THandleArray PhysicalMaterials; FPBDRigidsEvolutionGBF Evolution(Particles, PhysicalMaterials); Evolution.GetGravityForces().SetAcceleration(FVec3(0), 0); // Create a moving kinematic particle TArray ParticleHandles = Evolution.CreateDynamicParticles(1); Evolution.EnableParticle(ParticleHandles[0]); Evolution.SetParticleObjectState(ParticleHandles[0], EObjectStateType::Kinematic); Evolution.SetParticleKinematicTarget(ParticleHandles[0], FKinematicTarget::MakePositionTarget(FVec3(10,0,0), FRotation3::FromIdentity())); Evolution.AdvanceOneTimeStep(Dt); EXPECT_FALSE(ParticleHandles[0]->IsDynamic()); EXPECT_TRUE(ParticleHandles[0]->IsKinematic()); // Check that it is in the moving kinematics list { const TParticleView>& DynamicSleepingView = Particles.GetNonDisabledDynamicView(); const TParticleView>& DynamicMovingKinematicView = Particles.GetActiveDynamicMovingKinematicParticlesView(); EXPECT_EQ(DynamicSleepingView.Num(), 0); EXPECT_EQ(DynamicMovingKinematicView.Num(), 1); } // Disable the kinematic Evolution.DisableParticle(ParticleHandles[0]); // It should not be in any active views now { const TParticleView>& DynamicSleepingView = Particles.GetNonDisabledDynamicView(); const TParticleView>& DynamicMovingKinematicView = Particles.GetActiveDynamicMovingKinematicParticlesView(); EXPECT_EQ(DynamicSleepingView.Num(), 0); EXPECT_EQ(DynamicMovingKinematicView.Num(), 0); } // Make the particle dynamic and enable it Evolution.SetParticleObjectState(ParticleHandles[0], EObjectStateType::Dynamic); Evolution.EnableParticle(ParticleHandles[0]); // Check that it is in the active views, but not duplicated in either { const TParticleView>& DynamicSleepingView = Particles.GetNonDisabledDynamicView(); const TParticleView>& DynamicMovingKinematicView = Particles.GetActiveDynamicMovingKinematicParticlesView(); EXPECT_EQ(DynamicSleepingView.Num(), 1); EXPECT_EQ(DynamicMovingKinematicView.Num(), 1); } } // Check that we cannot set a kinematic target on a dynamic particle. // // This would cause the dynamic particle to be added to the MovingKinematics // list which means it would appear twice in the GetActiveDynamicMovingKinematicParticlesView // which can result in a race condition in collision detection as a particle pair will be // considered twice and possibly on different threads. GTEST_TEST(EvolutionTests, TestKinematicTargetOnDynamic) { const FReal Dt = FReal(1.0 / 60.0); FParticleUniqueIndicesMultithreaded UniqueIndices; FPBDRigidsSOAs Particles(UniqueIndices); THandleArray PhysicalMaterials; FPBDRigidsEvolutionGBF Evolution(Particles, PhysicalMaterials); Evolution.GetGravityForces().SetAcceleration(FVec3(0), 0); // Create a dynamic particle TArray ParticleHandles = Evolution.CreateDynamicParticles(1); Evolution.EnableParticle(ParticleHandles[0]); // Set the kinematic target Evolution.SetParticleKinematicTarget(ParticleHandles[0], FKinematicTarget::MakePositionTarget(FVec3(10, 0, 0), FRotation3::FromIdentity())); // We should not have a kinematic target EXPECT_FALSE(ParticleHandles[0]->KinematicTarget().IsSet()); Evolution.AdvanceOneTimeStep(Dt); // Check that it is in the active views, but not duplicated in either { const TParticleView>& DynamicSleepingView = Particles.GetNonDisabledDynamicView(); const TParticleView>& DynamicMovingKinematicView = Particles.GetActiveDynamicMovingKinematicParticlesView(); EXPECT_EQ(DynamicSleepingView.Num(), 1); EXPECT_EQ(DynamicMovingKinematicView.Num(), 1); } } }