Per-Object Physics Time dilation

I would like to discuss dilating the timestep of physics applied to an actor/component in UE5, i have been poring over the engine source code trying to gain an understanding of the Physics update structure in order to see if its reasonably possible to do so without building from source (which I have tried to do but I lack the space, time, and honestly reasonable capacity to do so)

Things I’ve learned (important for later on)
Overall, the engine starts updating physics inUWorld::Tick

which in the TG_DuringPhysics group creates a delegate (eventually) that runs:

Target->ExecuteTick(DeltaSeconds, TickType, CurrentThread, MyCompletionGraphEvent);

which then calls UWorld::StartPhysicsSim()

which retrieves the Physics Scene (usually FPhysScene but you can replace it with a different type which we will discuss)

and then calls: PhysScene->StartFrame();

StartFrame simply retrieves all of the solvers (usually just 1 from what i can tell which is the Scenes own solver)

for each solver it calls: Solver->AdvanceAndDispatch_External(UseDeltaTime)

which does a lot of things but from what i can tell calls:

FAllSolverTasks::AdvanceSolver()

And then: void FPhysicsSolverAdvanceTask::AdvanceSolver()
and finally: Solver.AdvanceSolverBy(...)

Technically, there are several solver types, but the only one i see being used by the base engine is Chaos::FPBDRigidsSolver

which makes yet another task: AdvanceOneTimeStepTask(this, MLastDt, SubStepInfo).DoWork();

which finally retrieves the scenes particles and through several calls updates the simulation, if you want to look at the function yourself its here:

from what i can tell this is the entire update chain, all inevitably ending up at the DoWork function which updates the solvers “evolution”

Other important details Ive learned:
A single solver manages updating all physics bodies collision, updates, etc
The solver is contained in the physics scene, specifically in the class FChaosScene:
// Solver representing this scene Chaos::FPhysicsSolver* SceneSolver;

which is made through the ChaosModule: SceneSolver = ChaosModule->CreateSolver(OwnerPtr, InAsyncDt, ThreadingMode, DebugName);

which extremely unfortunately, is hardcoded to make a RigidsSolver:
FPBDRigidsSolver* NewSolver = new FPBDRigidsSolver(SolverBufferMode, InOwner, InAsyncDt);

The meat of the discussion
I wanted to set out to make it possible so an actors physics could be updated with a per-instance scaled timestep, heres a list of things i tried and why they failed:

Scaling the timescale at the component level
→ Didnt work because the component doesnt actually contain methods for updating physics, its all done by the solver

Creating a custom physics scene to modify the update process

→ Didnt work for a few reasons, you can actually replace the worlds physics scene with your own by making your own derived UGameInstance class, and then:
void UFluereGameInstance::OnWorldInit(UWorld* world, const UWorld::InitializationValues IVS)
{
if (!fluere_physics_scene) fluere_physics_scene = new FFluerePhysicsScene(nullptr);
world->SetPhysicsScene(fluere_physics_scene);
}
however, the physics scene only exposes a few virtual functions, and none of them grant enough control to actually change the dilation of a per-objects physics, even if more virtuals were exposed, the scene actually doesnt have all that much control over the delta time of each solver/particle

Creating a custom solver class to manually update objects with a scaled time step

→ This couldve possibly worked since the solver is the correct level to manipulate to update objects in a custom way or with custom scaling like i wanted to do, however, my plan was to create my own solver class, and then replace the scenes solver pointer with my own class solver, but the solver must be initialized in some very specific ways and you probably cant without some serious UB hacks, let me explain:
As we know all the solvers are made by the function: FChaosSolversModule::CreateSolver
which essentially does this:
FPBDRigidsSolver* NewSolver = new FPBDRigidsSolver(SolverBufferMode, InOwner, InAsyncDt);

AllSolvers.Add(NewSolver);
TArray<FPhysicsSolverBase*>& OwnerSolverList = SolverMap.FindOrAdd(InOwner); OwnerSolverList.Add(NewSolver);

// Set up the material lists on the new solver, copying from the current primary list { FPhysicalMaterialManager& Manager = Chaos::FPhysicalMaterialManager::Get(); FPhysicsSceneGuardScopedWrite ScopedWrite(NewSolver->GetExternalDataLock_External()); NewSolver->QueryMaterials_External = Manager.GetPrimaryMaterials_External(); NewSolver->QueryMaterialMasks_External = Manager.GetPrimaryMaterialMasks_External(); NewSolver->SimMaterials = Manager.GetPrimaryMaterials_External(); NewSolver->SimMaterialMasks = Manager.GetPrimaryMaterialMasks_External(); }

now im sure you already see the problem, the OwnerSolverList, and the AllSolvers array are private, im not sure what these containers do with the solver, but i dont think it would be a good idea to not put a solver in them
the other bigger problem is the material lists, all of the members being set in the function are private, not even protected
and of course, the module is hard-coded to make a FPBDRigidsSolverso we cant just give it our custom class, but we cant initialize our own solver,
after the module makes the solver, FChaosScene does this to init it:
SceneSolver->GetChaosVDContextData().OwnerID = GetChaosVDContextData().Id;
SceneSolver->GetChaosVDContextData().Id = FChaosVDRuntimeModule::Get().GenerateUniqueID();
SceneSolver->GetChaosVDContextData().Type = static_cast(EChaosVDContextType::Solver);

SceneSolver->PhysSceneHack = this; SimCallback = SceneSolver->CreateAndRegisterSimCallbackObject_External<FChaosSceneSimCallback>();

UPhysicsSettingsCore* Settings = UPhysicsSettingsCore::Get(); SceneSolver->ApplyConfig(Settings->SolverOptions); SceneSolver->SetIsDeterministic(Settings->bEnableEnhancedDeterminism);

FPhysScene_Chaos graciously does not do anything else, but at a minimum to replace the solver we would have to do all the previous actions in the exact same order, and possibly undo some of the things the previously created solver did when it was being initialized

What im looking for with this discussion post:
At this point i realized there probably wasn’t a way to accomplish this without modifying the engine source, or with lots of hacks, I will keep looking around and trying things but i wanted to make this post so i wasn’t just walking aimlessly, so im asking the community and dev’s to add to this discussion in the following ways:

  1. Fact check me, im sure i got some of these details wrong, the engine is vast and i am but a man
  2. Is there a better way to approach this problem?
  3. If you have any experience changing or messing with the Chaos physics system please share your wisdom, especially if youve done something similar in the past

My engine details:
Version: 5.6.1-44394996+++UE5+Release-5.6
Platform: Windows 11 (25H2) [10.0.26200.6901] (x86_64)