// 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 "RewindData.h" #include "GeometryCollection/GeometryCollectionTestFramework.h" #include "Chaos/SimCallbackObject.h" #include "Chaos/Framework/ChaosResultsManager.h" #ifndef REWIND_DESYNC #define REWIND_DESYNC 0 #endif namespace ChaosTest { using namespace Chaos; using namespace GeometryCollectionTest; template void TickSolverHelper(TSolver* Solver, FReal Dt = 1.0) { Solver->AdvanceAndDispatch_External(Dt); Solver->UpdateGameThreadStructures(); } auto* CreateSolverHelper(int32 StepMode, int32 RewindHistorySize, int32 Optimization, FReal& OutSimDt) { constexpr FReal FixedDt = 1; constexpr FReal DtSizes[] = { FixedDt, FixedDt, FixedDt * 0.25, FixedDt * 4 }; //test fixed dt, sub-stepping, step collapsing // Make a solver FChaosSolversModule* Module = FChaosSolversModule::GetModule(); auto* Solver = Module->CreateSolver(nullptr,/*AsyncDt=*/-1); InitSolverSettings(Solver); /** int32 Optimization starts at 0 and iterate up to 2 * 0 = No optimizations * 1 = CollisionResimCache * 2 = Optimized RewindData */ Solver->EnableRewindCapture(RewindHistorySize, Optimization == 1); // Use CollisionResimCache optimization as optimization 1 if (Chaos::FRewindData* RewindData = Solver->GetRewindData()) { RewindData->SetRewindDataOptimization(Optimization == 2); // Use optimized RewindData in as optimization 2 } Solver->SetThreadingMode_External(EThreadingModeTemp::SingleThread); OutSimDt = DtSizes[StepMode]; if (StepMode > 0) { Solver->EnableAsyncMode(DtSizes[StepMode]); } return Solver; } struct TRewindHelper { template static void TestEmpty(const TLambda& Lambda, int32 RewindHistorySize = 200) { for (int Optimization = 0; Optimization < 3; ++Optimization) { for (int DtMode = 0; DtMode < 4; ++DtMode) { FChaosSolversModule* Module = FChaosSolversModule::GetModule(); FReal SimDt; auto* Solver = CreateSolverHelper(DtMode, RewindHistorySize, Optimization, SimDt); Solver->SetMaxDeltaTime_External(SimDt); //make sure it can step even for huge steps Lambda(Solver, SimDt, Optimization); Module->DestroySolver(Solver); } } } template static void TestDynamicSphere(const TLambda& Lambda, int32 RewindHistorySize = 200) { TestEmpty([&Lambda, RewindHistorySize](auto* Solver, FReal SimDt, int32 Optimization) { Chaos::FImplicitObjectPtr Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10)); // Make particles auto Proxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGeometry(Sphere); Solver->RegisterObject(Proxy); Lambda(Solver, SimDt, Optimization, Proxy, Sphere.GetReference()); }, RewindHistorySize); } }; GTEST_TEST(AllTraits, RewindTest_MovingGeomChange) { TRewindHelper::TestEmpty([](auto* Solver, FReal SimDt, int32 Optimization) { auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10)); auto Box = Chaos::FImplicitObjectPtr(new TBox(FVec3(0), FVec3(1))); auto Box2 = Chaos::FImplicitObjectPtr(new TBox(FVec3(2), FVec3(3))); // Make particles auto Proxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle()); auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGeometry(Sphere); Solver->RegisterObject(Proxy); const int32 LastGameStep = 20; for (int Step = 0; Step <= LastGameStep; ++Step) { //property that changes every step Particle.SetX(FVec3(0, 0, 100 - Step)); //property that changes once half way through if (Step == 3) { Particle.SetGeometry(Box); } if (Step == 5) { Particle.SetGeometry(Box2); } if (Step == 7) { Particle.SetGeometry(Box); } TickSolverHelper(Solver); } //ended up at z = 100 - LastGameStep EXPECT_EQ(Particle.X()[2], 100 - LastGameStep); //ended up with box geometry EXPECT_EQ(Box.GetReference(), Particle.GetGeometry()); const FRewindData* RewindData = Solver->GetRewindData(); //check state at every step except latest const int32 LastSimStep = LastGameStep / SimDt; for (int SimStep = 0; SimStep < LastSimStep - 1; ++SimStep) { const FReal TimeStart = SimStep * SimDt; const FReal TimeEnd = (SimStep + 1) * SimDt; const FReal LastInputTime = SimDt <= 1 ? TimeStart : TimeEnd - 1; //latest gt time associated with this interval const auto ParticleState = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), SimStep); EXPECT_EQ(ParticleState.GetX()[2], 100 - FMath::FloorToInt(LastInputTime)); //We teleported on GT so no interpolation if (LastInputTime < 3) { //was sphere EXPECT_EQ(ParticleState.GetGeometry(), Sphere.GetReference()); } else if (LastInputTime < 5 || LastInputTime >= 7) { //then became box EXPECT_EQ(ParticleState.GetGeometry(), Box.GetReference()); } else { //second box EXPECT_EQ(ParticleState.GetGeometry(), Box2.GetReference()); } } Solver->UnregisterObject(Proxy); }); } struct FRewindCallbackTestHelper : public IRewindCallback { FRewindCallbackTestHelper(const int32 InStepToRewindOn, const int32 InRewindToStep = 0) : StepToRewindOn(InStepToRewindOn) , RewindToStep(InRewindToStep) { } virtual int32 TriggerRewindIfNeeded_Internal(int32 PhysStep) override { return bRewound ? INDEX_NONE : TriggerRewindFunc(PhysStep); } virtual void ProcessInputs_Internal(int32 PhysicsStep, const TArray& SimCallbackInputs) override { ProcessInputsFunc(PhysicsStep, bRewound); } virtual void PreResimStep_Internal(int32 Step, bool bFirst) override { bRewound = true; } virtual void PostResimStep_Internal(int32 Step) override { PostFunc(Step); bRewound = false; } int32 StepToRewindOn; int32 RewindToStep; bool bRewound = false; TFunction ProcessInputsFunc = [](int32, bool) {}; TFunction PostFunc = [](int32) {}; TFunction TriggerRewindFunc = [this](int32 PhysicsStep) -> int32 { return PhysicsStep == StepToRewindOn ? RewindToStep : INDEX_NONE; }; }; template FRewindCallbackTestHelper* RegisterCallbackHelper(TSolver* Solver, const int32 NumStepsBeforeRewind = 0, const int32 RewindTo = 0) { auto Callback = MakeUnique(NumStepsBeforeRewind - 1, RewindTo); FRewindCallbackTestHelper* Result = Callback.Get(); Solver->SetRewindCallback(MoveTemp(Callback)); return Result; } GTEST_TEST(AllTraits, RewindTest_AddImpulseFromGT) { //We expect anything that came from GT to automatically be reapplied during rewind //This is for things that come outside of the net prediction system, like a teleport TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { const int32 LastGameStep = 20; FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver, FMath::TruncToInt(LastGameStep / SimDt)); Helper->PostFunc = [Proxy, SimDt](int32 Step) { const int32 GameStep = SimDt <= 1 ? Step * SimDt : (Step + 1) * SimDt; if (GameStep < 5) { EXPECT_EQ(Proxy->GetPhysicsThreadAPI()->V()[2], 0); } else { EXPECT_EQ(Proxy->GetPhysicsThreadAPI()->V()[2], 100); } }; auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGravityEnabled(false); for (int Step = 0; Step <= LastGameStep; ++Step) { if (Step == 5) { Particle.SetLinearImpulse(FVec3(0, 0, 100), /*bIsVelocity=*/false); } TickSolverHelper(Solver); } }); } GTEST_TEST(AllTraits, RewindTest_DeleteFromGT) { //GT writes of a deleted particle should be ignored during a resim (not crash) TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { const int32 LastGameStep = 20; RegisterCallbackHelper(Solver, FMath::TruncToInt(LastGameStep / SimDt)); auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGravityEnabled(false); for (int Step = 0; Step <= LastGameStep; ++Step) { if (Step == 5) { Particle.SetLinearImpulse(FVec3(0, 0, 100), /*bIsVelocity=*/false); } if(Step == 15) { Solver->UnregisterObject(Proxy); } TickSolverHelper(Solver); } }); } GTEST_TEST(AllTraits, RewindTest_ResimBeforeCreation) { //GT creates object half way through sim - we want to make sure resim properly ignores this TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { const int32 LastGameStep = 20; FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver, FMath::TruncToInt(LastGameStep / SimDt)); Helper->PostFunc = [Proxy, SimDt](int32 Step) { //simple movement without hitting object until we hit floor, should not ever hit sphere const FReal NoHitZ = 100 - (Step + 1) * SimDt * 10; if (NoHitZ > 40) { EXPECT_EQ(Proxy->GetPhysicsThreadAPI()->X()[2], NoHitZ); } else { EXPECT_GE(Proxy->GetPhysicsThreadAPI()->X()[2], 35); //floor should stop us (gave 5 units of error for solver) } }; auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGravityEnabled(false); ChaosTest::SetParticleSimDataToCollide({ Proxy->GetParticle_LowLevel() }); Particle.SetX(FVec3(0, 0, 100)); Particle.SetV(FVec3(0, 0, -10)); auto SphereGeom = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10)); FSingleParticlePhysicsProxy* SecondSphere = nullptr; auto FloorGeom = Chaos::FImplicitObjectPtr(new TBox(FVec3(-100, -100, 0), FVec3(100, 100, 30))); FSingleParticlePhysicsProxy* Floor = nullptr; for (int Step = 0; Step <= LastGameStep; ++Step) { if(Step == 5) { // Make blocking floor Floor = FSingleParticlePhysicsProxy::Create(Chaos::FGeometryParticle::CreateParticle()); auto& FloorParticle = Floor->GetGameThreadAPI(); FloorParticle.SetGeometry(FloorGeom); Solver->RegisterObject(Floor); ChaosTest::SetParticleSimDataToCollide({ Floor->GetParticle_LowLevel() }); } if (Step == 15) { // Make particles SecondSphere = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto& SecondSphereParticle = SecondSphere->GetGameThreadAPI(); SecondSphereParticle.SetGravityEnabled(false); SecondSphereParticle.SetX(FVec3(0, 0, 80)); //if resim doesn't disable, this will be in the Sphere's way even though it didn't exist yet SecondSphereParticle.SetGeometry(SphereGeom); Solver->RegisterObject(SecondSphere); ChaosTest::SetParticleSimDataToCollide({ SecondSphere->GetParticle_LowLevel() }); } TickSolverHelper(Solver); } Solver->UnregisterObject(SecondSphere); Solver->UnregisterObject(Floor); }); } // @todo(chaos): Rewind does not support SimData changes yet // (note this test used to pass, but there was a bug in UpdateShapesArrayFromGeometry that would leave SimData // as all-zero if you don't have a Union at the root. That bug is fixed which exposes this test failure) GTEST_TEST(AllTraits, DISABLED_RewindTest_ResimShapeFilter) { //GT modifies filter after object passes through, want to make sure resim restores this state correctly TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { const int32 LastGameStep = 20; FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver, FMath::TruncToInt(LastGameStep / SimDt)); Helper->PostFunc = [Proxy, SimDt](int32 Step) { //simple movement without hitting object const FReal NoHitZ = 100 - (Step + 1) * SimDt * 10; EXPECT_EQ(Proxy->GetPhysicsThreadAPI()->X()[2], NoHitZ); }; auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGravityEnabled(false); ChaosTest::SetParticleSimDataToCollide({ Proxy->GetParticle_LowLevel() }); Particle.SetX(FVec3(0, 0, 100)); Particle.SetV(FVec3(0, 0, -10)); auto SphereGeom = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10)); FSingleParticlePhysicsProxy* SecondSphere = nullptr; // Make particles SecondSphere = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto& SecondSphereParticle = SecondSphere->GetGameThreadAPI(); SecondSphereParticle.SetGravityEnabled(false); SecondSphereParticle.SetX(FVec3(0, 0, 10)); //if resim doesn't reset collision, this will be in the Sphere's way SecondSphereParticle.SetGeometry(SphereGeom); Solver->RegisterObject(SecondSphere); for (int Step = 0; Step <= LastGameStep; ++Step) { if (Step == 15) { //enable collision after particle passed ChaosTest::SetParticleSimDataToCollide({ SecondSphere->GetParticle_LowLevel() }); } TickSolverHelper(Solver); } Solver->UnregisterObject(SecondSphere); }); } GTEST_TEST(AllTraits, RewindTest_ResimSleepChange) { //change object state on physics thread, and make sure the state change is properly recorded in rewind data TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver); Helper->ProcessInputsFunc = [Proxy, RewindData = Solver->GetRewindData()](int32 PhysicsStep, bool bResim) { for (int32 Step = 0; Step < PhysicsStep; ++Step) { const EObjectStateType OldState = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step).ObjectState(); if (Step < 4) //we set to sleep on step 3, which means we won't see it as input state until 4 { EXPECT_EQ(OldState, EObjectStateType::Dynamic); } else { EXPECT_EQ(OldState, EObjectStateType::Sleeping); } } if (PhysicsStep == 3) { Proxy->GetPhysicsThreadAPI()->SetObjectState(EObjectStateType::Sleeping); } }; const int32 LastGameStep = 32; auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGravityEnabled(false); Particle.SetV(FVec3(0, 0, 1)); for (int Step = 0; Step <= LastGameStep; ++Step) { TickSolverHelper(Solver); } }); } GTEST_TEST(AllTraits, RewindTest_ModifyVelocityFromSimCallback) { //modify velocity from sim callback and from solver //rewind data should give us the velocity _before_ the sim callback (i.e. the solver is not stomping with the wrong PreV) TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver); Helper->ProcessInputsFunc = [Proxy, RewindData = Solver->GetRewindData(), SimDt](int32 PhysicsStep, bool bResim) { if (SimDt * PhysicsStep == 4) { Proxy->GetPhysicsThreadAPI()->SetV(FVec3(0, 0, 0)); } for (int32 Step = 0; Step < PhysicsStep; ++Step) { const FReal OldV = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step).GetV()[2]; const FReal Time = Step * SimDt; if (Time == 2) //velocity was reset by gt { EXPECT_NEAR(OldV, 0, 1e-2); } else if (Time < 2) //simple acceleration { EXPECT_NEAR(OldV, -Time, 1e-2); } else if (Time <= 4) //we set velocity at time = 4, so rewind data will not see it until frame 5 { if (SimDt > 2) //if SimDt is this large, the reset of zero at time 2 is swallowed by first step so we don't really know about it { EXPECT_NEAR(OldV, -Time, 1e-2); } else { //otherwise we see integration but from time 2 instead of time 0 EXPECT_NEAR(OldV, -(Time - 2), 1e-2); } } else { //everyone sees reset after time 4 so integration is from that time EXPECT_NEAR(OldV, -(Time - 4), 1e-2); } } }; const int32 LastGameStep = 32; const int32 NumPhysSteps = FMath::TruncToInt(LastGameStep / SimDt); auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGravityEnabled(true); Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0); Particle.SetV(FVec3(0, 0, 0)); for (int Step = 0; Step <= LastGameStep; ++Step) { if (Step == 2) { Particle.SetV(FVec3(0, 0, 0)); //reset velocity } TickSolverHelper(Solver); } }); } GTEST_TEST(AllTraits, RewindTest_SpawnEarlierCorrection) { // Test resim when object spawned earlier as part of correction TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { FSingleParticlePhysicsProxy* Floor = nullptr; bool bHasResimmed = false; FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver); Helper->TriggerRewindFunc = [&Floor, &bHasResimmed](int32 PhysicsStep) -> int32 { if(!bHasResimmed && Floor) { bHasResimmed = true; return 0; } return INDEX_NONE; }; Helper->ProcessInputsFunc = [Proxy, RewindData = Solver->GetRewindData(), SimDt, &Floor, &bHasResimmed](int32 PhysicsStep, bool bIsResimming) { const FReal Time = PhysicsStep * SimDt; if (bIsResimming) { if (PhysicsStep == 1) { RewindData->SpawnProxyIfNeeded(*Floor); } } if (!bHasResimmed || Time < 4.5) { //simply movement without hitting floor because it's spawned too late EXPECT_NEAR(Proxy->GetPhysicsThreadAPI()->X()[2], 14.5 - Time, 1e-2); } else { //floor spawned earlier so we hit it EXPECT_GE(Proxy->GetPhysicsThreadAPI()->X()[2], 10 - 1e-2); } }; const int32 LastGameStep = 32; auto FloorGeom = Chaos::FImplicitObjectPtr(new TBox(FVec3(-100, -100, -1), FVec3(100, 100, 0))); auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGravityEnabled(false); Particle.SetV(FVec3(0, 0, -1)); Particle.SetX(FVec3(0, 0, 14.5)); ChaosTest::SetParticleSimDataToCollide({ Proxy->GetParticle_LowLevel() }); for (int Step = 0; Step <= LastGameStep; ++Step) { TickSolverHelper(Solver); //spawn floor way late to ensure no collision on first run if (Step == 12) { Floor = FSingleParticlePhysicsProxy::Create(Chaos::FGeometryParticle::CreateParticle()); auto& FloorParticle = Floor->GetGameThreadAPI(); FloorParticle.SetGeometry(FloorGeom); Solver->RegisterObject(Floor); ChaosTest::SetParticleSimDataToCollide({ Floor->GetParticle_LowLevel() }); } } }); } GTEST_TEST(AllTraits, RewindTest_SpawnEarlierCorrection2) { // Test resim when object spawned earlier as part of correction TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { FSingleParticlePhysicsProxy* SpawnedProxy = nullptr; FSingleParticlePhysicsProxy* SpawnedProxyNoCorrection = nullptr; bool bHasResimmed = false; FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver); Helper->TriggerRewindFunc = [&SpawnedProxy, &SpawnedProxyNoCorrection, &bHasResimmed](int32 PhysicsStep) -> int32 { if(!bHasResimmed && SpawnedProxy) { bHasResimmed = true; return 0; } return INDEX_NONE; }; Helper->ProcessInputsFunc = [RewindData = Solver->GetRewindData(), SimDt, &SpawnedProxy, &SpawnedProxyNoCorrection, &bHasResimmed](int32 PhysicsStep, bool bIsResimming) { const FReal Time = PhysicsStep * SimDt; if (bIsResimming && SpawnedProxy) { if (PhysicsStep == 10) { RewindData->SpawnProxyIfNeeded(*SpawnedProxy); SpawnedProxy->GetPhysicsThreadAPI()->SetX(FVec3(500, 0, 100.0)); } } if (SpawnedProxy && SpawnedProxyNoCorrection) { auto PT0 = SpawnedProxy->GetPhysicsThreadAPI(); auto PT1 = SpawnedProxyNoCorrection->GetPhysicsThreadAPI(); // After we've applied the correction and simmed past frame 10, we expect the first proxy to be "ahead" of the second one that didn't get corrected if (bHasResimmed && PhysicsStep > 10) { EXPECT_LT(PT0->X()[2], PT1->X()[2]); } } }; const int32 LastGameStep = 32; auto SphereGeom = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10)); auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGravityEnabled(false); Particle.SetV(FVec3(0, 0, -1)); Particle.SetX(FVec3(0, 0, 14.5)); ChaosTest::SetParticleSimDataToCollide({ Proxy->GetParticle_LowLevel() }); for (int Step = 0; Step <= LastGameStep; ++Step) { TickSolverHelper(Solver); //spawn floor way late to ensure no collision on first run if (Step == 12) { // Make particles. E.g: // we just found out from the server these were spawned and these are the latest positions replicated from the server. // but actually these positions are the server positions from frame 10. Lets see if we can correct this. { SpawnedProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto& GT = SpawnedProxy->GetGameThreadAPI(); GT.SetV(FVec3(0, 0, 0.0)); GT.SetX(FVec3(500, 0, 100.0)); GT.SetGeometry(SphereGeom); Solver->RegisterObject(SpawnedProxy); ChaosTest::SetParticleSimDataToCollide({ SpawnedProxy->GetParticle_LowLevel() }); } { SpawnedProxyNoCorrection = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto& GT = SpawnedProxyNoCorrection->GetGameThreadAPI(); GT.SetV(FVec3(0, 0, 0.0)); GT.SetX(FVec3(100, 0, 100.0)); GT.SetGeometry(SphereGeom); Solver->RegisterObject(SpawnedProxyNoCorrection); ChaosTest::SetParticleSimDataToCollide({ SpawnedProxyNoCorrection->GetParticle_LowLevel() }); } } } }); } GTEST_TEST(AllTraits, RewindTest_MovingToNotMovingInterpolation) { // //This tests that even though it's only dirty for one frame, we still interpolate it over many //Makes sure rewind data is still passed back to GT even though particle is asleep before and after it moves (i.e. not dirty during rewind step) TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { //only care about resim when async results are used if (Solver->IsUsingAsyncResults() == false) { return; } const FReal ResimTime = 20; const FReal SleepTime = 12; bool bHasResimmed = false; FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver); Helper->TriggerRewindFunc = [&bHasResimmed, ResimTime, SimDt](int32 PhysicsStep) -> int32 { const FReal Time = PhysicsStep * SimDt; if (!bHasResimmed && Time == ResimTime) { bHasResimmed = true; return 2; } return INDEX_NONE; }; Helper->ProcessInputsFunc = [Proxy, RewindData = Solver->GetRewindData(), SimDt, ResimTime, SleepTime, &bHasResimmed](int32 PhysicsStep, bool bIsResimming) { const FReal Time = PhysicsStep * SimDt; if (bHasResimmed) { if (Proxy->GetPhysicsThreadAPI()->V()[2] == 1) { Proxy->GetPhysicsThreadAPI()->SetV(FVec3(0, 0, 0)); } } else { if (Time >= SleepTime && Proxy->GetPhysicsThreadAPI()->ObjectState() == EObjectStateType::Dynamic) { Proxy->GetPhysicsThreadAPI()->SetObjectState(EObjectStateType::Sleeping); } } }; const int32 LastGameStep = 64; auto FloorGeom = Chaos::FImplicitObjectPtr(new TBox(FVec3(-100, -100, -1), FVec3(100, 100, 0))); auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGravityEnabled(false); Particle.SetV(FVec3(0, 0, 0)); Particle.SetX(FVec3(0, 0, 0)); FReal Time = 0; const FReal StartMovingTime = 4; const FReal StartMovingTimeDiscrete = FMath::FloorToInt(StartMovingTime / SimDt) * SimDt; const FReal GTDt = 1; const FReal InterpStartTime = ResimTime + SimDt; const FReal SleepLocation = SleepTime - StartMovingTimeDiscrete; const FReal CorrectedLocation = SimDt <= 1 ? 0 : 4; FReal PrevZDuringInterp = SleepLocation; //shouldn't be further then this because already interpolating back to 0 for (int Step = 0; Step <= LastGameStep; ++Step) { if (Time == StartMovingTime) { Particle.SetV(FVec3(0, 0, 1)); } TickSolverHelper(Solver); Time += GTDt; const FReal InterpolatedTime = Time - SimDt * Solver->GetAsyncInterpolationMultiplier(); if (InterpolatedTime <= InterpStartTime) { if(InterpolatedTime < StartMovingTimeDiscrete) { //No movement yet EXPECT_NEAR(Particle.X()[2], 0, 1e-2); } else { //simple movement with constant velocity until goes to sleep if(InterpolatedTime >= SleepTime) { EXPECT_NEAR(Particle.X()[2], SleepLocation, 1e-2); } else { EXPECT_NEAR(Particle.X()[2], InterpolatedTime - StartMovingTimeDiscrete, 1e-2); } } } else { //leash mode EXPECT_GE(Particle.X()[2], CorrectedLocation); EXPECT_LE(Particle.X()[2], PrevZDuringInterp); PrevZDuringInterp = Particle.X()[2]; } } }); } GTEST_TEST(AllTraits, RewindTest_ResimInSync) { //apply forces in the same way until resim step 4 which should trigger a desync TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { struct FRewindCallback : public IRewindCallback { FSingleParticlePhysicsProxy* Proxy; FRewindData* RewindData; virtual void ProcessInputs_Internal(int32 PhysicsStep, const TArray& SimCallbackInputs) override { bool bIsResimming = RewindData->IsResim(); if(PhysicsStep == 2) { Proxy->GetPhysicsThreadAPI()->AddForce(FVec3(1, 0, 0)); } if(bIsResimming && PhysicsStep == 4) { Proxy->GetPhysicsThreadAPI()->AddForce(FVec3(1, 0, 0)); //cause a desync } if(bIsResimming) { if(PhysicsStep <= 4) { EXPECT_EQ(Proxy->GetHandle_LowLevel()->SyncState(), ESyncState::InSync); } else { EXPECT_EQ(Proxy->GetHandle_LowLevel()->SyncState(), ESyncState::HardDesync); } } else { EXPECT_EQ(Proxy->GetHandle_LowLevel()->SyncState(), ESyncState::InSync); } } virtual int32 TriggerRewindIfNeeded_Internal(int32 LastCompletedStep) override { if (LastCompletedStep == ResimEndFrame) { return ResimStartFrame; } return INDEX_NONE; } int32 ResimStartFrame = 1; int32 ResimEndFrame = 10; }; const int32 LastGameStep = 32; const int32 NumPhysSteps = FMath::TruncToInt(LastGameStep / SimDt); auto UniqueRewindCallback = MakeUnique(); auto RewindCallback = UniqueRewindCallback.Get(); RewindCallback->Proxy = Proxy; RewindCallback->RewindData = Solver->GetRewindData(); Solver->SetRewindCallback(MoveTemp(UniqueRewindCallback)); auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGravityEnabled(false); Particle.SetV(FVec3(0, 0, 1)); for (int Step = 0; Step <= LastGameStep; ++Step) { if(Step == 3) { Particle.AddForce(FVec3(0, 0, -10)); } TickSolverHelper(Solver); } }); } GTEST_TEST(AllTraits, RewindTest_ResimInSync2) { //different velocity during resim step 5 which should cause a desync TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { struct FRewindCallback : public IRewindCallback { FSingleParticlePhysicsProxy* Proxy; FRewindData* RewindData; virtual void ProcessInputs_Internal(int32 PhysicsStep, const TArray& SimCallbackInputs) override { bool bIsResimming = RewindData->IsResim(); if (PhysicsStep == 2) { Proxy->GetPhysicsThreadAPI()->SetV(FVec3(0,0,2)); } if (bIsResimming) { if(PhysicsStep == 3) { //only setting velocity during resim, but exact same value so should still be in sync Proxy->GetPhysicsThreadAPI()->SetV(FVec3(0, 0, 2)); } if(PhysicsStep == 5) { Proxy->GetPhysicsThreadAPI()->SetV(FVec3(0, 0, 5)); } } if (bIsResimming) { EXPECT_EQ(Proxy->GetHandle_LowLevel()->SyncState(), PhysicsStep <= 5 ? ESyncState::InSync : ESyncState::HardDesync); } else { EXPECT_EQ(Proxy->GetHandle_LowLevel()->SyncState(), ESyncState::InSync); } } virtual int32 TriggerRewindIfNeeded_Internal(int32 LastCompletedStep) override { if (LastCompletedStep == ResimEndFrame) { return ResimStartFrame; } return INDEX_NONE; } int32 ResimStartFrame = 1; int32 ResimEndFrame = 10; }; const int32 LastGameStep = 32; const int32 NumPhysSteps = FMath::TruncToInt(LastGameStep / SimDt); auto UniqueRewindCallback = MakeUnique(); auto RewindCallback = UniqueRewindCallback.Get(); RewindCallback->Proxy = Proxy; RewindCallback->RewindData = Solver->GetRewindData(); Solver->SetRewindCallback(MoveTemp(UniqueRewindCallback)); auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGravityEnabled(false); Particle.SetV(FVec3(0, 0, 1)); for (int Step = 0; Step <= LastGameStep; ++Step) { TickSolverHelper(Solver); } }); } GTEST_TEST(AllTraits, RewindTest_ResimInSync3) { //different position during resim step 5 which should cause a desync //want a completely clean property to make sure we properly update resim buffer when property is dirtied only on second run TRewindHelper::TestEmpty([](auto* Solver, FReal SimDt, int32 Optimization) { struct FRewindCallback : public IRewindCallback { FSingleParticlePhysicsProxy* Proxy; FRewindData* RewindData; virtual void ProcessInputs_Internal(int32 PhysicsStep, const TArray& SimCallbackInputs) override { bool bIsResimming = RewindData->IsResim(); if (bIsResimming) { if (PhysicsStep == 5) { Proxy->GetPhysicsThreadAPI()->SetX(FVec3(0, 0, 5)); } } if (bIsResimming) { EXPECT_EQ(Proxy->GetHandle_LowLevel()->SyncState(), PhysicsStep <= 5 ? ESyncState::InSync : ESyncState::HardDesync); } else { EXPECT_EQ(Proxy->GetHandle_LowLevel()->SyncState(), ESyncState::InSync); } } virtual int32 TriggerRewindIfNeeded_Internal(int32 LastCompletedStep) override { if (LastCompletedStep == ResimEndFrame) { return ResimStartFrame; } return INDEX_NONE; } int32 ResimStartFrame = 1; int32 ResimEndFrame = 10; }; auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10)); auto Proxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); Proxy->GetGameThreadAPI().SetGeometry(Sphere); Solver->RegisterObject(Proxy); const int32 LastGameStep = 32; const int32 NumPhysSteps = FMath::TruncToInt(LastGameStep / SimDt); auto UniqueRewindCallback = MakeUnique(); auto RewindCallback = UniqueRewindCallback.Get(); RewindCallback->Proxy = Proxy; RewindCallback->RewindData = Solver->GetRewindData(); Solver->SetRewindCallback(MoveTemp(UniqueRewindCallback)); for (int Step = 0; Step <= LastGameStep; ++Step) { TickSolverHelper(Solver); } }); } GTEST_TEST(AllTraits, RewindTest_ResimInSync4) { //different position during resim step 5 which should cause a desync //want a completely clean property to make sure we properly update resim buffer when property is dirtied only on second run //add a dirty property every frame to make sure we are getting an entry in the original sim, but with a clean property //unrelated property must be dirtied on GT because simcallback dirty just copies all properties at the moment TRewindHelper::TestEmpty([](auto* Solver, FReal SimDt, int32 Optimization) { //this test requires dirty writes from GT //we want those to line up with PT tick 1 to 1 if (SimDt != 1) { return; } struct FRewindCallback : public IRewindCallback { FSingleParticlePhysicsProxy* Proxy; FRewindData* RewindData; virtual void ProcessInputs_Internal(int32 PhysicsStep, const TArray& SimCallbackInputs) override { bool bIsResimming = true; if (PhysicsStep > HighestStep) { bIsResimming = false; HighestStep = PhysicsStep; } ensure(bIsResimming == RewindData->IsResim()); // this would catch if IsResim is lieing to us if (bIsResimming) { if (PhysicsStep == 5) { Proxy->GetPhysicsThreadAPI()->SetX(FVec3(0, 0, 5)); } } if(bIsResimming) { EXPECT_EQ(Proxy->GetHandle_LowLevel()->SyncState(), PhysicsStep <= 5 ? ESyncState::InSync : ESyncState::HardDesync); } else { EXPECT_EQ(Proxy->GetHandle_LowLevel()->SyncState(), ESyncState::InSync); } } virtual int32 TriggerRewindIfNeeded_Internal(int32 LastCompletedStep) override { if (LastCompletedStep == ResimEndFrame) { return ResimStartFrame; } return INDEX_NONE; } int32 ResimStartFrame = 1; int32 ResimEndFrame = 10; int32 HighestStep = INDEX_NONE; }; auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10)); auto Proxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); Proxy->GetGameThreadAPI().SetGeometry(Sphere); Solver->RegisterObject(Proxy); const int32 LastGameStep = 32; const int32 NumPhysSteps = FMath::TruncToInt(LastGameStep / SimDt); auto UniqueRewindCallback = MakeUnique(); auto RewindCallback = UniqueRewindCallback.Get(); RewindCallback->Proxy = Proxy; RewindCallback->RewindData = Solver->GetRewindData(); Solver->SetRewindCallback(MoveTemp(UniqueRewindCallback)); for (int Step = 0; Step <= LastGameStep; ++Step) { Proxy->GetGameThreadAPI().SetV(FVec3(0, 0, Step)); //dirty an unrelated property to still get an entry in the history buffer TickSolverHelper(Solver); } }); } GTEST_TEST(AllTraits, RewindTest_ResimSleepChangeRewind) { // Test puting object to sleep during Resim TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { int32 ExpectedSleepFrame = INDEX_NONE; const int32 ResimStartFrame = 1; FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver, 11, ResimStartFrame); Helper->ProcessInputsFunc = [&ExpectedSleepFrame, ResimStartFrame, Proxy, RewindData = Solver->GetRewindData()](const int32 PhysicsStep, bool bIsResimming) { if (bIsResimming) { if (PhysicsStep >= ResimStartFrame + 2) { Proxy->GetPhysicsThreadAPI()->SetObjectState(EObjectStateType::Sleeping, false, false); ExpectedSleepFrame = ResimStartFrame + 3; } else { Proxy->GetPhysicsThreadAPI()->SetObjectState(EObjectStateType::Dynamic, false, false); } return; } for (int32 Step = ResimStartFrame; Step < PhysicsStep; ++Step) { const EObjectStateType OldState = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step).ObjectState(); if (ExpectedSleepFrame != INDEX_NONE && Step >= ExpectedSleepFrame) { EXPECT_EQ(OldState, EObjectStateType::Sleeping); } else { EXPECT_EQ(OldState, EObjectStateType::Dynamic); } } }; auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGravityEnabled(false); Particle.SetV(FVec3(0, 0, 1)); for (int Step = 0; Step <= 32; ++Step) { TickSolverHelper(Solver); } }); } GTEST_TEST(AllTraits, RewindTest_ResimSleepChangeRewind2) { // This does two ObjectState corrections // -Object starts out asleep // -On step 10 we force a rewind to frame 4 and wake it up during the resim (apply "correction") // -This on its own works fine // -On step 12 we force another rewind to frame 6 and put it to sleep during the resim // -This seems to have no effect: the object stays awake // // TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { struct FRewindCallback : public IRewindCallback { FSingleParticlePhysicsProxy* Proxy; FRewindData* RewindData; virtual void ProcessInputs_Internal(int32 PhysicsStep, const TArray& SimCallbackInputs) override { if (bIsResimming) { if (ForceAwakeFrame != INDEX_NONE && PhysicsStep == ForceAwakeFrame) { Proxy->GetPhysicsThreadAPI()->SetObjectState(EObjectStateType::Dynamic); } if (ForceSleepFrame != INDEX_NONE && PhysicsStep == ForceSleepFrame) { Proxy->GetPhysicsThreadAPI()->SetObjectState(EObjectStateType::Sleeping); } return; } for (int32 Step = ResimStartFrame; Step < PhysicsStep; ++Step) { const EObjectStateType OldState = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step).ObjectState(); if (TestAwakeFrame == INDEX_NONE || Step < TestAwakeFrame) { // If we haven't applied the first awake correction yet, or this is a step prior to that correction, we should be asleep EXPECT_EQ(OldState, EObjectStateType::Sleeping); } else if (TestSleepFrame != INDEX_NONE && Step >= TestSleepFrame) { // If we *have* applied the second sleep correction and this is a step on or after that correction, we should be asleep EXPECT_EQ(OldState, EObjectStateType::Sleeping); } else { // Everything else falls in the middle, we should be awake EXPECT_EQ(OldState, EObjectStateType::Dynamic); } } } virtual int32 TriggerRewindIfNeeded_Internal(int32 LastCompletedStep) override { if (bIsResimming) { return INDEX_NONE; } if (LastCompletedStep == 10) { ForceAwakeFrame = 4; bIsResimming = true; ResimEndFrame = LastCompletedStep; TestAwakeFrame = ForceAwakeFrame+1; ResimStartFrame = ForceAwakeFrame; return ResimStartFrame; } if (LastCompletedStep == 12) { ForceSleepFrame = 6; bIsResimming = true; ResimEndFrame = LastCompletedStep; TestSleepFrame = ForceSleepFrame+1; ResimStartFrame = ForceSleepFrame; return ResimStartFrame; } return INDEX_NONE; } void PostResimStep_Internal(int32 PhysicsStep) override { if (ResimEndFrame == PhysicsStep) { ForceAwakeFrame = INDEX_NONE; ForceSleepFrame = INDEX_NONE; bIsResimming = false; } } int32 ForceAwakeFrame = INDEX_NONE; int32 ForceSleepFrame = INDEX_NONE; int32 TestAwakeFrame = INDEX_NONE; int32 TestSleepFrame = INDEX_NONE; int32 ResimStartFrame = 0; bool bIsResimming = false; int32 ResimEndFrame = INDEX_NONE; }; const int32 LastGameStep = 32; const int32 NumPhysSteps = FMath::TruncToInt(LastGameStep / SimDt); auto UniqueRewindCallback = MakeUnique(); auto RewindCallback = UniqueRewindCallback.Get(); RewindCallback->Proxy = Proxy; RewindCallback->RewindData = Solver->GetRewindData(); Solver->SetRewindCallback(MoveTemp(UniqueRewindCallback)); auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGravityEnabled(false); Particle.SetV(FVec3(0, 0, 1)); Particle.SetObjectState(EObjectStateType::Sleeping); for (int Step = 0; Step <= LastGameStep; ++Step) { TickSolverHelper(Solver); } }); } GTEST_TEST(AllTraits, RewindTest_RewindBeforeSleep) { // Rewind to before an object was put to sleep and see that the Active view is valid TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { int32 ExpectedSleepFrame = INDEX_NONE; const int32 ResimStartFrame = 0; FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver, 11, ResimStartFrame); Helper->ProcessInputsFunc = [&ExpectedSleepFrame, ResimStartFrame, Proxy, RewindData = Solver->GetRewindData(), Solver](const int32 PhysicsStep, bool bIsResimming) { if(PhysicsStep == 5) { Proxy->GetPhysicsThreadAPI()->SetV(FVec3(0, 0, 0)); Proxy->GetPhysicsThreadAPI()->SetObjectState(EObjectStateType::Sleeping); } //before sleep the active view contains the particle, after we put it to sleep the active view is empty EXPECT_EQ(Solver->GetParticles().GetActiveParticlesView().Num(), PhysicsStep < 5 ? 1 : 0); }; auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGravityEnabled(false); Particle.SetV(FVec3(0, 0, 1)); for (int Step = 0; Step <= 32; ++Step) { TickSolverHelper(Solver); } }); } GTEST_TEST(AllTraits, RewindTest_RewindBeforeAwake) { // Rewind to before an object was made awake and confirm that the active view is valid // This test rewinds to step 3 because it ensures there's no PushData which may fixup the view. Need to make sure it's fixed regardless of push data TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { int32 ExpectedSleepFrame = INDEX_NONE; const int32 ResimStartFrame = 3; FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver, 13, ResimStartFrame); Helper->ProcessInputsFunc = [&ExpectedSleepFrame, ResimStartFrame, Proxy, RewindData = Solver->GetRewindData(), Solver](const int32 PhysicsStep, bool bIsResimming) { if (PhysicsStep == 12) { Proxy->GetPhysicsThreadAPI()->SetV(FVec3(0, 0, 1)); Proxy->GetPhysicsThreadAPI()->SetObjectState(EObjectStateType::Dynamic); } //before wake up the active view was empty, after we wake it up the view contains the particle EXPECT_EQ(Solver->GetParticles().GetActiveParticlesView().Num(), PhysicsStep < 12 ? 0 : 1); }; auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGravityEnabled(false); Particle.SetObjectState(EObjectStateType::Sleeping); for (int Step = 0; Step <= 32; ++Step) { TickSolverHelper(Solver); } }); } GTEST_TEST(AllTraits, RewindTest_RewindBeforeMadeKinematic) { // Rewind before a dynamic was made kinematic and check the view is properly handling the rewind TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { int32 ExpectedSleepFrame = INDEX_NONE; const int32 ResimStartFrame = 3; FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver, 13, ResimStartFrame); Helper->ProcessInputsFunc = [&ExpectedSleepFrame, ResimStartFrame, Proxy, RewindData = Solver->GetRewindData(), Solver](const int32 PhysicsStep, bool bIsResimming) { if (PhysicsStep == 12) { Proxy->GetPhysicsThreadAPI()->SetObjectState(EObjectStateType::Kinematic); } if(PhysicsStep < 12) { EXPECT_EQ(Solver->GetParticles().GetNonDisabledDynamicView().Num(), 1); EXPECT_EQ(Solver->GetParticles().GetActiveKinematicParticlesView().Num(), 0); } else { EXPECT_EQ(Solver->GetParticles().GetNonDisabledDynamicView().Num(), 0); EXPECT_EQ(Solver->GetParticles().GetActiveKinematicParticlesView().Num(), 1); } }; auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGravityEnabled(false); Particle.SetV(FVec3(0, 0, 1)); for (int Step = 0; Step <= 32; ++Step) { TickSolverHelper(Solver); } }); } GTEST_TEST(AllTraits, RewindTest_RewindBeforeMadeDynamic) { // Rewind before a kinematic was made dynamic and check the view is properly handling the rewind TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { int32 ExpectedSleepFrame = INDEX_NONE; const int32 ResimStartFrame = 3; FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver, 13, ResimStartFrame); Helper->ProcessInputsFunc = [&ExpectedSleepFrame, ResimStartFrame, Proxy, RewindData = Solver->GetRewindData(), Solver](const int32 PhysicsStep, bool bIsResimming) { if (PhysicsStep == 12) { Proxy->GetPhysicsThreadAPI()->SetObjectState(EObjectStateType::Dynamic); } if (PhysicsStep < 12) { EXPECT_EQ(Solver->GetParticles().GetNonDisabledDynamicView().Num(), 0); EXPECT_EQ(Solver->GetParticles().GetActiveKinematicParticlesView().Num(), 1); } else { EXPECT_EQ(Solver->GetParticles().GetNonDisabledDynamicView().Num(), 1); EXPECT_EQ(Solver->GetParticles().GetActiveKinematicParticlesView().Num(), 0); } }; auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGravityEnabled(false); Proxy->GetGameThreadAPI().SetObjectState(EObjectStateType::Kinematic); for (int Step = 0; Step <= 32; ++Step) { TickSolverHelper(Solver); } }); } GTEST_TEST(AllTraits, RewindTest_RecordForcesInSimCallback) { //Makes sure that we record the forces applied during sim callback TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver); Helper->ProcessInputsFunc = [Proxy, RewindData = Solver->GetRewindData(), Optimization](const int32 PhysicsStep, bool bIsResimming) { if (PhysicsStep == 3) { Proxy->GetPhysicsThreadAPI()->SetAcceleration(FVec3(0, 0, 10) + Proxy->GetPhysicsThreadAPI()->Acceleration(), 0); Proxy->GetPhysicsThreadAPI()->SetAngularAcceleration(FVec3(0, 0, 10) + Proxy->GetPhysicsThreadAPI()->AngularAcceleration()); } for (int32 Step = 0; Step < PhysicsStep; ++Step) { if (Optimization != 2) // Optimization 2 does not cache data in PrePushData so no need to check that phase { //Before push no force or torque at all FGeometryParticleState PrePushState = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step, FFrameAndPhase::EParticleHistoryPhase::PrePushData); EXPECT_EQ(PrePushState.Acceleration()[2], 0); EXPECT_EQ(PrePushState.AngularAcceleration()[2], 0); } { //After push (but before callback) only see GT force, and no torque FGeometryParticleState PostPushState = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step, FFrameAndPhase::EParticleHistoryPhase::PostPushData); EXPECT_EQ(PostPushState.Acceleration()[2], 1); //GT always sets force of 1 EXPECT_EQ(PostPushState.AngularAcceleration()[2], 0); //GT never sets torque } if (Optimization != 2) // Optimization 2 does not cache data in PostCallbacks so no need to check that phase { FGeometryParticleState State = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step, FFrameAndPhase::EParticleHistoryPhase::PostCallbacks); if (Step == 3) { EXPECT_EQ(State.Acceleration()[2], 11); //1 from GT + 10 from callback EXPECT_EQ(State.AngularAcceleration()[2], 10); //10 from callback (nothing from GT) } else { EXPECT_EQ(State.Acceleration()[2], 1); //GT always sets force of 1 EXPECT_EQ(State.AngularAcceleration()[2], 0); //GT never sets torque } } } }; auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGravityEnabled(false); for (int Step = 0; Step <= 32; ++Step) { Particle.AddForce(FVec3(0, 0, 1)); TickSolverHelper(Solver); } }); } GTEST_TEST(AllTraits, RewindTest_AddForce) { TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { auto& Particle = Proxy->GetGameThreadAPI(); const int32 LastGameStep = 20; for (int Step = 0; Step <= LastGameStep; ++Step) { //sim-writable property that changes every step Particle.AddForce(FVec3(0, 0, Step + 1)); TickSolverHelper(Solver); } const FRewindData* RewindData = Solver->GetRewindData(); //check state at every step except latest const int32 LastSimStep = LastGameStep / SimDt; for (int Step = 0; Step < LastSimStep - 1; ++Step) { const auto ParticleState = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step); FReal ExpectedForce = Step + 1; if (SimDt < 1) { //each sub-step gets a constant force applied ExpectedForce = FMath::FloorToFloat(Step * SimDt) + 1; } else if (SimDt > 1) { //each step gets an average of the forces applied ((step+1) + (step+2) + (step+3) + (step+4))/4 = step + (1+2+3+4)/4 = step + 2.5 //where step is game step: so really it's step * 4 ExpectedForce = Step * 4 + 2.5; } EXPECT_EQ(ParticleState.Acceleration()[2], ExpectedForce); } }); } GTEST_TEST(AllTraits, RewindTest_IntermittentForce) { TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { auto& Particle = Proxy->GetGameThreadAPI(); const int32 LastGameStep = 20; for (int Step = 0; Step <= LastGameStep; ++Step) { //sim-writable property that changes infrequently and not at beginning if (Step == 3) { Particle.AddForce(FVec3(0, 0, Step)); } if (Step == 5) { Particle.AddForce(FVec3(0, 0, Step)); } TickSolverHelper(Solver); } const FRewindData* RewindData = Solver->GetRewindData(); //check state at every step except latest const int32 LastSimStep = LastGameStep / SimDt; for (int Step = 0; Step < LastSimStep - 1; ++Step) { const auto ParticleState = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step); if (SimDt <= 1) { const float SimTime = Step * SimDt; if (SimTime >= 3 && SimTime < 4) { EXPECT_EQ(ParticleState.Acceleration()[2], 3); } else if (SimTime >= 5 && SimTime < 6) { EXPECT_EQ(ParticleState.Acceleration()[2], 5); } else { EXPECT_EQ(ParticleState.Acceleration()[2], 0); } } else { //we get an average if (Step == 0) { EXPECT_EQ(ParticleState.Acceleration()[2], 3 / 4.f); } else if (Step == 1) { EXPECT_EQ(ParticleState.Acceleration()[2], 5 / 4.f); } else { EXPECT_EQ(ParticleState.Acceleration()[2], 0); } } } }); } GTEST_TEST(AllTraits, RewindTest_IntermittentGeomChange) { TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { auto& Particle = Proxy->GetGameThreadAPI(); auto Box = Chaos::FImplicitObjectPtr(new TBox(FVec3(0), FVec3(1))); auto Box2 = Chaos::FImplicitObjectPtr(new TBox(FVec3(2), FVec3(3))); const int32 LastGameStep = 20; for (int Step = 0; Step <= LastGameStep; ++Step) { //property that changes once half way through if (Step == 3) { Particle.SetGeometry(Box); } if (Step == 5) { Particle.SetGeometry(Box2); } if (Step == 7) { Particle.SetGeometry(Box); } TickSolverHelper(Solver); } const FRewindData* RewindData = Solver->GetRewindData(); //check state at every step except latest const int32 LastSimStep = LastGameStep / SimDt; for (int Step = 0; Step < LastSimStep - 1; ++Step) { const auto ParticleState = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step); if (SimDt <= 1) { const float SimTime = Step * SimDt; if (SimTime < 3) { //was sphere EXPECT_EQ(ParticleState.GetGeometry(), Sphere); } else if (SimTime < 5 || SimTime >= 7) { //then became box EXPECT_EQ(ParticleState.GetGeometry(), Box.GetReference()); } else { //second box EXPECT_EQ(ParticleState.GetGeometry(), Box2.GetReference()); } } else { //changes happen within interval so stays box entire time EXPECT_EQ(ParticleState.GetGeometry(), Box.GetReference()); } } }); } GTEST_TEST(AllTraits, RewindTest_FallingObjectWithTeleport) { TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { auto& Particle = Proxy->GetGameThreadAPI(); Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0); Particle.SetGravityEnabled(true); Particle.SetX(FVec3(0, 0, 100)); const int32 LastGameStep = 20; for (int Step = 0; Step <= LastGameStep; ++Step) { //teleport from GT if (Step == 5) { Particle.SetX(FVec3(0, 0, 10)); Particle.SetV(FVec3(0, 0, 0)); } TickSolverHelper(Solver); } const FRewindData* RewindData = Solver->GetRewindData(); //check state at every step except latest const int32 LastSimStep = LastGameStep / SimDt; FReal ExpectedVZ = 0; FReal ExpectedXZ = 100; for (int Step = 0; Step < LastSimStep - 1; ++Step) { const auto ParticleState = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step); const FReal SimStart = SimDt * Step; const FReal SimEnd = SimDt * (Step + 1); if (SimStart <= 5 && SimEnd > 5) { ExpectedVZ = 0; ExpectedXZ = 10; } EXPECT_NEAR(ParticleState.GetX()[2], ExpectedXZ, 1e-4); EXPECT_NEAR(ParticleState.GetV()[2], ExpectedVZ, 1e-4); ExpectedVZ -= SimDt; ExpectedXZ += ExpectedVZ * SimDt; } }); } struct FSimCallbackHelperInput : FSimCallbackInput { void Reset() {} int InCounter; }; struct FSimCallbackHelperOutput : FSimCallbackOutput { void Reset() {} int OutCounter; }; GTEST_TEST(AllTraits, RewindTest_SimCallbackInputsOutputs) { TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { struct FSimCallbackHelper : TSimCallbackObject { int32 TriggerCount = 0; virtual void OnPreSimulate_Internal() override { GetProducerOutputData_Internal().OutCounter = TriggerCount++; } }; struct FRewindCallback : public IRewindCallback { FRewindCallback(int32 InNumPhysicsSteps, FReal InSimDt) : NumPhysicsSteps(InNumPhysicsSteps) , SimDt(InSimDt) { } TArray InCounters; int32 NumPhysicsSteps; bool bResim = false; FReal SimDt; virtual int32 TriggerRewindIfNeeded_Internal(int32 LastCompletedStep) override { if (LastCompletedStep + 1 == NumPhysicsSteps && NumPhysicsSteps != INDEX_NONE) { NumPhysicsSteps = INDEX_NONE; //don't resim again after this return 0; } return INDEX_NONE; } virtual void ProcessInputs_External(int32 PhysicsStep, const TArray& SimCallbackInputs) override { EXPECT_EQ(bResim, false); //should never be triggered during resim, since resim happens on internal thread EXPECT_EQ(SimCallbackInputs.Num(), 1); FSimCallbackHelperInput* Input = static_cast(SimCallbackInputs[0].Input); if(SimDt > 1) { //several external ticks before we finally get the final input EXPECT_EQ(FMath::TruncToInt((PhysicsStep+1) * SimDt - 1), Input->InCounter); } else { //potentially the same input over multiple sub-steps EXPECT_EQ(FMath::TruncToInt(PhysicsStep * SimDt), Input->InCounter); } } virtual void ProcessInputs_Internal(int32 PhysicsStep, const TArray& SimCallbackInputs) override { EXPECT_EQ(SimCallbackInputs.Num(), 1); FSimCallbackHelperInput* Input = static_cast(SimCallbackInputs[0].Input); if(bResim) { EXPECT_EQ(InCounters[PhysicsStep], Input->InCounter); } else { InCounters.Add(Input->InCounter); } } virtual void PreResimStep_Internal(int32 PhysicsStep, bool bFirst) override { bResim = true; } virtual void PostResimStep_Internal(int32 PhysicsStep) override { bResim = false; } }; const int32 LastGameStep = 20; const int32 NumPhysSteps = FMath::TruncToInt(LastGameStep / SimDt); Solver->SetRewindCallback(MakeUnique(NumPhysSteps, SimDt)); FSimCallbackHelper* SimCallback = Solver->template CreateAndRegisterSimCallbackObject_External(); { auto& Particle = Proxy->GetGameThreadAPI(); Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0); Particle.SetGravityEnabled(true); Particle.SetX(FVec3(0, 0, 100)); for (int Step = 0; Step < LastGameStep; ++Step) { SimCallback->GetProducerInputData_External()->InCounter = Step; TickSolverHelper(Solver); } } //during async we can't consume all outputs right away, so we won't get the resim results right away //with sync we should be able to get all the results right away and this acts as a good test if(!Solver->IsUsingAsyncResults()) { //we get all the original outputs plus the rewind outputs, they counter just keeps going up, but the InternalTime should reflect the rewind int32 Count = 0; FReal CurTime = 0.f; while (TSimCallbackOutputHandle Output = SimCallback->PopOutputData_External()) { EXPECT_FLOAT_EQ(Output->InternalTime, CurTime); EXPECT_EQ(Count, Output->OutCounter); ++Count; if(Count == NumPhysSteps) { CurTime = 0; //reset time for resim } else { CurTime += SimDt; } } EXPECT_EQ(Count, NumPhysSteps * 2); //should have two results for each physics step since we rewound } }); } struct FSimCallbackHelperInput2 : FSimCallbackInput { void Reset() { StepToCounter.Reset(); } TMap StepToCounter; }; GTEST_TEST(AllTraits, RewindTest_SimCallbackProcessExternalInputs) { //If inputs are not set until external callback, make sure they are associated with the right frame TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { struct FSimCallbackHelper : TSimCallbackObject { FPBDRigidsSolver* Solver = nullptr; virtual void OnPreSimulate_Internal() override { EXPECT_EQ(GetConsumerInput_Internal()->StepToCounter[Solver->GetCurrentFrame()], Solver->GetCurrentFrame()); } }; struct FRewindCallback : public IRewindCallback { FSimCallbackHelper* SimCallback; FRewindCallback(FSimCallbackHelper* Callback) : SimCallback(Callback){} virtual void InjectInputs_External(int32 PhysicsStep, int32 NumSteps) override { for(int32 Idx = 0; Idx < NumSteps; ++Idx) { const int32 Step = Idx + PhysicsStep; SimCallback->GetProducerInputData_External()->StepToCounter.Add(Step, Step); } } }; FSimCallbackHelper* SimCallback = Solver->template CreateAndRegisterSimCallbackObject_External(); SimCallback->Solver = Solver; Solver->SetRewindCallback(MakeUnique(SimCallback)); for (int Step = 0; Step < 32; ++Step) { TickSolverHelper(Solver); } }); } GTEST_TEST(AllTraits, RewindTest_SimCallbackInputsCorrection) { TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { struct FSimCallbackHelper : TSimCallbackObject { int32 TriggerCount = 0; virtual void OnPreSimulate_Internal() override { GetProducerOutputData_Internal().OutCounter = GetConsumerInput_Internal()->InCounter; } }; struct FRewindCallback : public IRewindCallback { FRewindCallback(int32 InNumPhysicsSteps) : NumPhysicsSteps(InNumPhysicsSteps) { } TArray InCounters; int32 NumPhysicsSteps; bool bResim = false; const int32 Correction = -10; virtual int32 TriggerRewindIfNeeded_Internal(int32 LastCompletedStep) override { if (LastCompletedStep + 1 == NumPhysicsSteps && NumPhysicsSteps != INDEX_NONE) { NumPhysicsSteps = INDEX_NONE; //don't resim again after this return 0; } return INDEX_NONE; } virtual void ProcessInputs_Internal(int32 PhysicsStep, const TArray& SimCallbackInputs) override { EXPECT_EQ(SimCallbackInputs.Num(), 1); FSimCallbackHelperInput* Input = static_cast(SimCallbackInputs[0].Input); if (bResim) { Input->InCounter = InCounters[PhysicsStep] + Correction; EXPECT_EQ(InCounters[PhysicsStep] + Correction, Input->InCounter); } else { InCounters.Add(Input->InCounter); } } virtual void PreResimStep_Internal(int32 PhysicsStep, bool bFirst) override { bResim = true; } virtual void PostResimStep_Internal(int32 PhysicsStep) override { bResim = false; } }; const int32 LastGameStep = 20; const int32 NumPhysSteps = FMath::TruncToInt(LastGameStep / SimDt); auto UniqueRewindCallback = MakeUnique(NumPhysSteps); auto RewindCallback = UniqueRewindCallback.Get(); Solver->SetRewindCallback(MoveTemp(UniqueRewindCallback)); FSimCallbackHelper* SimCallback = Solver->template CreateAndRegisterSimCallbackObject_External(); { auto& Particle = Proxy->GetGameThreadAPI(); Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0); Particle.SetGravityEnabled(true); Particle.SetX(FVec3(0, 0, 100)); for (int Step = 0; Step < LastGameStep; ++Step) { SimCallback->GetProducerInputData_External()->InCounter = Step; TickSolverHelper(Solver); } } //during async we can't consume all outputs right away, so we won't get the resim results right away //with sync we should be able to get all the results right away and this acts as a good test if (!Solver->IsUsingAsyncResults()) { //we get all the original outputs plus the rewind outputs, the correction happens at NumPhysSteps and then the outputs should be the resim + correction int32 Count = 0; FReal CurTime = 0.f; int32 Correction = 0; while (TSimCallbackOutputHandle Output = SimCallback->PopOutputData_External()) { EXPECT_FLOAT_EQ(Output->InternalTime, CurTime); const int32 ExpectedResult = FMath::FloorToInt(Output->InternalTime * SimDt) + Correction; EXPECT_EQ(ExpectedResult, Output->OutCounter); ++Count; if (Count == NumPhysSteps) { CurTime = 0; //reset time for resim Correction = RewindCallback->Correction; } else { CurTime += SimDt; } } EXPECT_EQ(Count, NumPhysSteps * 2); //should have two results for each physics step since we rewound } }); } GTEST_TEST(AllTraits, DISABLED_RewindTest_SimCallbackCorrectionInterpolation) { // NOTE: Disabled because the logic that is being tested is no longer used. /*want to interpolate between original sim and correction edge cases to test: - gt write should stomp interpolation - make sure sleep state works - not moving in original, but moving in resim - moving in original, but not moving in resim - moving along one axis and corrected on another - resim while interpolation is still happening */ TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { //only care about resim when async results are used if (Solver->IsUsingAsyncResults() == false) { return; } struct FRewindCallback : public IRewindCallback { FRewindCallback(int32 InNumPhysicsSteps) : NumPhysicsSteps(InNumPhysicsSteps) { } int32 NumPhysicsSteps; bool bResim = false; bool bPendingCorrection = true; const FReal ZCorrection = 100; FSingleParticlePhysicsProxy* Proxy; virtual int32 TriggerRewindIfNeeded_Internal(int32 LastCompletedStep) override { if (LastCompletedStep + 1 == NumPhysicsSteps && NumPhysicsSteps != INDEX_NONE) { NumPhysicsSteps = INDEX_NONE; //don't resim again after this return 0; } return INDEX_NONE; } virtual void ProcessInputs_Internal(int32 PhysicsStep, const TArray& SimCallbackInputs) override { if (bResim && bPendingCorrection) { Proxy->GetPhysicsThreadAPI()->SetX(FVec3(0, 0, ZCorrection)); bPendingCorrection = false; } } virtual void PreResimStep_Internal(int32 PhysicsStep, bool bFirst) override { bResim = true; } virtual void PostResimStep_Internal(int32 PhysicsStep) override { bResim = false; } }; const int32 LastGameStep = 20; const int32 NumPhysSteps = FMath::TruncToInt(LastGameStep / SimDt); auto UniqueRewindCallback = MakeUnique(NumPhysSteps); auto RewindCallback = UniqueRewindCallback.Get(); RewindCallback->Proxy = Proxy; const FReal GTDt = 1; FReal Time = 0; FReal ZStart = 0; const FReal ZVel = -1; FReal LastCorrectionStep = 0; Solver->SetRewindCallback(MoveTemp(UniqueRewindCallback)); Solver->SetAsyncInterpolationMultiplier(4.0f); { auto& Particle = Proxy->GetGameThreadAPI(); Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0); Particle.SetGravityEnabled(false); Particle.SetX(FVec3(0, 0, 0)); Particle.SetV(FVec3(0, 0, -1)); for (int Step = 0; Step < LastGameStep; ++Step) { TickSolverHelper(Solver); Time += GTDt; const FReal InterpolatedTime = Time - SimDt * Solver->GetAsyncInterpolationMultiplier(); if (InterpolatedTime < 0) { //not enough time to interpolate so just take initial value EXPECT_NEAR(Particle.X()[2], ZStart, 1e-2); } else { //interpolated EXPECT_NEAR(Particle.X()[2], ZStart + ZVel * InterpolatedTime, 1e-2); } } //resim happened Solver->SetAsyncInterpolationMultiplier(2.0f); for (int Step = LastGameStep; Step < 2*LastGameStep; ++Step) { const FReal PrevZ = Particle.X()[2]; TickSolverHelper(Solver); Time += GTDt; const FReal InterpolatedTime = Time - SimDt * Solver->GetAsyncInterpolationMultiplier(); if(InterpolatedTime > 20) //resim happened { ZStart = 100; //corrected to 100 start point } //expected interpolation from pt to gt const int32 NextSimStep = FMath::CeilToInt(InterpolatedTime / SimDt); const FReal NextSimStepTime = NextSimStep * SimDt; const FReal ExpectedValue = ZStart + ZVel * InterpolatedTime; // const FReal TargetValue = ZStart + ZVel * NextSimStepTime; FProxyInterpolationBase* InterpolationData = Proxy->GetInterpolationData(); if (InterpolationData && InterpolationData->IsErrorSmoothing()) { if (!InterpolationData->IsErrorVelocitySmoothing()) { const FReal CorrectionStep = Particle.X()[2] - PrevZ; if (LastCorrectionStep != 0) { // Make sure we have a linear correction EXPECT_NEAR(LastCorrectionStep, CorrectionStep, 1e-2); } LastCorrectionStep = CorrectionStep; } } else { if (!!LastCorrectionStep) { // Correction is now done, check if the object arrived at the current position by the correction or if it was snapped into place via not doing correction anymore EXPECT_NEAR(PrevZ, Particle.X()[2] - LastCorrectionStep, 1e-2); LastCorrectionStep = 0; } //no resim interpolation, just simple value interpolation EXPECT_NEAR(Particle.X()[2], ExpectedValue, 1e-2); } } } }); } GTEST_TEST(AllTraits, RewindTest_ResimFallingObjectWithTeleport) { TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { const int32 LastGameStep = 20; FReal ExpectedVZ = 0; FReal ExpectedXZ = 100; int32 FirstStepResim = INT_MAX; int32 NumResimSteps = 0; FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver, FMath::TruncToInt(LastGameStep / SimDt) ); Helper->ProcessInputsFunc = [Proxy, SimDt, &ExpectedVZ, &ExpectedXZ, &FirstStepResim, &NumResimSteps](const int32 Step, const bool bResim) { if(bResim) { FirstStepResim = FMath::Min(Step, FirstStepResim); NumResimSteps++; auto& Particle = *Proxy->GetPhysicsThreadAPI(); const float SimStart = SimDt * Step; const float SimEnd = SimDt * (Step + 1); if (SimStart <= 5 && SimEnd > 5) { ExpectedVZ = 0; ExpectedXZ = 10; Particle.SetX(FVec3(0, 0, 10)); Particle.SetV(FVec3(0, 0, 0)); } EXPECT_NEAR(Particle.X()[2], ExpectedXZ, 1e-4); EXPECT_NEAR(Particle.V()[2], ExpectedVZ, 1e-4); } }; Helper->PostFunc = [Proxy, SimDt, &ExpectedVZ, &ExpectedXZ](const int32 Step) { auto& Particle = *Proxy->GetPhysicsThreadAPI(); ExpectedVZ -= SimDt; ExpectedXZ += ExpectedVZ * SimDt; EXPECT_NEAR(Particle.X()[2], ExpectedXZ, 1e-4); EXPECT_NEAR(Particle.V()[2], ExpectedVZ, 1e-4); }; { auto& Particle = Proxy->GetGameThreadAPI(); Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0); Particle.SetGravityEnabled(true); Particle.SetX(FVec3(0, 0, 100)); for (int Step = 0; Step < LastGameStep; ++Step) { //teleport from GT if (Step == 5) { Particle.SetX(FVec3(0, 0, 10)); Particle.SetV(FVec3(0, 0, 0)); } TickSolverHelper(Solver); } } EXPECT_EQ(FirstStepResim, 0); EXPECT_EQ(NumResimSteps, LastGameStep / SimDt); //no desync so should be empty #if REWIND_DESYNC const TArray DesyncedParticles = RewindData->ComputeDesyncInfo(); EXPECT_EQ(DesyncedParticles.Num(), 0); #endif }); } GTEST_TEST(AllTraits, RewindTest_ResimFallingObjectWithTeleportAsFollower) { TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { const int32 LastGameStep = 20; { auto& Particle = Proxy->GetGameThreadAPI(); Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0); Particle.SetGravityEnabled(true); Particle.SetX(FVec3(0, 0, 100)); Particle.SetResimType(EResimType::ResimAsFollower); for (int Step = 0; Step <= LastGameStep; ++Step) { //teleport from GT if (Step == 5) { Particle.SetX(FVec3(0, 0, 10)); Particle.SetV(FVec3(0, 0, 0)); } TickSolverHelper(Solver); } } FPhysicsThreadContextScope Scope(true); FRewindData* RewindData = Solver->GetRewindData(); //RewindData->RewindToFrame(0); Solver->DisableAsyncMode(); //during resim we sim directly at fixed dt auto& Particle = *Proxy->GetPhysicsThreadAPI(); const int32 LastSimStep = LastGameStep / SimDt; FReal ExpectedVZ = 0; FReal ExpectedXZ = 100; for (int Step = 0; Step < LastSimStep - 1; ++Step) { const float SimStart = SimDt * Step; const float SimEnd = SimDt * (Step + 1); if (SimStart <= 5 && SimEnd > 5) { ExpectedVZ = 0; ExpectedXZ = 10; } else { #if REWIND_DESYNC //we'll see the teleport automatically because ResimAsFollower //but it's done by solver so before tick teleport is not known EXPECT_NEAR(Particle.X()[2], ExpectedXZ, 1e-4); EXPECT_NEAR(Particle.GetV()[2], ExpectedVZ, 1e-4); #endif } TickSolverHelper(Solver, SimDt); ExpectedVZ -= SimDt; ExpectedXZ += ExpectedVZ * SimDt; #if REWIND_DESYNC EXPECT_NEAR(Particle.X()[2], ExpectedXZ, 1e-4); EXPECT_NEAR(Particle.GetV()[2], ExpectedVZ, 1e-4); #endif } #if REWIND_DESYNC //no desync so should be empty const TArray DesyncedParticles = RewindData->ComputeDesyncInfo(); EXPECT_EQ(DesyncedParticles.Num(), 0); #endif }); } GTEST_TEST(AllTraits, RewindTest_NumDirty) { for (int Optimization = 0; Optimization < 2; ++Optimization) { auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10)); FChaosSolversModule* Module = FChaosSolversModule::GetModule(); // Make a solver auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1); InitSolverSettings(Solver); //note: this 5 is just a suggestion, there could be more frames saved than that Solver->EnableRewindCapture(5, !!Optimization); // Make particles auto Proxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGeometry(Sphere); Solver->RegisterObject(Proxy); Particle.SetGravityEnabled(true); for (int Step = 0; Step < 10; ++Step) { TickSolverHelper(Solver); const FRewindData* RewindData = Solver->GetRewindData(); EXPECT_EQ(RewindData->GetNumDirtyParticles(), 1); } //stop movement Particle.SetGravityEnabled(false); Particle.SetV(FVec3(0)); // Wait for sleep (active particles get added to the dirty list) // NOTE: Sleep requires 20 frames of inactivity by default, plus the time for smoothed velocity to damp to zero // (see FPBDIslandManager::SleepInactive) for(int Step = 0; Step < 500; ++Step) { TickSolverHelper(Solver); } { //enough frames with no changes so no longer dirty const FRewindData* RewindData = Solver->GetRewindData(); EXPECT_EQ(RewindData->GetNumDirtyParticles(), 0); } { //single change so back to being dirty Particle.SetGravityEnabled(true); TickSolverHelper(Solver); const FRewindData* RewindData = Solver->GetRewindData(); EXPECT_EQ(RewindData->GetNumDirtyParticles(), 1); } // Throw out the proxy Solver->UnregisterObject(Proxy); Module->DestroySolver(Solver); } } GTEST_TEST(AllTraits, RewindTest_Resim) { for (int Optimization = 0; Optimization < 2; ++Optimization) { auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10)); FChaosSolversModule* Module = FChaosSolversModule::GetModule(); // Make a solver auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1); InitSolverSettings(Solver); Solver->EnableRewindCapture(5, !!Optimization); // Make particles auto Proxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto KinematicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle()); const int32 LastStep = 12; TArray X; const int RewindStep = 7; FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver, LastStep, RewindStep); Helper->ProcessInputsFunc = [Proxy, KinematicProxy](const int32 Step, const bool bResim) { if (bResim) { if(Step == 7) { Proxy->GetPhysicsThreadAPI()->SetX(FVec3(0, 0, 100)); KinematicProxy->GetPhysicsThreadAPI()->SetX(FVec3(2)); } else if (Step == 8) { KinematicProxy->GetPhysicsThreadAPI()->SetX(FVec3(50)); } } }; { auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGeometry(Sphere); Solver->RegisterObject(Proxy); Particle.SetGravityEnabled(true); auto& Kinematic = KinematicProxy->GetGameThreadAPI(); Kinematic.SetGeometry(Sphere); Solver->RegisterObject(KinematicProxy); Kinematic.SetX(FVec3(2, 2, 2)); for (int Step = 0; Step <= LastStep; ++Step) { X.Add(Particle.X()); if (Step == 8) { Kinematic.SetX(FVec3(50, 50, 50)); } if (Step == 10) { Kinematic.SetX(FVec3(60, 60, 60)); } TickSolverHelper(Solver); } } #if REWIND_DESYNC for (int Step = RewindStep; Step <= LastStep; ++Step) { X[Step] = Particle.X(); TickSolverHelper(Solver); auto PTParticle = Proxy->GetHandle_LowLevel()->CastToRigidParticle(); //using handle directly because outside sim callback scope and we have ensures for that auto PTKinematic = KinematicProxy->GetHandle_LowLevel()->CastToKinematicParticle(); //see that particle has desynced if (Step < LastStep) { //If we're still in the past make sure future has been marked as desync FGeometryParticleState State(*Proxy->GetHandle_LowLevel()); EXPECT_EQ(EFutureQueryResult::Desync, RewindData->GetFutureStateAtFrame(State, Step)); EXPECT_EQ(PTParticle->SyncState(), ESyncState::HardDesync); FGeometryParticleState KinState(*KinematicProxy->GetHandle_LowLevel()); const EFutureQueryResult KinFutureStatus = RewindData->GetFutureStateAtFrame(KinState, Step); if (Step < 10) { EXPECT_EQ(KinFutureStatus, EFutureQueryResult::Ok); EXPECT_EQ(PTKinematic->SyncState(), ESyncState::InSync); } else { EXPECT_EQ(KinFutureStatus, EFutureQueryResult::Desync); EXPECT_EQ(PTKinematic->SyncState(), ESyncState::HardDesync); } } else { //Last resim frame ran so everything is marked as in sync EXPECT_EQ(PTParticle->SyncState(), ESyncState::InSync); EXPECT_EQ(PTKinematic->SyncState(), ESyncState::InSync); } } //expect both particles to be hard desynced const TArray DesyncedParticles = RewindData->ComputeDesyncInfo(); EXPECT_EQ(DesyncedParticles.Num(), 2); EXPECT_EQ(DesyncedParticles[0].MostDesynced, ESyncState::HardDesync); EXPECT_EQ(DesyncedParticles[1].MostDesynced, ESyncState::HardDesync); #endif #if 0 //can't read from resim data in public API EXPECT_EQ(Kinematic.X()[2], 50); //Rewound kinematic and only did one update, so use that first update //Make sure we recorded the new data for (int Step = RewindStep; Step <= LastStep; ++Step) { const FGeometryParticleState State = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step); EXPECT_EQ(State.X()[2], X[Step][2]); const FGeometryParticleState KinState = RewindData->GetPastStateAtFrame(*KinematicProxy->GetHandle_LowLevel(), Step); if (Step < 8) { EXPECT_EQ(KinState.X()[2], 2); } else { EXPECT_EQ(KinState.X()[2], 50); //in resim we didn't do second move, so recorded data must be updated } } #endif // Throw out the proxy Solver->UnregisterObject(Proxy); Module->DestroySolver(Solver); } } GTEST_TEST(AllTraits, RewindTest_ResimDesyncAfterMissingTeleport) { for (int Optimization = 0; Optimization < 2; ++Optimization) { auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10)); FChaosSolversModule* Module = FChaosSolversModule::GetModule(); // Make a solver auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1); InitSolverSettings(Solver); Solver->EnableRewindCapture(7, !!Optimization); // Make particles auto Proxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); const int LastStep = 11; TArray X; { auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGeometry(Sphere); Solver->RegisterObject(Proxy); Particle.SetGravityEnabled(true); for (int Step = 0; Step <= LastStep; ++Step) { if (Step == 7) { Particle.SetX(FVec3(0, 0, 5)); } if (Step == 9) { Particle.SetX(FVec3(0, 0, 1)); } X.Add(Particle.X()); TickSolverHelper(Solver); } X.Add(Particle.X()); } const int RewindStep = 5; #if PHYSICS_THREAD_CONTEXT FPhysicsThreadContextScope Scope(true); #endif auto& Particle = *Proxy->GetPhysicsThreadAPI(); FRewindData* RewindData = Solver->GetRewindData(); //EXPECT_TRUE(RewindData->RewindToFrame(RewindStep)); for (int Step = RewindStep; Step <= LastStep; ++Step) { #if REWIND_DESYNC FGeometryParticleState FutureState(*Proxy->GetHandle_LowLevel()); EXPECT_EQ(RewindData->GetFutureStateAtFrame(FutureState, Step + 1), Step < 10 ? EFutureQueryResult::Ok : EFutureQueryResult::Desync); if (Step < 10) { EXPECT_EQ(X[Step + 1][2], FutureState.X()[2]); } #endif if (Step == 7) { Particle.SetX(FVec3(0, 0, 5)); } //skip step 9 SetX to trigger a desync TickSolverHelper(Solver); #if REWIND_DESYNC //can't compare future with end of frame because we overwrite the result if (Step != 6 && Step != 8 && Step < 9) { EXPECT_EQ(Particle.X()[2], FutureState.X()[2]); } #endif } #if REWIND_DESYNC //expected desync const TArray DesyncedParticles = RewindData->ComputeDesyncInfo(); EXPECT_EQ(DesyncedParticles.Num(), 1); EXPECT_EQ(DesyncedParticles[0].MostDesynced, ESyncState::HardDesync); EXPECT_EQ(DesyncedParticles[0].Particle, Proxy->GetHandle_LowLevel()); #endif // Throw out the proxy Solver->UnregisterObject(Proxy); Module->DestroySolver(Solver); } } GTEST_TEST(AllTraits, RewindTest_ResimDesyncAfterChangingMass) { for (int Optimization = 0; Optimization < 2; ++Optimization) { auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10)); FChaosSolversModule* Module = FChaosSolversModule::GetModule(); // Make a solver auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1); InitSolverSettings(Solver); Solver->EnableRewindCapture(7, !!Optimization); FReal CurMass = 1.0; int32 LastStep = 11; // Make particles auto Proxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); { auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGeometry(Sphere); Solver->RegisterObject(Proxy); Particle.SetGravityEnabled(true); Particle.SetM(CurMass); for (int Step = 0; Step <= LastStep; ++Step) { if (Step == 7) { Particle.SetM(2); } if (Step == 9) { Particle.SetM(3); } TickSolverHelper(Solver); } } const int RewindStep = 5; #if PHYSICS_THREAD_CONTEXT FPhysicsThreadContextScope Scope(true); #endif FRewindData* RewindData = Solver->GetRewindData(); //EXPECT_TRUE(RewindData->RewindToFrame(RewindStep)); auto& Particle = *Proxy->GetPhysicsThreadAPI(); for (int Step = RewindStep; Step <= LastStep; ++Step) { #if REWIND_DESYNC FGeometryParticleState FutureState(*Proxy->GetHandle_LowLevel()); EXPECT_EQ(RewindData->GetFutureStateAtFrame(FutureState, Step), Step < 10 ? EFutureQueryResult::Ok : EFutureQueryResult::Desync); if (Step < 7) { EXPECT_EQ(1, FutureState.M()); } #endif if (Step == 7) { Particle.SetM(2); } //skip step 9 SetM to trigger a desync TickSolverHelper(Solver); } #if REWIND_DESYNC //expected desync const TArray DesyncedParticles = RewindData->ComputeDesyncInfo(); EXPECT_EQ(DesyncedParticles.Num(), 1); EXPECT_EQ(DesyncedParticles[0].MostDesynced, ESyncState::HardDesync); EXPECT_EQ(DesyncedParticles[0].Particle, Proxy->GetHandle_LowLevel()); #endif // Throw out the proxy Solver->UnregisterObject(Proxy); Module->DestroySolver(Solver); } } GTEST_TEST(AllTraits, RewindTest_DesyncFromPT) { #if REWIND_DESYNC for (int Optimization = 0; Optimization < 2; ++Optimization) { //We want to detect when sim results change //Detecting output of position and velocity is expensive and hard to track //Instead we need to rely on fast forward mechanism, this is still in progress auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10)); auto Box = Chaos::FImplicitObjectPtr(new TBox(FVec3(-100, -100, -100), FVec3(100, 100, 0))); FChaosSolversModule* Module = FChaosSolversModule::GetModule(); // Make a solver auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1); InitSolverSettings(Solver); Solver->EnableRewindCapture(7, !!Optimization); // Make particles auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto KinematicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle()); const int32 LastStep = 11; { auto& Dynamic = DynamicProxy->GetGameThreadAPI(); auto& Kinematic = KinematicProxy->GetGameThreadAPI(); Dynamic.SetGeometry(Sphere); Dynamic.SetGravityEnabled(true); Solver->RegisterObject(DynamicProxy); Kinematic.SetGeometry(Box); Solver->RegisterObject(KinematicProxy); Dynamic.SetX(FVec3(0, 0, 17)); Dynamic.SetGravityEnabled(false); Dynamic.SetV(FVec3(0, 0, -1)); Dynamic.SetObjectState(EObjectStateType::Dynamic); Kinematic.SetX(FVec3(0, 0, 0)); ChaosTest::SetParticleSimDataToCollide({ DynamicProxy->GetParticle_LowLevel(),KinematicProxy->GetParticle_LowLevel() }); for (int Step = 0; Step <= LastStep; ++Step) { TickSolverHelper(Solver); } // We may end up a bit away from the surface (dt * V), due to solving for 0 velocity and not 0 position error EXPECT_GE(Dynamic.X()[2], 10); EXPECT_LE(Dynamic.X()[2], 11); } const int RewindStep = 5; auto& Dynamic = *DynamicProxy->GetPhysicsThreadAPI(); auto& Kinematic = *KinematicProxy->GetPhysicsThreadAPI(); FPhysicsThreadContextScope Scope(true); FRewindData* RewindData = Solver->GetRewindData(); EXPECT_TRUE(RewindData->RewindToFrame(RewindStep)); Kinematic.SetX(FVec3(0, 0, -1)); for (int Step = RewindStep; Step <= LastStep; ++Step) { //at the end of frame 6 a desync occurs because velocity is no longer clamped (kinematic moved) //because of this desync will happen for any step after 6 if (Step <= 6) { FGeometryParticleState FutureState(*DynamicProxy->GetHandle_LowLevel()); EXPECT_EQ(RewindData->GetFutureStateAtFrame(FutureState, Step), EFutureQueryResult::Ok); } else if (Step >= 8) { //collision would have happened at frame 7, so anything after will desync. We skip a few frames because solver is fuzzy at that point //that is we can choose to solve velocity in a few ways. Main thing we want to know is that a desync eventually happened FGeometryParticleState FutureState(*DynamicProxy->GetHandle_LowLevel()); EXPECT_EQ(RewindData->GetFutureStateAtFrame(FutureState, Step), EFutureQueryResult::Desync); } TickSolverHelper(Solver); } //both kinematic and simulated are desynced const TArray DesyncedParticles = RewindData->ComputeDesyncInfo(); EXPECT_EQ(DesyncedParticles.Num(), 2); EXPECT_EQ(DesyncedParticles[0].MostDesynced, ESyncState::HardDesync); EXPECT_EQ(DesyncedParticles[1].MostDesynced, ESyncState::HardDesync); // We may end up a bit away from the surface (dt * V), due to solving for 0 velocity and not 0 position error EXPECT_GE(Dynamic.X()[2], 9); EXPECT_LE(Dynamic.X()[2], 10); Module->DestroySolver(Solver); } #endif } GTEST_TEST(AllTraits, DISABLED_RewindTest_ResimDesyncFromChangeForce) { for (int Optimization = 0; Optimization < 2; ++Optimization) { auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10)); FChaosSolversModule* Module = FChaosSolversModule::GetModule(); // Make a solver auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1); InitSolverSettings(Solver); Solver->EnableRewindCapture(7, !!Optimization); // Make particles auto Proxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); int32 LastStep = 11; { auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetGeometry(Sphere); Solver->RegisterObject(Proxy); Particle.SetGravityEnabled(false); Particle.SetV(FVec3(0, 0, 10)); for (int Step = 0; Step <= LastStep; ++Step) { if (Step == 7) { Particle.AddForce(FVec3(0, 1, 0)); } if (Step == 9) { Particle.AddForce(FVec3(100, 0, 0)); } TickSolverHelper(Solver); } } const int RewindStep = 5; auto& Particle = *Proxy->GetPhysicsThreadAPI(); #if PHYSICS_THREAD_CONTEXT FPhysicsThreadContextScope Scope(true); #endif { FRewindData* RewindData = Solver->GetRewindData(); //EXPECT_TRUE(RewindData->RewindToFrame(RewindStep)); for (int Step = RewindStep; Step <= LastStep; ++Step) { #if REWIND_DESYNC FGeometryParticleState FutureState(*Proxy->GetHandle_LowLevel()); EXPECT_EQ(RewindData->GetFutureStateAtFrame(FutureState, Step), Step < 10 ? EFutureQueryResult::Ok : EFutureQueryResult::Desync); #endif if (Step == 7) { Particle.AddForce(FVec3(0, 1, 0)); } //skip step 9 SetF to trigger a desync TickSolverHelper(Solver); } EXPECT_EQ(Particle.V()[0], 0); #if REWIND_DESYNC //desync const TArray DesyncedParticles = RewindData->ComputeDesyncInfo(); EXPECT_EQ(DesyncedParticles.Num(), 1); EXPECT_EQ(DesyncedParticles[0].MostDesynced, ESyncState::HardDesync); #endif } //rewind to exactly step 7 to make sure force is not already applied for us { FRewindData* RewindData = Solver->GetRewindData(); //EXPECT_TRUE(RewindData->RewindToFrame(7)); EXPECT_EQ(Particle.Acceleration()[1], 0); } // Throw out the proxy Solver->UnregisterObject(Proxy); Module->DestroySolver(Solver); } } GTEST_TEST(AllTraits, RewindTest_ResimAsFollower) { #if REWIND_DESYNC for (int Optimization = 0; Optimization < 2; ++Optimization) { auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10)); auto Box = Chaos::FImplicitObjectPtr(new TBox(FVec3(-100, -100, -100), FVec3(100, 100, 0))); FChaosSolversModule* Module = FChaosSolversModule::GetModule(); // Make a solver auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1); InitSolverSettings(Solver); Solver->EnableRewindCapture(7, !!Optimization); // Make particles auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto KinematicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle()); const int32 LastStep = 11; TArray Xs; { auto& Dynamic = DynamicProxy->GetGameThreadAPI(); auto& Kinematic = KinematicProxy->GetGameThreadAPI(); Dynamic.SetGeometry(Sphere); Dynamic.SetGravityEnabled(true); Solver->RegisterObject(DynamicProxy); Kinematic.SetGeometry(Box); Solver->RegisterObject(KinematicProxy); Dynamic.SetX(FVec3(0, 0, 17)); Dynamic.SetGravityEnabled(false); Dynamic.SetV(FVec3(0, 0, -1)); Dynamic.SetObjectState(EObjectStateType::Dynamic); Dynamic.SetResimType(EResimType::ResimAsFollower); Kinematic.SetX(FVec3(0, 0, 0)); ChaosTest::SetParticleSimDataToCollide({ DynamicProxy->GetParticle_LowLevel(),KinematicProxy->GetParticle_LowLevel() }); for (int Step = 0; Step <= LastStep; ++Step) { TickSolverHelper(Solver); Xs.Add(Dynamic.X()); } EXPECT_GE(Dynamic.X()[2], 10); EXPECT_LE(Dynamic.X()[2], 11); } const int RewindStep = 5; FPhysicsThreadContextScope Scope(true); auto& Dynamic = *DynamicProxy->GetPhysicsThreadAPI(); auto& Kinematic = *KinematicProxy->GetPhysicsThreadAPI(); FRewindData* RewindData = Solver->GetRewindData(); EXPECT_TRUE(RewindData->RewindToFrame(RewindStep)); //make avoid collision Kinematic.SetX(FVec3(0, 0, 100000)); for (int Step = RewindStep; Step <= LastStep; ++Step) { //Resim but dynamic will take old path since it's marked as ResimAsFollower TickSolverHelper(Solver); EXPECT_VECTOR_FLOAT_EQ(Dynamic.X(), Xs[Step]); } #if REWIND_DESYNC // follower - so dynamic in sync, kinematic desync const TArray DesyncedParticles = RewindData->ComputeDesyncInfo(); EXPECT_EQ(DesyncedParticles.Num(), 1); EXPECT_EQ(DesyncedParticles[0].MostDesynced, ESyncState::HardDesync); EXPECT_EQ(DesyncedParticles[0].Particle, KinematicProxy->GetHandle_LowLevel()); #endif // We may end up a bit away from the surface (dt * V), due to solving for 0 velocity and not 0 position error EXPECT_GE(Dynamic.X()[2], 10); EXPECT_LE(Dynamic.X()[2], 11); Module->DestroySolver(Solver); } #endif } GTEST_TEST(AllTraits, DISABLED_RewindTest_FullResimFallSeeCollisionCorrection) { for (int Optimization = 0; Optimization < 2; ++Optimization) { auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10)); auto Box = Chaos::FImplicitObjectPtr(new TBox(FVec3(-100, -100, -100), FVec3(100, 100, 0))); FChaosSolversModule* Module = FChaosSolversModule::GetModule(); // Make a solver auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1); InitSolverSettings(Solver); Solver->EnableRewindCapture(100, !!Optimization); // Make particles auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto KinematicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle()); const int32 LastStep = 11; TArray Xs; { auto& Dynamic = DynamicProxy->GetGameThreadAPI(); auto& Kinematic = KinematicProxy->GetGameThreadAPI(); Dynamic.SetGeometry(Sphere); Dynamic.SetGravityEnabled(true); Solver->RegisterObject(DynamicProxy); Kinematic.SetGeometry(Box); Solver->RegisterObject(KinematicProxy); Dynamic.SetX(FVec3(0, 0, 17)); Dynamic.SetGravityEnabled(false); Dynamic.SetV(FVec3(0, 0, -1)); Dynamic.SetObjectState(EObjectStateType::Dynamic); Kinematic.SetX(FVec3(0, 0, -1000)); ChaosTest::SetParticleSimDataToCollide({ DynamicProxy->GetParticle_LowLevel(),KinematicProxy->GetParticle_LowLevel() }); for (int Step = 0; Step <= LastStep; ++Step) { TickSolverHelper(Solver); Xs.Add(Dynamic.X()); } // We may end up a bit away from the surface (dt * V), due to solving for 0 velocity and not 0 position error EXPECT_GE(Dynamic.X()[2], 5); EXPECT_LE(Dynamic.X()[2], 6); } const int RewindStep = 0; #if PHYSICS_THREAD_CONTEXT FPhysicsThreadContextScope Scope(true); #endif auto& Dynamic = *DynamicProxy->GetPhysicsThreadAPI(); auto& Kinematic = *KinematicProxy->GetPhysicsThreadAPI(); FRewindData* RewindData = Solver->GetRewindData(); //EXPECT_TRUE(RewindData->RewindToFrame(RewindStep)); //force collision Kinematic.SetX(FVec3(0, 0, 0)); for (int Step = RewindStep; Step <= LastStep; ++Step) { //Resim sees collision since it's ResimAsFull TickSolverHelper(Solver); #if REWIND_DESYNC EXPECT_GE(Dynamic.X()[2], 10); #endif } #if REWIND_DESYNC // We may end up a bit away from the surface (dt * V), due to solving for 0 velocity and not 0 position error EXPECT_GE(Dynamic.X()[2], 10); EXPECT_LE(Dynamic.X()[2], 11); #endif #if REWIND_DESYNC //both desync const TArray DesyncedParticles = RewindData->ComputeDesyncInfo(); EXPECT_EQ(DesyncedParticles.Num(), 2); EXPECT_EQ(DesyncedParticles[0].MostDesynced, ESyncState::HardDesync); EXPECT_EQ(DesyncedParticles[1].MostDesynced, ESyncState::HardDesync); #endif Module->DestroySolver(Solver); } } GTEST_TEST(AllTraits, DISABLED_RewindTest_ResimAsFollowerFallIgnoreCollision) { for (int Optimization = 0; Optimization < 2; ++Optimization) { auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10)); auto Box = Chaos::FImplicitObjectPtr(new TBox(FVec3(-100, -100, -100), FVec3(100, 100, 0))); FChaosSolversModule* Module = FChaosSolversModule::GetModule(); // Make a solver auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1); InitSolverSettings(Solver); Solver->EnableRewindCapture(100, !!Optimization); // Make particles auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto KinematicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle()); const int32 LastStep = 11; TArray Xs; { auto& Dynamic = DynamicProxy->GetGameThreadAPI(); auto& Kinematic = KinematicProxy->GetGameThreadAPI(); Dynamic.SetGeometry(Sphere); Dynamic.SetGravityEnabled(true); Solver->RegisterObject(DynamicProxy); Kinematic.SetGeometry(Box); Solver->RegisterObject(KinematicProxy); Dynamic.SetX(FVec3(0, 0, 17)); Dynamic.SetGravityEnabled(false); Dynamic.SetV(FVec3(0, 0, -1)); Dynamic.SetObjectState(EObjectStateType::Dynamic); Dynamic.SetResimType(EResimType::ResimAsFollower); Kinematic.SetX(FVec3(0, 0, -1000)); ChaosTest::SetParticleSimDataToCollide({ DynamicProxy->GetParticle_LowLevel(),KinematicProxy->GetParticle_LowLevel() }); for (int Step = 0; Step <= LastStep; ++Step) { TickSolverHelper(Solver); Xs.Add(Dynamic.X()); } // We may end up a bit away from the surface (dt * V), due to solving for 0 velocity and not 0 position error EXPECT_GE(Dynamic.X()[2], 5); EXPECT_LE(Dynamic.X()[2], 6); } #if PHYSICS_THREAD_CONTEXT FPhysicsThreadContextScope Scope(true); #endif auto& Dynamic = *DynamicProxy->GetPhysicsThreadAPI(); auto& Kinematic = *KinematicProxy->GetPhysicsThreadAPI(); const int RewindStep = 0; FRewindData* RewindData = Solver->GetRewindData(); //EXPECT_TRUE(RewindData->RewindToFrame(RewindStep)); //force collision Kinematic.SetX(FVec3(0, 0, 0)); for (int Step = RewindStep; Step <= LastStep; ++Step) { //Resim ignores collision since it's ResimAsFollower TickSolverHelper(Solver); EXPECT_VECTOR_FLOAT_EQ(Dynamic.X(), Xs[Step]); } // We may end up a bit away from the surface (dt * V), due to solving for 0 velocity and not 0 position error EXPECT_GE(Dynamic.X()[2], 5); EXPECT_LE(Dynamic.X()[2], 6); #if REWIND_DESYNC //dynamic follower so only kinematic desyncs const TArray DesyncedParticles = RewindData->ComputeDesyncInfo(); EXPECT_EQ(DesyncedParticles.Num(), 1); EXPECT_EQ(DesyncedParticles[0].MostDesynced, ESyncState::HardDesync); EXPECT_EQ(DesyncedParticles[0].Particle, KinematicProxy->GetHandle_LowLevel()); #endif Module->DestroySolver(Solver); } } GTEST_TEST(AllTraits, RewindTest_ResimAsFollowerWithForces) { #if REWIND_DESYNC for (int Optimization = 0; Optimization < 2; ++Optimization) { auto Box = Chaos::FImplicitObjectPtr(new TBox(FVec3(-10, -10, -10), FVec3(10, 10, 10))); FChaosSolversModule* Module = FChaosSolversModule::GetModule(); // Make a solver auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1); InitSolverSettings(Solver); Solver->EnableRewindCapture(7, !!Optimization); // Make particles auto FullSimProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto FollowerSimProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); const int32 LastStep = 11; TArray Xs; { auto& FullSim = FullSimProxy->GetGameThreadAPI(); auto& FollowerSim = FollowerSimProxy->GetGameThreadAPI(); FullSim.SetGeometry(Box); FullSim.SetGravityEnabled(false); Solver->RegisterObject(FullSimProxy); FollowerSim.SetGeometry(Box); FollowerSim.SetGravityEnabled(false); Solver->RegisterObject(FollowerSimProxy); FullSim.SetX(FVec3(0, 0, 20)); FullSim.SetObjectState(EObjectStateType::Dynamic); FullSim.SetM(1); FullSim.SetInvM(1); FollowerSim.SetX(FVec3(0, 0, 0)); FollowerSim.SetResimType(EResimType::ResimAsFollower); FollowerSim.SetM(1); FollowerSim.SetInvM(1); ChaosTest::SetParticleSimDataToCollide({ FullSimProxy->GetParticle_LowLevel(),FollowerSimProxy->GetParticle_LowLevel() }); for (int Step = 0; Step <= LastStep; ++Step) { FollowerSim.SetLinearImpulse(FVec3(0, 0, 0.5)); TickSolverHelper(Solver); Xs.Add(FullSim.X()); } } FPhysicsThreadContextScope Scope(true); const int RewindStep = 5; auto& FullSim = *FullSimProxy->GetPhysicsThreadAPI(); auto& FollowerSim = *FollowerSimProxy->GetPhysicsThreadAPI(); FRewindData* RewindData = Solver->GetRewindData(); EXPECT_TRUE(RewindData->RewindToFrame(RewindStep)); for (int Step = RewindStep; Step <= LastStep; ++Step) { //resim - follower sim should have its impulses automatically added thus moving FullSim in the exact same way TickSolverHelper(Solver); EXPECT_VECTOR_FLOAT_EQ(FullSim.X(), Xs[Step]); } #if REWIND_DESYNC //follower so no desync const TArray DesyncedParticles = RewindData->ComputeDesyncInfo(); EXPECT_EQ(DesyncedParticles.Num(), 0); #endif Module->DestroySolver(Solver); } #endif } GTEST_TEST(AllTraits, RewindTest_ResimAsFollowerWokenUp) { #if REWIND_DESYNC for (int Optimization = 0; Optimization < 2; ++Optimization) { auto Box = Chaos::FImplicitObjectPtr(new TBox(FVec3(-10, -10, -10), FVec3(10, 10, 10))); FChaosSolversModule* Module = FChaosSolversModule::GetModule(); // Make a solver auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1); InitSolverSettings(Solver); Solver->EnableRewindCapture(7, !!Optimization); // Make particles auto ImpulsedObjProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto HitObjProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); const int32 ApplyImpulseStep = 8; const int32 LastStep = 11; TArray Xs; { auto& ImpulsedObj = ImpulsedObjProxy->GetGameThreadAPI(); auto& HitObj = HitObjProxy->GetGameThreadAPI(); ImpulsedObj.SetGeometry(Box); ImpulsedObj.SetGravityEnabled(false); Solver->RegisterObject(ImpulsedObjProxy); HitObj.SetGeometry(Box); HitObj.SetGravityEnabled(false); Solver->RegisterObject(HitObjProxy); ImpulsedObj.SetX(FVec3(0, 0, 20)); ImpulsedObj.SetM(1); ImpulsedObj.SetInvM(1); ImpulsedObj.SetResimType(EResimType::ResimAsFollower); ImpulsedObj.SetObjectState(EObjectStateType::Sleeping); HitObj.SetX(FVec3(0, 0, 0)); HitObj.SetM(1); HitObj.SetInvM(1); HitObj.SetResimType(EResimType::ResimAsFollower); HitObj.SetObjectState(EObjectStateType::Sleeping); ChaosTest::SetParticleSimDataToCollide({ ImpulsedObjProxy->GetParticle_LowLevel(),HitObjProxy->GetParticle_LowLevel() }); for (int Step = 0; Step <= LastStep; ++Step) { if (ApplyImpulseStep == Step) { ImpulsedObj.SetLinearImpulse(FVec3(0, 0, -10)); } TickSolverHelper(Solver); Xs.Add(HitObj.X()); } } auto& ImpulsedObj = *ImpulsedObjProxy->GetPhysicsThreadAPI(); auto& HitObj = *HitObjProxy->GetPhysicsThreadAPI(); FPhysicsThreadContextScope Scope(true); const int RewindStep = 5; FRewindData* RewindData = Solver->GetRewindData(); EXPECT_TRUE(RewindData->RewindToFrame(RewindStep)); for (int Step = RewindStep; Step <= LastStep; ++Step) { TickSolverHelper(Solver); EXPECT_VECTOR_FLOAT_EQ(HitObj.X(), Xs[Step]); } #if REWIND_DESYNC //follower so no desync const TArray DesyncedParticles = RewindData->ComputeDesyncInfo(); EXPECT_EQ(DesyncedParticles.Num(), 0); #endif Module->DestroySolver(Solver); } #endif } GTEST_TEST(AllTraits, RewindTest_ResimAsFollowerWokenUpNoHistory) { #if REWIND_DESYNC for (int Optimization = 0; Optimization < 2; ++Optimization) { auto Box = Chaos::FImplicitObjectPtr(new TBox(FVec3(-10, -10, -10), FVec3(10, 10, 10))); FChaosSolversModule* Module = FChaosSolversModule::GetModule(); // Make a solver auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1); InitSolverSettings(Solver); Solver->EnableRewindCapture(7, !!Optimization); // Make particles auto ImpulsedObjProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto HitObjProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); const int32 ApplyImpulseStep = 97; const int32 LastStep = 100; TArray Xs; { auto& ImpulsedObj = ImpulsedObjProxy->GetGameThreadAPI(); auto& HitObj = HitObjProxy->GetGameThreadAPI(); ImpulsedObj.SetGeometry(Box); ImpulsedObj.SetGravityEnabled(false); Solver->RegisterObject(ImpulsedObjProxy); HitObj.SetGeometry(Box); HitObj.SetGravityEnabled(false); Solver->RegisterObject(HitObjProxy); ImpulsedObj.SetX(FVec3(0, 0, 20)); ImpulsedObj.SetM(1); ImpulsedObj.SetInvM(1); ImpulsedObj.SetObjectState(EObjectStateType::Sleeping); HitObj.SetX(FVec3(0, 0, 0)); HitObj.SetM(1); HitObj.SetInvM(1); HitObj.SetResimType(EResimType::ResimAsFollower); HitObj.SetObjectState(EObjectStateType::Sleeping); ChaosTest::SetParticleSimDataToCollide({ ImpulsedObjProxy->GetParticle_LowLevel(),HitObjProxy->GetParticle_LowLevel() }); for (int Step = 0; Step <= LastStep; ++Step) { TickSolverHelper(Solver); Xs.Add(HitObj.X()); //not a full re-sim so we should end up with exact same result } } const int RewindStep = 95; FPhysicsThreadContextScope Scope(true); auto& ImpulsedObj = *ImpulsedObjProxy->GetPhysicsThreadAPI(); auto& HitObj = *HitObjProxy->GetPhysicsThreadAPI(); FRewindData* RewindData = Solver->GetRewindData(); EXPECT_TRUE(RewindData->RewindToFrame(RewindStep)); for (int Step = RewindStep; Step <= LastStep; ++Step) { //during resim apply correction impulse if (ApplyImpulseStep == Step) { ImpulsedObj.SetLinearImpulse(FVec3(0, 0, -10)); } TickSolverHelper(Solver); //even though there's now a different collision in the sim, the final result of follower is the same as before EXPECT_VECTOR_FLOAT_EQ(HitObj.X(), Xs[Step]); } #if REWIND_DESYNC //only desync non-follower const TArray DesyncedParticles = RewindData->ComputeDesyncInfo(); EXPECT_EQ(DesyncedParticles.Num(), 1); EXPECT_EQ(DesyncedParticles[0].MostDesynced, ESyncState::HardDesync); EXPECT_EQ(DesyncedParticles[0].Particle, ImpulsedObjProxy->GetHandle_LowLevel()); #endif Module->DestroySolver(Solver); } #endif } GTEST_TEST(AllTraits, DISABLED_RewindTest_DesyncSimOutOfCollision) { for (int Optimization = 0; Optimization < 2; ++Optimization) { auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10)); auto Box = Chaos::FImplicitObjectPtr(new TBox(FVec3(-100, -100, -100), FVec3(100, 100, 0))); FChaosSolversModule* Module = FChaosSolversModule::GetModule(); // Make a solver auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1); InitSolverSettings(Solver); Solver->EnableRewindCapture(100, !!Optimization); // Make particles auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto KinematicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle()); const int32 LastStep = 11; TArray Xs; { auto& Dynamic = DynamicProxy->GetGameThreadAPI(); auto& Kinematic = KinematicProxy->GetGameThreadAPI(); Dynamic.SetGeometry(Sphere); Dynamic.SetGravityEnabled(true); Solver->RegisterObject(DynamicProxy); Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0); Kinematic.SetGeometry(Box); Solver->RegisterObject(KinematicProxy); Dynamic.SetX(FVec3(0, 0, 17)); Dynamic.SetGravityEnabled(true); Dynamic.SetObjectState(EObjectStateType::Dynamic); Kinematic.SetX(FVec3(0, 0, 0)); ChaosTest::SetParticleSimDataToCollide({ DynamicProxy->GetParticle_LowLevel(),KinematicProxy->GetParticle_LowLevel() }); for (int Step = 0; Step <= LastStep; ++Step) { TickSolverHelper(Solver); Xs.Add(Dynamic.X()); } EXPECT_GE(Dynamic.X()[2], 10); } #if PHYSICS_THREAD_CONTEXT FPhysicsThreadContextScope Scope(true); #endif auto& Dynamic = *DynamicProxy->GetPhysicsThreadAPI(); auto& Kinematic = *KinematicProxy->GetPhysicsThreadAPI(); const int RewindStep = 8; FRewindData* RewindData = Solver->GetRewindData(); //EXPECT_TRUE(RewindData->RewindToFrame(RewindStep)); //remove from collision, should wakeup entire island and force a soft desync Kinematic.SetX(FVec3(0, 0, -10000)); auto PTDynamic = DynamicProxy->GetHandle_LowLevel()->CastToRigidParticle(); //using handle directly because outside sim callback scope and we have ensures for that auto PTKinematic = KinematicProxy->GetHandle_LowLevel()->CastToKinematicParticle(); for (int Step = RewindStep; Step <= LastStep; ++Step) { //physics sim desync will not be known until the next frame because we can only compare inputs (teleport overwrites result of end of frame for example) if (Step > RewindStep + 1) { #if REWIND_DESYNC EXPECT_EQ(PTDynamic->SyncState(), ESyncState::HardDesync); #endif } TickSolverHelper(Solver); EXPECT_LE(Dynamic.X()[2], 10 + KINDA_SMALL_NUMBER); #if REWIND_DESYNC //kinematic desync will be known at end of frame because the simulation doesn't write results (so we know right away it's a desync) if (Step < LastStep) { EXPECT_EQ(PTKinematic->SyncState(), ESyncState::HardDesync); } else { //everything in sync after last step EXPECT_EQ(PTKinematic->SyncState(), ESyncState::InSync); EXPECT_EQ(PTDynamic->SyncState(), ESyncState::InSync); } #endif } #if REWIND_DESYNC //both desync const TArray DesyncedParticles = RewindData->ComputeDesyncInfo(); EXPECT_EQ(DesyncedParticles.Num(), 2); EXPECT_EQ(DesyncedParticles[0].MostDesynced, ESyncState::HardDesync); EXPECT_EQ(DesyncedParticles[1].MostDesynced, ESyncState::HardDesync); #endif Module->DestroySolver(Solver); } } GTEST_TEST(AllTraits, DISABLED_RewindTest_SoftDesyncFromSameIsland) { auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10)); auto Box = Chaos::FImplicitObjectPtr(new TBox(FVec3(-100, -100, -100), FVec3(100, 100, 0))); FChaosSolversModule* Module = FChaosSolversModule::GetModule(); // Make a solver auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1); InitSolverSettings(Solver); Solver->EnableRewindCapture(100,true); //soft desync only exists when resim optimization is on // Make particles auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto KinematicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle()); const int32 LastStep = 11; TArray Xs; { auto& Dynamic = DynamicProxy->GetGameThreadAPI(); auto& Kinematic = KinematicProxy->GetGameThreadAPI(); Dynamic.SetGeometry(Sphere); Dynamic.SetGravityEnabled(true); Solver->RegisterObject(DynamicProxy); Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0); Kinematic.SetGeometry(Box); Solver->RegisterObject(KinematicProxy); Dynamic.SetX(FVec3(0, 0, 37)); Dynamic.SetGravityEnabled(true); Dynamic.SetObjectState(EObjectStateType::Dynamic); Kinematic.SetX(FVec3(0, 0, 0)); ChaosTest::SetParticleSimDataToCollide({ DynamicProxy->GetParticle_LowLevel(),KinematicProxy->GetParticle_LowLevel() }); for (int Step = 0; Step <= LastStep; ++Step) { TickSolverHelper(Solver); Xs.Add(Dynamic.X()); } // We may end up a bit away from the surface (dt * V), due to solving for 0 velocity and not 0 position error EXPECT_GE(Dynamic.X()[2], 10); EXPECT_LE(Dynamic.X()[2], 12); } #if PHYSICS_THREAD_CONTEXT FPhysicsThreadContextScope Scope(true); #endif auto& Dynamic = *DynamicProxy->GetPhysicsThreadAPI(); auto& Kinematic = *KinematicProxy->GetPhysicsThreadAPI(); const int RewindStep = 0; FRewindData* RewindData = Solver->GetRewindData(); //EXPECT_TRUE(RewindData->RewindToFrame(RewindStep)); //mark kinematic as desynced (this should give us identical results which will trigger all particles in island to be soft desync) auto PTDynamic = DynamicProxy->GetHandle_LowLevel()->CastToRigidParticle(); //using handle directly because outside sim callback scope and we have ensures for that auto PTKinematic = KinematicProxy->GetHandle_LowLevel()->CastToKinematicParticle(); PTKinematic->SetSyncState(ESyncState::HardDesync); bool bEverSoft = false; for (int Step = RewindStep; Step <= LastStep; ++Step) { TickSolverHelper(Solver); #if REWIND_DESYNC //kinematic desync will be known at end of frame because the simulation doesn't write results (so we know right away it's a desync) if (Step < LastStep) { EXPECT_EQ(PTKinematic->SyncState(), ESyncState::HardDesync); //islands merge and split depending on internal solve //but we should see dynamic being soft desync at least once when islands merge if (PTDynamic->SyncState() == ESyncState::SoftDesync) { bEverSoft = true; } } else { //everything in sync after last step EXPECT_EQ(PTKinematic->SyncState(), ESyncState::InSync); EXPECT_EQ(PTDynamic->SyncState(), ESyncState::InSync); } #endif } #if REWIND_DESYNC //kinematic hard desync, dynamic only soft desync const TArray DesyncedParticles = RewindData->ComputeDesyncInfo(); EXPECT_EQ(DesyncedParticles.Num(), 2); EXPECT_EQ(DesyncedParticles[0].MostDesynced, DesyncedParticles[0].Particle == KinematicProxy->GetHandle_LowLevel() ? ESyncState::HardDesync : ESyncState::SoftDesync); EXPECT_EQ(DesyncedParticles[1].MostDesynced, DesyncedParticles[1].Particle == KinematicProxy->GetHandle_LowLevel() ? ESyncState::HardDesync : ESyncState::SoftDesync); EXPECT_TRUE(bEverSoft); // We may end up a bit away from the surface (dt * V), due to solving for 0 velocity and not 0 position error EXPECT_GE(Dynamic.X()[2], 10); EXPECT_LE(Dynamic.X()[2], 12); #endif Module->DestroySolver(Solver); } GTEST_TEST(AllTraits, DISABLED_RewindTest_SoftDesyncFromSameIslandThenBackToInSync) { auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10)); auto Box = Chaos::FImplicitObjectPtr(new TBox(FVec3(-100, -100, -10), FVec3(100, 100, 0))); FChaosSolversModule* Module = FChaosSolversModule::GetModule(); // Make a solver auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1); InitSolverSettings(Solver); Solver->EnableRewindCapture(100,true); //soft desync only exists when resim optimization is on // Make particles auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto KinematicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle()); const int32 LastStep = 15; TArray Xs; { auto& Dynamic = DynamicProxy->GetGameThreadAPI(); auto& Kinematic = KinematicProxy->GetGameThreadAPI(); Dynamic.SetGeometry(Sphere); Dynamic.SetGravityEnabled(true); Solver->RegisterObject(DynamicProxy); Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0); Kinematic.SetGeometry(Box); Solver->RegisterObject(KinematicProxy); Dynamic.SetX(FVec3(1000, 0, 37)); Dynamic.SetGravityEnabled(true); Dynamic.SetObjectState(EObjectStateType::Dynamic); Kinematic.SetX(FVec3(0, 0, 0)); ChaosTest::SetParticleSimDataToCollide({ DynamicProxy->GetParticle_LowLevel(),KinematicProxy->GetParticle_LowLevel() }); for (int Step = 0; Step <= LastStep; ++Step) { TickSolverHelper(Solver); Xs.Add(Dynamic.X()); } } #if PHYSICS_THREAD_CONTEXT FPhysicsThreadContextScope Scope(true); #endif auto& Dynamic = *DynamicProxy->GetPhysicsThreadAPI(); auto& Kinematic = *KinematicProxy->GetPhysicsThreadAPI(); const int RewindStep = 0; FRewindData* RewindData = Solver->GetRewindData(); //EXPECT_TRUE(RewindData->RewindToFrame(RewindStep)); //move kinematic very close but do not alter dynamic //should be soft desync while in island and then get back to in sync auto PTDynamic = DynamicProxy->GetHandle_LowLevel()->CastToRigidParticle(); //using handle directly because outside sim callback scope and we have ensures for that auto PTKinematic = KinematicProxy->GetHandle_LowLevel()->CastToKinematicParticle(); Kinematic.SetX(FVec3(1000 - 110, 0, 0)); bool bEverSoft = false; for (int Step = RewindStep; Step <= LastStep; ++Step) { TickSolverHelper(Solver); #if REWIND_DESYNC //kinematic desync will be known at end of frame because the simulation doesn't write results (so we know right away it's a desync) if (Step < LastStep) { EXPECT_EQ(PTKinematic->SyncState(), ESyncState::HardDesync); //islands merge and split depending on internal solve //but we should see dynamic being soft desync at least once when islands merge if (PTDynamic->SyncState() == ESyncState::SoftDesync) { bEverSoft = true; } //by end should be in sync because islands should definitely be split at this point if (Step == LastStep - 1) { EXPECT_EQ(PTDynamic->SyncState(), ESyncState::InSync); } } else { //everything in sync after last step EXPECT_EQ(PTKinematic->SyncState(), ESyncState::InSync); EXPECT_EQ(PTDynamic->SyncState(), ESyncState::InSync); } #endif } #if REWIND_DESYNC //kinematic hard desync, dynamic only soft desync const TArray DesyncedParticles = RewindData->ComputeDesyncInfo(); EXPECT_EQ(DesyncedParticles.Num(), 2); EXPECT_EQ(DesyncedParticles[0].MostDesynced, DesyncedParticles[0].Particle == KinematicProxy->GetHandle_LowLevel() ? ESyncState::HardDesync : ESyncState::SoftDesync); EXPECT_EQ(DesyncedParticles[1].MostDesynced, DesyncedParticles[1].Particle == KinematicProxy->GetHandle_LowLevel() ? ESyncState::HardDesync : ESyncState::SoftDesync); //no collision so just kept falling EXPECT_LT(Dynamic.X()[2], 10); #endif Module->DestroySolver(Solver); } GTEST_TEST(AllTraits, DISABLED_RewindTest_SoftDesyncFromSameIslandThenBackToInSync_GeometryCollection_SingleFallingUnderGravity) { //TODO: disabled because at the moment GC particles are always marked as dirty - this messes up with transient dirty during rewind //Should probably rethink why GC has its own dirty view for (int Optimization = 0; Optimization < 2; ++Optimization) { FGeometryCollectionWrapper* Collection = TNewSimulationObject::Init()->As(); FFramework UnitTest; UnitTest.Solver->EnableRewindCapture(100, !!Optimization); UnitTest.AddSimulationObject(Collection); UnitTest.Initialize(); TArray Xs; const int32 LastStep = 10; for (int Step = 0; Step <= LastStep; ++Step) { UnitTest.Advance(); Xs.Add(Collection->DynamicCollection->GetTransform(0).GetTranslation()[2]); } const int32 RewindStep = 3; #if PHYSICS_THREAD_CONTEXT FPhysicsThreadContextScope Scope(true); #endif FRewindData* RewindData = UnitTest.Solver->GetRewindData(); //EXPECT_TRUE(RewindData->RewindToFrame(RewindStep)); //GC doesn't marshal data from GT to PT so at the moment all we get is the GT data immediately after rewind, but it doesn't make it over to PT or collection //Not sure if I can even access GT particle so can't verify that, but saw it in debugger at least for (int Step = RewindStep; Step <= LastStep; ++Step) { UnitTest.Advance(); //TODO: turn this on when we find a way to marshal data from GT to PT //EXPECT_EQ(Collection->DynamicCollection->Transform[0].GetTranslation()[2],Xs[Step]); } } } //Helps compare multiple runs for determinism //Also helps comparing runs across different compilers and delta times class FSimComparisonHelper { public: void SaveFrame(const TParticleView>& NonDisabledDyanmic) { FEntry Frame; Frame.X.Reserve(NonDisabledDyanmic.Num()); Frame.R.Reserve(NonDisabledDyanmic.Num()); for (const auto& Dynamic : NonDisabledDyanmic) { Frame.X.Add(Dynamic.GetX()); Frame.R.Add(Dynamic.GetR()); } History.Add(MoveTemp(Frame)); } static void ComputeMaxErrors(const FSimComparisonHelper& A, const FSimComparisonHelper& B, FReal& OutMaxLinearError, FReal& OutMaxAngularError, int32 HistoryMultiple = 1, const TArray* BMapping = nullptr) { ensure(B.History.Num() == (A.History.Num() * HistoryMultiple)); FReal MaxLinearError2 = 0; FReal MaxAngularError2 = 0; for (int32 Idx = 0; Idx < A.History.Num(); ++Idx) { const int32 OtherIdx = Idx * HistoryMultiple + (HistoryMultiple - 1); const FEntry& Entry = A.History[Idx]; const FEntry& OtherEntry = B.History[OtherIdx]; FReal MaxLinearError, MaxAngularError; FEntry::CompareEntry(Entry, OtherEntry, MaxLinearError, MaxAngularError, BMapping); MaxLinearError2 = FMath::Max(MaxLinearError2, MaxLinearError * MaxLinearError); MaxAngularError2 = FMath::Max(MaxAngularError2, MaxAngularError * MaxAngularError); } OutMaxLinearError = FMath::Sqrt(MaxLinearError2); OutMaxAngularError = FMath::Sqrt(MaxAngularError2); } private: struct FEntry { TArray X; TArray R; static void CompareEntry(const FEntry& A, const FEntry& B, FReal& OutMaxLinearError, FReal& OutMaxAngularError, const TArray* BMapping = nullptr) { FReal MaxLinearError2 = 0; FReal MaxAngularError2 = 0; auto BMappingHelper = [BMapping](const int32 Idx) { return BMapping ? (*BMapping)[Idx] : Idx; }; check(A.X.Num() == A.R.Num()); check(A.X.Num() == B.X.Num()); for (int32 Idx = 0; Idx < A.X.Num(); ++Idx) { const FReal LinearError2 = (A.X[Idx] - B.X[BMappingHelper(Idx)]).SizeSquared(); MaxLinearError2 = FMath::Max(LinearError2, MaxLinearError2); //if exactly the same we want 0 for testing purposes, inverse does not get that so just skip it if (B.R[BMappingHelper(Idx)] != A.R[Idx]) { //For angular error we look at the rotation needed to go from B to A const FRotation3 Delta = B.R[BMappingHelper(Idx)] * A.R[Idx].Inverse(); FVec3 Axis; FReal Angle; Delta.ToAxisAndAngleSafe(Axis, Angle, FVec3(0, 0, 1)); const FReal Angle2 = Angle * Angle; MaxAngularError2 = FMath::Max(Angle2, MaxAngularError2); } } OutMaxLinearError = FMath::Sqrt(MaxLinearError2); OutMaxAngularError = FMath::Sqrt(MaxAngularError2); } }; TArray History; }; template void RunHelper(FSimComparisonHelper& SimComparison, int32 NumSteps, FReal Dt, const InitLambda& InitFunc, const TArray* Mapping = nullptr) { FChaosSolversModule* Module = FChaosSolversModule::GetModule(); // Make a solver auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1); InitSolverSettings(Solver); Solver->SetIsDeterministic(true); TArray Storage = InitFunc(Solver, Mapping); for (int32 Step = 0; Step < NumSteps; ++Step) { TickSolverHelper(Solver, Dt); SimComparison.SaveFrame(Solver->GetParticles().GetNonDisabledDynamicView()); } Module->DestroySolver(Solver); } GTEST_TEST(AllTraits, DeterministicSim_SimpleFallingBox) { auto Box = Chaos::FImplicitObjectPtr(new TBox(FVec3(-10, -10, -10), FVec3(10, 10, 10))); const auto InitLambda = [&Box](auto& Solver, auto) { TArray Storage; auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto& Dynamic = DynamicProxy->GetGameThreadAPI(); Dynamic.SetGeometry(Box); Dynamic.SetGravityEnabled(true); Solver->RegisterObject(DynamicProxy); Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0); Dynamic.SetObjectState(EObjectStateType::Dynamic); Storage.Add(DynamicProxy); return Storage; }; FSimComparisonHelper FirstRun; RunHelper(FirstRun, 100, 1 / 30.f, InitLambda); FSimComparisonHelper SecondRun; RunHelper(SecondRun, 100, 1 / 30.f, InitLambda); FReal MaxLinearError, MaxAngularError; FSimComparisonHelper::ComputeMaxErrors(FirstRun, SecondRun, MaxLinearError, MaxAngularError); EXPECT_EQ(MaxLinearError, 0); EXPECT_EQ(MaxAngularError, 0); } GTEST_TEST(AllTraits, DeterministicSim_ThresholdTest) { auto Box = Chaos::FImplicitObjectPtr(new TBox(FVec3(-10, -10, -10), FVec3(10, 10, 10))); FVec3 StartPos(0); FRotation3 StartRotation = FRotation3::FromIdentity(); const auto InitLambda = [&Box, &StartPos, &StartRotation](auto& Solver, auto) { TArray Storage; auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto& Dynamic = DynamicProxy->GetGameThreadAPI(); Dynamic.SetGeometry(Box); Dynamic.SetGravityEnabled(true); Solver->RegisterObject(DynamicProxy); Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0); Dynamic.SetObjectState(EObjectStateType::Dynamic); Dynamic.SetX(StartPos); Dynamic.SetR(StartRotation); Storage.Add(DynamicProxy); return Storage; }; FSimComparisonHelper FirstRun; RunHelper(FirstRun, 10, 1 / 30.f, InitLambda); //move X within threshold StartPos = FVec3(0, 0, 1); FSimComparisonHelper SecondRun; RunHelper(SecondRun, 10, 1 / 30.f, InitLambda); FReal MaxLinearError, MaxAngularError; FSimComparisonHelper::ComputeMaxErrors(FirstRun, SecondRun, MaxLinearError, MaxAngularError); EXPECT_EQ(MaxAngularError, 0); EXPECT_LT(MaxLinearError, 1.01); EXPECT_GT(MaxLinearError, 0.99); //move R within threshold StartPos = FVec3(0, 0, 0); StartRotation = FRotation3::FromAxisAngle(FVec3(1, 1, 0).GetSafeNormal(), 1); FSimComparisonHelper ThirdRun; RunHelper(ThirdRun, 10, 1 / 30.f, InitLambda); FSimComparisonHelper::ComputeMaxErrors(FirstRun, ThirdRun, MaxLinearError, MaxAngularError); EXPECT_EQ(MaxLinearError, 0); EXPECT_LT(MaxAngularError, 1.01); EXPECT_GT(MaxAngularError, 0.99); } GTEST_TEST(AllTraits, DeterministicSim_DoubleTick) { auto Box = Chaos::FImplicitObjectPtr(new TBox(FVec3(-10, -10, -10), FVec3(10, 10, 10))); const auto InitLambda = [&Box](auto& Solver, auto) { TArray Storage; auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto& Dynamic = DynamicProxy->GetGameThreadAPI(); Dynamic.SetGeometry(Box); Dynamic.SetGravityEnabled(false); Solver->RegisterObject(DynamicProxy); Dynamic.SetObjectState(EObjectStateType::Dynamic); Dynamic.SetV(FVec3(1, 0, 0)); Storage.Add(DynamicProxy); return Storage; }; FSimComparisonHelper FirstRun; RunHelper(FirstRun, 100, 1 / 30.f, InitLambda); //tick twice as often FSimComparisonHelper SecondRun; RunHelper(SecondRun, 200, 1 / 60.f, InitLambda); FReal MaxLinearError, MaxAngularError; FSimComparisonHelper::ComputeMaxErrors(FirstRun, SecondRun, MaxLinearError, MaxAngularError, 2); EXPECT_NEAR(MaxLinearError, 0, 1e-4); EXPECT_NEAR(MaxAngularError, 0, 1e-4); } GTEST_TEST(AllTraits, DeterministicSim_DoubleTickGravity) { auto Box = Chaos::FImplicitObjectPtr(new TBox(FVec3(-10, -10, -10), FVec3(10, 10, 10))); const FReal Gravity = -980; const auto InitLambda = [&Box, Gravity](auto& Solver, auto) { TArray Storage; auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto& Dynamic = DynamicProxy->GetGameThreadAPI(); Dynamic.SetGeometry(Box); Dynamic.SetGravityEnabled(true); Solver->RegisterObject(DynamicProxy); Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, Gravity), 0); Dynamic.SetObjectState(EObjectStateType::Dynamic); Storage.Add(DynamicProxy); return Storage; }; const int32 NumSteps = 7; FSimComparisonHelper FirstRun; RunHelper(FirstRun, NumSteps, 1 / 30.f, InitLambda); //tick twice as often FSimComparisonHelper SecondRun; RunHelper(SecondRun, NumSteps * 2, 1 / 60.f, InitLambda); //expected integration gravity error const auto EulerIntegrationHelper = [Gravity](int32 Steps, FReal Dt) { FReal Z = 0; FReal V = 0; for (int32 Step = 0; Step < Steps; ++Step) { V += Gravity * Dt; Z += V * Dt; } return Z; }; const FReal ExpectedZ30 = EulerIntegrationHelper(NumSteps, 1 / 30.f); const FReal ExpectedZ60 = EulerIntegrationHelper(NumSteps * 2, 1 / 60.f); EXPECT_LT(ExpectedZ30, ExpectedZ60); //30 gains speed faster (we use the end velocity to integrate so the bigger dt, the more added energy) const FReal ExpectedError = ExpectedZ60 - ExpectedZ30; FReal MaxLinearError, MaxAngularError; FSimComparisonHelper::ComputeMaxErrors(FirstRun, SecondRun, MaxLinearError, MaxAngularError, 2); EXPECT_LT(MaxLinearError, ExpectedError + 1e-4); EXPECT_EQ(MaxAngularError, 0); } GTEST_TEST(AllTraits, DeterministicSim_DoubleTickCollide) { auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 50)); const auto InitLambda = [&Sphere](auto& Solver, auto) { TArray Storage; auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto& Dynamic = DynamicProxy->GetGameThreadAPI(); Dynamic.SetGeometry(Sphere); Solver->RegisterObject(DynamicProxy); Dynamic.SetObjectState(EObjectStateType::Dynamic); Dynamic.SetGravityEnabled(false); Dynamic.SetV(FVec3(0, 0, -25)); auto DynamicProxy2 = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto& Dynamic2 = DynamicProxy2->GetGameThreadAPI(); Dynamic2.SetGeometry(Sphere); Solver->RegisterObject(DynamicProxy2); Dynamic2.SetX(FVec3(0, 0, -100 - 25 / 60.f - 0.1)); //make it so it overlaps for 30fps but not 60 Dynamic2.SetGravityEnabled(false); ChaosTest::SetParticleSimDataToCollide({ DynamicProxy->GetParticle_LowLevel(),DynamicProxy2->GetParticle_LowLevel() }); Storage.Add(DynamicProxy); Storage.Add(DynamicProxy2); return Storage; }; const int32 NumSteps = 7; FSimComparisonHelper FirstRun; RunHelper(FirstRun, NumSteps, 1 / 30.f, InitLambda); //tick twice as often FSimComparisonHelper SecondRun; RunHelper(SecondRun, NumSteps * 2, 1 / 60.f, InitLambda); FReal MaxLinearError, MaxAngularError; FSimComparisonHelper::ComputeMaxErrors(FirstRun, SecondRun, MaxLinearError, MaxAngularError, 2); } GTEST_TEST(AllTraits, DeterministicSim_DoubleTickStackCollide) { auto SmallBox = Chaos::FImplicitObjectPtr(new TBox(FVec3(-50, -50, -50), FVec3(50, 50, 50))); auto Box = Chaos::FImplicitObjectPtr(new TBox(FVec3(-1000, -1000, -1000), FVec3(1000, 1000, 0))); const auto InitLambda = [&SmallBox, &Box](auto& Solver, auto) { Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -980), 0); TArray Storage; for (int Idx = 0; Idx < 5; ++Idx) { auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto& Dynamic = DynamicProxy->GetGameThreadAPI(); Dynamic.SetGeometry(SmallBox); Solver->RegisterObject(DynamicProxy); Dynamic.SetObjectState(EObjectStateType::Dynamic); Dynamic.SetGravityEnabled(true); Dynamic.SetX(FVec3(0, 20 * Idx, 100 * Idx)); //slightly offset Storage.Add(DynamicProxy); } auto KinematicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle()); auto& Kinematic = KinematicProxy->GetGameThreadAPI(); Kinematic.SetGeometry(Box); Solver->RegisterObject(KinematicProxy); Kinematic.SetX(FVec3(0, 0, -50)); Storage.Add(KinematicProxy); for (int i = 0; i < Storage.Num(); ++i) { for (int j = i + 1; j < Storage.Num(); ++j) { ChaosTest::SetParticleSimDataToCollide({ Storage[i]->GetParticle_LowLevel(),Storage[j]->GetParticle_LowLevel() }); } } return Storage; }; const int32 NumSteps = 20; FSimComparisonHelper FirstRun; RunHelper(FirstRun, NumSteps, 1 / 30.f, InitLambda); //tick twice as often FSimComparisonHelper SecondRun; RunHelper(SecondRun, NumSteps, 1 / 30.f, InitLambda); //make sure deterministic FReal MaxLinearError, MaxAngularError; FSimComparisonHelper::ComputeMaxErrors(FirstRun, SecondRun, MaxLinearError, MaxAngularError, 1); EXPECT_EQ(MaxLinearError, 0); EXPECT_EQ(MaxAngularError, 0); //try with 60fps FSimComparisonHelper ThirdRun; RunHelper(ThirdRun, NumSteps * 2, 1 / 60.f, InitLambda); FSimComparisonHelper::ComputeMaxErrors(FirstRun, ThirdRun, MaxLinearError, MaxAngularError, 2); } GTEST_TEST(AllTraits, DeterministicSim_DifferentCreationOrder) { auto SmallBox = Chaos::FImplicitObjectPtr(new TBox(FVec3(-50, -50, -50), FVec3(50, 50, 50))); auto Box = Chaos::FImplicitObjectPtr(new TBox(FVec3(-1000, -1000, -1000), FVec3(1000, 1000, 0))); const int32 NumParticles = 50; const auto InitLambda = [&SmallBox, &Box, NumParticles](auto& Solver, const TArray* Mapping) { auto MappingHelper = [Mapping](const int32 Idx) { return Mapping ? (*Mapping)[Idx] : Idx; }; Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -980), 0); TArray Storage; for (int Idx = 0; Idx < NumParticles; ++Idx) { auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto& Dynamic = DynamicProxy->GetGameThreadAPI(); Dynamic.SetGeometry(SmallBox); Solver->RegisterObject(DynamicProxy); Dynamic.SetObjectState(EObjectStateType::Dynamic); Dynamic.SetGravityEnabled(true); Dynamic.SetParticleID(FParticleID{ MappingHelper(Idx), INDEX_NONE }); Dynamic.SetX(FVec3(0, 5 * MappingHelper(Idx), 100 * MappingHelper(Idx) + 50)); //slightly offset Dynamic.SetI(FVec3(1000, 1000, 1000)); Dynamic.SetInvI(FVec3(1/1000.0, 1/1000.0, 1/1000.0)); Storage.Add(DynamicProxy); } auto KinematicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle()); auto& Kinematic = KinematicProxy->GetGameThreadAPI(); Kinematic.SetGeometry(Box); Solver->RegisterObject(KinematicProxy); Kinematic.SetX(FVec3(0, 0, 0)); Storage.Add(KinematicProxy); for (int i = 0; i < Storage.Num(); ++i) { for (int j = i + 1; j < Storage.Num(); ++j) { ChaosTest::SetParticleSimDataToCollide({ Storage[i]->GetParticle_LowLevel(),Storage[j]->GetParticle_LowLevel() }); } } return Storage; }; const int32 NumSteps = 20; FSimComparisonHelper FirstRun; RunHelper(FirstRun, NumSteps, 1 / 30.f, InitLambda); //tick twice as often TArray Mapping; for (int32 Idx = 0; Idx < NumParticles; ++Idx) { Mapping.Add(NumParticles - Idx - 1); //Mapping.Add(Idx); } FSimComparisonHelper SecondRun; RunHelper(SecondRun, NumSteps, 1 / 30.f, InitLambda, &Mapping); //make sure deterministic FReal MaxLinearError, MaxAngularError; FSimComparisonHelper::ComputeMaxErrors(FirstRun, SecondRun, MaxLinearError, MaxAngularError, 1, &Mapping); EXPECT_EQ(MaxLinearError, 0); EXPECT_EQ(MaxAngularError, 0); } GTEST_TEST(AllTraits, RewindTest_InterpolatedTwoChannels) { Chaos::AsyncInterpolationMultiplier = 3.0f; int32 PrevNumActiveChannels = Chaos::DefaultNumActiveChannels; Chaos::DefaultNumActiveChannels = 2; //Have two moving particles, one in each channel to see that there's a delay in time on second channel TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere) { if (!Solver->IsUsingAsyncResults()) { return; } auto& Particle = Proxy->GetGameThreadAPI(); Particle.SetV(FVec3(0, 0, 1)); Particle.SetGravityEnabled(false); auto Proxy2 = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle()); auto& Particle2 = Proxy2->GetGameThreadAPI(); auto Sphere2 = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10)); Particle2.SetGeometry(Sphere2); Particle2.SetV(FVec3(0, 0, 1)); Particle2.SetGravityEnabled(false); if (FProxyInterpolationBase* InterpolationData = Proxy2->GetInterpolationData()) { InterpolationData->SetInterpChannel_External(1); } Solver->RegisterObject(Proxy2); FReal Time = 0; const FReal GtDt = 1; for (int Step = 0; Step < 32; ++Step) { TickSolverHelper(Solver); Time += GtDt; const FReal InterpolatedTime0 = Time - SimDt * Solver->GetAsyncInterpolationMultiplier(); const FReal InterpolatedTime1 = InterpolatedTime0 - Chaos::SecondChannelDelay; if (InterpolatedTime0 < 0) { //No movement yet EXPECT_NEAR(Particle.X()[2], 0, 1e-2); } else { EXPECT_NEAR(Particle.X()[2], InterpolatedTime0, 1e-2); } if (InterpolatedTime1 < 0) { //No movement yet EXPECT_NEAR(Particle2.X()[2], 0, 1e-2); } else { EXPECT_NEAR(Particle2.X()[2], InterpolatedTime1, 1e-2); } } }); Chaos::DefaultNumActiveChannels = PrevNumActiveChannels; } }