diff --git a/CHANGELOG.md b/CHANGELOG.md index 0891ecba9..4ac01c00f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,7 @@ Bug #5424: Creatures do not headtrack player Bug #5425: Poison effect only appears for one frame Bug #5427: GetDistance unknown ID error is misleading + Bug #5431: Physics performance degradation after a specific number of actors on a scene Bug #5435: Enemies can't hurt the player when collision is off Bug #5441: Enemies can't push a player character when in critical strike stance Bug #5451: Magic projectiles don't disappear with the caster @@ -143,6 +144,7 @@ Feature #5730: Add graphic herbalism option to the launcher and documents Feature #5771: ori command should report where a mesh is loaded from and whether the x version is used. Feature #5813: Instanced groundcover support + Feature #5910: Fall back to delta time when physics can't keep up Task #5480: Drop Qt4 support Task #5520: Improve cell name autocompleter implementation Task #5844: Update 'toggle sneak' documentation diff --git a/apps/openmw/mwphysics/mtphysics.cpp b/apps/openmw/mwphysics/mtphysics.cpp index 1fa6251f8..11eb7f909 100644 --- a/apps/openmw/mwphysics/mtphysics.cpp +++ b/apps/openmw/mwphysics/mtphysics.cpp @@ -101,7 +101,7 @@ namespace osg::Vec3f interpolateMovements(MWPhysics::ActorFrameData& actorData, float timeAccum, float physicsDt) { - const float interpolationFactor = timeAccum / physicsDt; + const float interpolationFactor = std::clamp(timeAccum / physicsDt, 0.0f, 1.0f); return actorData.mPosition * interpolationFactor + actorData.mActorRaw->getPreviousPosition() * (1.f - interpolationFactor); } @@ -138,7 +138,8 @@ namespace namespace MWPhysics { PhysicsTaskScheduler::PhysicsTaskScheduler(float physicsDt, std::shared_ptr collisionWorld) - : mPhysicsDt(physicsDt) + : mDefaultPhysicsDt(physicsDt) + , mPhysicsDt(physicsDt) , mTimeAccum(0.f) , mCollisionWorld(std::move(collisionWorld)) , mNumJobs(0) @@ -152,6 +153,11 @@ namespace MWPhysics , mNextLOS(0) , mFrameNumber(0) , mTimer(osg::Timer::instance()) + , mPrevStepCount(1) + , mBudget(physicsDt) + , mAsyncBudget(0.0f) + , mBudgetCursor(0) + , mAsyncStartTime(0) , mTimeBegin(0) , mTimeEnd(0) , mFrameStart(0) @@ -220,13 +226,61 @@ namespace MWPhysics thread.join(); } - const std::vector& PhysicsTaskScheduler::moveActors(int numSteps, float timeAccum, std::vector&& actorsData, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) + std::tuple PhysicsTaskScheduler::calculateStepConfig(float timeAccum) const + { + int maxAllowedSteps = 2; + int numSteps = timeAccum / mDefaultPhysicsDt; + + // adjust maximum step count based on whether we're likely physics bottlenecked or not + // if maxAllowedSteps ends up higher than numSteps, we will not invoke delta time + // if it ends up lower than numSteps, but greater than 1, we will run a number of true delta time physics steps that we expect to be within budget + // if it ends up lower than numSteps and also 1, we will run a single delta time physics step + // if we did not do this, and had a fixed step count limit, + // we would have an unnecessarily low render framerate if we were only physics bottlenecked, + // and we would be unnecessarily invoking true delta time if we were only render bottlenecked + + // get physics timing stats + float budgetMeasurement = std::max(mBudget.get(), mAsyncBudget.get()); + // time spent per step in terms of the intended physics framerate + budgetMeasurement /= mDefaultPhysicsDt; + // ensure sane minimum value + budgetMeasurement = std::max(0.00001f, budgetMeasurement); + // we're spending almost or more than realtime per physics frame; limit to a single step + if (budgetMeasurement > 0.95) + maxAllowedSteps = 1; + // physics is fairly cheap; limit based on expense + if (budgetMeasurement < 0.5) + maxAllowedSteps = std::ceil(1.0/budgetMeasurement); + // limit to a reasonable amount + maxAllowedSteps = std::min(10, maxAllowedSteps); + + // fall back to delta time for this frame if fixed timestep physics would fall behind + float actualDelta = mDefaultPhysicsDt; + if (numSteps > maxAllowedSteps) + { + numSteps = maxAllowedSteps; + // ensure that we do not simulate a frame ahead when doing delta time; this reduces stutter and latency + // this causes interpolation to 100% use the most recent physics result when true delta time is happening + // and we deliberately simulate up to exactly the timestamp that we want to render + actualDelta = timeAccum/float(numSteps+1); + // actually: if this results in a per-step delta less than the target physics steptime, clamp it + // this might reintroduce some stutter, but only comes into play in obscure cases + // (because numSteps is originally based on mDefaultPhysicsDt, this won't cause us to overrun) + actualDelta = std::max(actualDelta, mDefaultPhysicsDt); + } + + return std::make_tuple(numSteps, actualDelta); + } + + const std::vector& PhysicsTaskScheduler::moveActors(float & timeAccum, std::vector&& actorsData, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) { // This function run in the main thread. // While the mSimulationMutex is held, background physics threads can't run. std::unique_lock lock(mSimulationMutex); + double timeStart = mTimer->tick(); + mMovedActors.clear(); // start by finishing previous background computation @@ -251,14 +305,21 @@ namespace MWPhysics mMovedActors.emplace_back(data.mActorRaw->getPtr()); } } + if(mAdvanceSimulation) + mAsyncBudget.update(mTimer->delta_s(mAsyncStartTime, mTimeEnd), mPrevStepCount, mBudgetCursor); updateStats(frameStart, frameNumber, stats); } + auto [numSteps, newDelta] = calculateStepConfig(timeAccum); + timeAccum -= numSteps*newDelta; + // init for (auto& data : actorsData) data.updatePosition(); + mPrevStepCount = numSteps; mRemainingSteps = numSteps; mTimeAccum = timeAccum; + mPhysicsDt = newDelta; mActorsFrameData = std::move(actorsData); mAdvanceSimulation = (mRemainingSteps != 0); mNewFrame = true; @@ -269,20 +330,30 @@ namespace MWPhysics if (mAdvanceSimulation) mWorldFrameData = std::make_unique(); + if (mAdvanceSimulation) + mBudgetCursor += 1; + if (mNumThreads == 0) { syncComputation(); + if(mAdvanceSimulation) + mBudget.update(mTimer->delta_s(timeStart, mTimer->tick()), numSteps, mBudgetCursor); return mMovedActors; } + mAsyncStartTime = mTimer->tick(); lock.unlock(); mHasJob.notify_all(); + if (mAdvanceSimulation) + mBudget.update(mTimer->delta_s(timeStart, mTimer->tick()), 1, mBudgetCursor); return mMovedActors; } const std::vector& PhysicsTaskScheduler::resetSimulation(const ActorMap& actors) { std::unique_lock lock(mSimulationMutex); + mBudget.reset(mDefaultPhysicsDt); + mAsyncBudget.reset(0.0f); mMovedActors.clear(); mActorsFrameData.clear(); for (const auto& [_, actor] : actors) diff --git a/apps/openmw/mwphysics/mtphysics.hpp b/apps/openmw/mwphysics/mtphysics.hpp index b35ebd5ee..137755c21 100644 --- a/apps/openmw/mwphysics/mtphysics.hpp +++ b/apps/openmw/mwphysics/mtphysics.hpp @@ -13,6 +13,7 @@ #include "physicssystem.hpp" #include "ptrholder.hpp" +#include "components/misc/budgetmeasurement.hpp" namespace Misc { @@ -32,7 +33,7 @@ namespace MWPhysics /// @param timeAccum accumulated time from previous run to interpolate movements /// @param actorsData per actor data needed to compute new positions /// @return new position of each actor - const std::vector& moveActors(int numSteps, float timeAccum, std::vector&& actorsData, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); + const std::vector& moveActors(float & timeAccum, std::vector&& actorsData, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); const std::vector& resetSimulation(const ActorMap& actors); @@ -58,11 +59,13 @@ namespace MWPhysics void updateAabbs(); void updatePtrAabb(const std::weak_ptr& ptr); void updateStats(osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); + std::tuple calculateStepConfig(float timeAccum) const; std::unique_ptr mWorldFrameData; std::vector mActorsFrameData; std::vector mMovedActors; - const float mPhysicsDt; + float mDefaultPhysicsDt; + float mPhysicsDt; float mTimeAccum; std::shared_ptr mCollisionWorld; std::vector mLOSCache; @@ -94,6 +97,12 @@ namespace MWPhysics unsigned int mFrameNumber; const osg::Timer* mTimer; + + int mPrevStepCount; + Misc::BudgetMeasurement mBudget; + Misc::BudgetMeasurement mAsyncBudget; + unsigned int mBudgetCursor; + osg::Timer_t mAsyncStartTime; osg::Timer_t mTimeBegin; osg::Timer_t mTimeEnd; osg::Timer_t mFrameStart; diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index dc9ab629a..65ed13f80 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -744,19 +744,14 @@ namespace MWPhysics { mTimeAccum += dt; - const int maxAllowedSteps = 20; - int numSteps = mTimeAccum / mPhysicsDt; - numSteps = std::min(numSteps, maxAllowedSteps); - - mTimeAccum -= numSteps * mPhysicsDt; - if (skipSimulation) return mTaskScheduler->resetSimulation(mActors); - return mTaskScheduler->moveActors(numSteps, mTimeAccum, prepareFrameData(numSteps), frameStart, frameNumber, stats); + // modifies mTimeAccum + return mTaskScheduler->moveActors(mTimeAccum, prepareFrameData(mTimeAccum >= mPhysicsDt), frameStart, frameNumber, stats); } - std::vector PhysicsSystem::prepareFrameData(int numSteps) + std::vector PhysicsSystem::prepareFrameData(bool willSimulate) { std::vector actorsFrameData; actorsFrameData.reserve(mMovementQueue.size()); @@ -796,7 +791,7 @@ namespace MWPhysics // Ue current value only if we don't advance the simulation. Otherwise we might get a stale value. MWWorld::Ptr standingOn; - if (numSteps == 0) + if (!willSimulate) standingOn = physicActor->getStandingOnPtr(); actorsFrameData.emplace_back(std::move(physicActor), standingOn, moveToWaterSurface, movement, slowFall, waterlevel); diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp index 80b2d98bc..354823986 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -252,7 +252,7 @@ namespace MWPhysics void updateWater(); - std::vector prepareFrameData(int numSteps); + std::vector prepareFrameData(bool willSimulate); osg::ref_ptr mUnrefQueue; diff --git a/components/misc/budgetmeasurement.hpp b/components/misc/budgetmeasurement.hpp new file mode 100644 index 000000000..3d56477af --- /dev/null +++ b/components/misc/budgetmeasurement.hpp @@ -0,0 +1,42 @@ +#ifndef OPENMW_COMPONENTS_MISC_BUDGETMEASUREMENT_H +#define OPENMW_COMPONENTS_MISC_BUDGETMEASUREMENT_H + + +namespace Misc +{ + +class BudgetMeasurement +{ + std::array mBudgetHistory; + std::array mBudgetStepCount; + +public: + BudgetMeasurement(const float default_expense) + { + mBudgetHistory = {default_expense, default_expense, default_expense, default_expense}; + mBudgetStepCount = {1, 1, 1, 1}; + } + + void reset(const float default_expense) + { + mBudgetHistory = {default_expense, default_expense, default_expense, default_expense}; + mBudgetStepCount = {1, 1, 1, 1}; + } + + void update(double delta, unsigned int stepCount, size_t cursor) + { + mBudgetHistory[cursor%4] = delta; + mBudgetStepCount[cursor%4] = stepCount; + } + + double get() const + { + float sum = (mBudgetHistory[0] + mBudgetHistory[1] + mBudgetHistory[2] + mBudgetHistory[3]); + unsigned int stepCountSum = (mBudgetStepCount[0] + mBudgetStepCount[1] + mBudgetStepCount[2] + mBudgetStepCount[3]); + return sum/float(stepCountSum); + } +}; + +} + +#endif