mirror of https://github.com/OpenMW/openmw.git
Merge branch 'async-physics' into 'master'
Async physics See merge request OpenMW/openmw!248pull/3013/head
commit
c16fa27407
@ -0,0 +1,607 @@
|
||||
#include <BulletCollision/CollisionShapes/btCollisionShape.h>
|
||||
#include <LinearMath/btThreads.h>
|
||||
|
||||
#include "components/debug/debuglog.hpp"
|
||||
#include <components/misc/barrier.hpp>
|
||||
#include "components/misc/convert.hpp"
|
||||
#include "components/settings/settings.hpp"
|
||||
#include "../mwmechanics/actorutil.hpp"
|
||||
#include "../mwmechanics/movement.hpp"
|
||||
#include "../mwworld/class.hpp"
|
||||
#include "../mwworld/player.hpp"
|
||||
|
||||
#include "actor.hpp"
|
||||
#include "movementsolver.hpp"
|
||||
#include "mtphysics.hpp"
|
||||
#include "object.hpp"
|
||||
#include "physicssystem.hpp"
|
||||
|
||||
class btIParallelSumBody; // needed to compile with bullet < 2.88
|
||||
|
||||
namespace
|
||||
{
|
||||
/// @brief A scoped lock that is either shared or exclusive depending on configuration
|
||||
template<class Mutex>
|
||||
class MaybeSharedLock
|
||||
{
|
||||
public:
|
||||
/// @param mutex a shared mutex
|
||||
/// @param canBeSharedLock decide wether the lock will be shared or exclusive
|
||||
MaybeSharedLock(Mutex& mutex, bool canBeSharedLock) : mMutex(mutex), mCanBeSharedLock(canBeSharedLock)
|
||||
{
|
||||
if (mCanBeSharedLock)
|
||||
mMutex.lock_shared();
|
||||
else
|
||||
mMutex.lock();
|
||||
}
|
||||
|
||||
~MaybeSharedLock()
|
||||
{
|
||||
if (mCanBeSharedLock)
|
||||
mMutex.unlock_shared();
|
||||
else
|
||||
mMutex.unlock();
|
||||
}
|
||||
private:
|
||||
Mutex& mMutex;
|
||||
bool mCanBeSharedLock;
|
||||
};
|
||||
|
||||
void handleFall(MWPhysics::ActorFrameData& actorData, bool simulationPerformed)
|
||||
{
|
||||
const float heightDiff = actorData.mPosition.z() - actorData.mOldHeight;
|
||||
|
||||
const bool isStillOnGround = (simulationPerformed && actorData.mWasOnGround && actorData.mActorRaw->getOnGround());
|
||||
|
||||
if (isStillOnGround || actorData.mFlying || actorData.mSwimming || actorData.mSlowFall < 1)
|
||||
actorData.mNeedLand = true;
|
||||
else if (heightDiff < 0)
|
||||
actorData.mFallHeight += heightDiff;
|
||||
}
|
||||
|
||||
void handleJump(const MWWorld::Ptr &ptr)
|
||||
{
|
||||
const bool isPlayer = (ptr == MWMechanics::getPlayer());
|
||||
// Advance acrobatics and set flag for GetPCJumping
|
||||
if (isPlayer)
|
||||
{
|
||||
ptr.getClass().skillUsageSucceeded(ptr, ESM::Skill::Acrobatics, 0);
|
||||
MWBase::Environment::get().getWorld()->getPlayer().setJumping(true);
|
||||
}
|
||||
|
||||
// Decrease fatigue
|
||||
if (!isPlayer || !MWBase::Environment::get().getWorld()->getGodModeState())
|
||||
{
|
||||
const MWWorld::Store<ESM::GameSetting> &gmst = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();
|
||||
const float fFatigueJumpBase = gmst.find("fFatigueJumpBase")->mValue.getFloat();
|
||||
const float fFatigueJumpMult = gmst.find("fFatigueJumpMult")->mValue.getFloat();
|
||||
const float normalizedEncumbrance = std::min(1.f, ptr.getClass().getNormalizedEncumbrance(ptr));
|
||||
const float fatigueDecrease = fFatigueJumpBase + normalizedEncumbrance * fFatigueJumpMult;
|
||||
MWMechanics::DynamicStat<float> fatigue = ptr.getClass().getCreatureStats(ptr).getFatigue();
|
||||
fatigue.setCurrent(fatigue.getCurrent() - fatigueDecrease);
|
||||
ptr.getClass().getCreatureStats(ptr).setFatigue(fatigue);
|
||||
}
|
||||
ptr.getClass().getMovementSettings(ptr).mPosition[2] = 0;
|
||||
}
|
||||
|
||||
void updateStandingCollision(MWPhysics::ActorFrameData& actorData, MWPhysics::CollisionMap& standingCollisions)
|
||||
{
|
||||
if (!actorData.mStandingOn.isEmpty())
|
||||
standingCollisions[actorData.mPtr] = actorData.mStandingOn;
|
||||
else
|
||||
standingCollisions.erase(actorData.mPtr);
|
||||
}
|
||||
|
||||
void updateMechanics(MWPhysics::ActorFrameData& actorData)
|
||||
{
|
||||
if (actorData.mDidJump)
|
||||
handleJump(actorData.mPtr);
|
||||
|
||||
MWMechanics::CreatureStats& stats = actorData.mPtr.getClass().getCreatureStats(actorData.mPtr);
|
||||
if (actorData.mNeedLand)
|
||||
stats.land(actorData.mPtr == MWMechanics::getPlayer() && (actorData.mFlying || actorData.mSwimming));
|
||||
else if (actorData.mFallHeight < 0)
|
||||
stats.addToFallHeight(-actorData.mFallHeight);
|
||||
}
|
||||
|
||||
osg::Vec3f interpolateMovements(const MWPhysics::ActorFrameData& actorData, float timeAccum, float physicsDt)
|
||||
{
|
||||
const float interpolationFactor = timeAccum / physicsDt;
|
||||
return actorData.mPosition * interpolationFactor + actorData.mActorRaw->getPreviousPosition() * (1.f - interpolationFactor);
|
||||
}
|
||||
|
||||
struct WorldFrameData
|
||||
{
|
||||
WorldFrameData() : mIsInStorm(MWBase::Environment::get().getWorld()->isInStorm())
|
||||
, mStormDirection(MWBase::Environment::get().getWorld()->getStormDirection())
|
||||
{}
|
||||
|
||||
bool mIsInStorm;
|
||||
osg::Vec3f mStormDirection;
|
||||
};
|
||||
|
||||
namespace Config
|
||||
{
|
||||
/* The purpose of these 2 classes is to make OpenMW works with Bullet compiled with either single or multithread support.
|
||||
At runtime, Bullet resolve the call to btParallelFor() to:
|
||||
- btITaskScheduler::parallelFor() if bullet is multithreaded
|
||||
- btIParallelForBody::forLoop() if bullet is singlethreaded.
|
||||
|
||||
NOTE: From Bullet 2.88, there is a btDefaultTaskScheduler(), that returns NULL if multithreading is not supported.
|
||||
It might be worth considering to simplify the API once OpenMW stops supporting 2.87.
|
||||
*/
|
||||
|
||||
template<class ...>
|
||||
using void_t = void;
|
||||
|
||||
/// @brief for Bullet <= 2.87
|
||||
template <class T, class = void>
|
||||
class MultiThreadedBulletImpl : public T
|
||||
{
|
||||
public:
|
||||
MultiThreadedBulletImpl(): T("") {};
|
||||
~MultiThreadedBulletImpl() override = default;
|
||||
int getMaxNumThreads() const override { return 1; };
|
||||
int getNumThreads() const override { return 1; };
|
||||
void setNumThreads(int numThreads) override {};
|
||||
|
||||
/// @brief will be called by Bullet if threading is supported
|
||||
void parallelFor(int iBegin, int iEnd, int batchsize, const btIParallelForBody& body) override {};
|
||||
};
|
||||
|
||||
/// @brief for Bullet >= 2.88
|
||||
template <class T>
|
||||
class MultiThreadedBulletImpl<T, void_t<decltype(&T::parallelSum)>> : public T
|
||||
{
|
||||
public:
|
||||
MultiThreadedBulletImpl(): T("") {};
|
||||
~MultiThreadedBulletImpl() override = default;
|
||||
int getMaxNumThreads() const override { return 1; };
|
||||
int getNumThreads() const override { return 1; };
|
||||
void setNumThreads(int numThreads) override {};
|
||||
|
||||
/// @brief will be called by Bullet if threading is supported
|
||||
void parallelFor(int iBegin, int iEnd, int batchsize, const btIParallelForBody& body) override {};
|
||||
|
||||
btScalar parallelSum(int iBegin, int iEnd, int grainSize, const btIParallelSumBody& body) override { return {}; };
|
||||
};
|
||||
|
||||
using MultiThreadedBullet = MultiThreadedBulletImpl<btITaskScheduler>;
|
||||
|
||||
class SingleThreadedBullet : public btIParallelForBody
|
||||
{
|
||||
public:
|
||||
explicit SingleThreadedBullet(bool &threadingSupported): mThreadingSupported(threadingSupported) {};
|
||||
/// @brief will be called by Bullet if threading is NOT supported
|
||||
void forLoop(int iBegin, int iEnd) const override
|
||||
{
|
||||
mThreadingSupported = false;
|
||||
}
|
||||
private:
|
||||
bool &mThreadingSupported;
|
||||
};
|
||||
|
||||
/// @return either the number of thread as configured by the user, or 1 if Bullet doesn't support multithreading
|
||||
int computeNumThreads(bool& threadSafeBullet)
|
||||
{
|
||||
int wantedThread = Settings::Manager::getInt("async num threads", "Physics");
|
||||
|
||||
auto bulletScheduler = std::make_unique<MultiThreadedBullet>();
|
||||
btSetTaskScheduler(bulletScheduler.get());
|
||||
bool threadingSupported = true;
|
||||
btParallelFor(0, 0, 0, SingleThreadedBullet(threadingSupported));
|
||||
|
||||
threadSafeBullet = threadingSupported;
|
||||
if (!threadingSupported && wantedThread > 1)
|
||||
{
|
||||
Log(Debug::Warning) << "Bullet was not compiled with multithreading support, 1 async thread will be used";
|
||||
return 1;
|
||||
}
|
||||
return std::max(0, wantedThread);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace MWPhysics
|
||||
{
|
||||
PhysicsTaskScheduler::PhysicsTaskScheduler(float physicsDt, std::shared_ptr<btCollisionWorld> collisionWorld)
|
||||
: mPhysicsDt(physicsDt)
|
||||
, mCollisionWorld(std::move(collisionWorld))
|
||||
, mNumJobs(0)
|
||||
, mRemainingSteps(0)
|
||||
, mLOSCacheExpiry(Settings::Manager::getInt("lineofsight keep inactive cache", "Physics"))
|
||||
, mDeferAabbUpdate(Settings::Manager::getBool("defer aabb update", "Physics"))
|
||||
, mNewFrame(false)
|
||||
, mAdvanceSimulation(false)
|
||||
, mQuit(false)
|
||||
, mNextJob(0)
|
||||
, mNextLOS(0)
|
||||
{
|
||||
mNumThreads = Config::computeNumThreads(mThreadSafeBullet);
|
||||
|
||||
if (mNumThreads >= 1)
|
||||
{
|
||||
for (int i = 0; i < mNumThreads; ++i)
|
||||
mThreads.emplace_back([&] { worker(); } );
|
||||
}
|
||||
else
|
||||
{
|
||||
mLOSCacheExpiry = -1;
|
||||
mDeferAabbUpdate = false;
|
||||
}
|
||||
|
||||
mPreStepBarrier = std::make_unique<Misc::Barrier>(mNumThreads, [&]()
|
||||
{
|
||||
updateAabbs();
|
||||
});
|
||||
|
||||
mPostStepBarrier = std::make_unique<Misc::Barrier>(mNumThreads, [&]()
|
||||
{
|
||||
if (mRemainingSteps)
|
||||
--mRemainingSteps;
|
||||
mNextJob.store(0, std::memory_order_release);
|
||||
updateActorsPositions();
|
||||
});
|
||||
|
||||
mPostSimBarrier = std::make_unique<Misc::Barrier>(mNumThreads, [&]()
|
||||
{
|
||||
udpateActorsAabbs();
|
||||
mNewFrame = false;
|
||||
if (mLOSCacheExpiry >= 0)
|
||||
{
|
||||
std::unique_lock<std::shared_timed_mutex> lock(mLOSCacheMutex);
|
||||
mLOSCache.erase(
|
||||
std::remove_if(mLOSCache.begin(), mLOSCache.end(),
|
||||
[](const LOSRequest& req) { return req.mStale; }),
|
||||
mLOSCache.end());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
PhysicsTaskScheduler::~PhysicsTaskScheduler()
|
||||
{
|
||||
std::unique_lock<std::shared_timed_mutex> lock(mSimulationMutex);
|
||||
mQuit = true;
|
||||
mNumJobs = 0;
|
||||
mRemainingSteps = 0;
|
||||
lock.unlock();
|
||||
mHasJob.notify_all();
|
||||
for (auto& thread : mThreads)
|
||||
thread.join();
|
||||
}
|
||||
|
||||
const PtrPositionList& PhysicsTaskScheduler::moveActors(int numSteps, float timeAccum, std::vector<ActorFrameData>&& actorsData, CollisionMap& standingCollisions, bool skipSimulation)
|
||||
{
|
||||
// This function run in the main thread.
|
||||
// While the mSimulationMutex is held, background physics threads can't run.
|
||||
|
||||
std::unique_lock<std::shared_timed_mutex> lock(mSimulationMutex);
|
||||
|
||||
// start by finishing previous background computation
|
||||
if (mNumThreads != 0)
|
||||
{
|
||||
if (mAdvanceSimulation)
|
||||
standingCollisions.clear();
|
||||
|
||||
for (auto& data : mActorsFrameData)
|
||||
{
|
||||
// Ignore actors that were deleted while the background thread was running
|
||||
if (!data.mActor.lock())
|
||||
continue;
|
||||
|
||||
updateMechanics(data);
|
||||
if (mAdvanceSimulation)
|
||||
updateStandingCollision(data, standingCollisions);
|
||||
}
|
||||
}
|
||||
|
||||
// init
|
||||
mRemainingSteps = numSteps;
|
||||
mTimeAccum = timeAccum;
|
||||
mActorsFrameData = std::move(actorsData);
|
||||
mAdvanceSimulation = (mRemainingSteps != 0);
|
||||
mNewFrame = true;
|
||||
mNumJobs = mActorsFrameData.size();
|
||||
mNextLOS.store(0, std::memory_order_relaxed);
|
||||
mNextJob.store(0, std::memory_order_release);
|
||||
|
||||
if (mAdvanceSimulation)
|
||||
mWorldFrameData = std::make_unique<WorldFrameData>();
|
||||
|
||||
// update each actor position based on latest data
|
||||
for (auto& data : mActorsFrameData)
|
||||
data.updatePosition();
|
||||
|
||||
// we are asked to skip the simulation (load a savegame for instance)
|
||||
// just return the actors' reference position without applying the movements
|
||||
if (skipSimulation)
|
||||
{
|
||||
standingCollisions.clear();
|
||||
mMovementResults.clear();
|
||||
for (const auto& m : mActorsFrameData)
|
||||
mMovementResults[m.mPtr] = m.mPosition;
|
||||
return mMovementResults;
|
||||
}
|
||||
|
||||
if (mNumThreads == 0)
|
||||
{
|
||||
mMovementResults.clear();
|
||||
syncComputation();
|
||||
|
||||
if (mAdvanceSimulation)
|
||||
{
|
||||
standingCollisions.clear();
|
||||
for (auto& data : mActorsFrameData)
|
||||
updateStandingCollision(data, standingCollisions);
|
||||
}
|
||||
return mMovementResults;
|
||||
}
|
||||
|
||||
// Remove actors that were deleted while the background thread was running
|
||||
for (auto& data : mActorsFrameData)
|
||||
{
|
||||
if (!data.mActor.lock())
|
||||
mMovementResults.erase(data.mPtr);
|
||||
}
|
||||
std::swap(mMovementResults, mPreviousMovementResults);
|
||||
|
||||
// mMovementResults is shared between all workers instance
|
||||
// pre-allocate all nodes so that we don't need synchronization
|
||||
mMovementResults.clear();
|
||||
for (const auto& m : mActorsFrameData)
|
||||
mMovementResults[m.mPtr] = m.mPosition;
|
||||
|
||||
lock.unlock();
|
||||
mHasJob.notify_all();
|
||||
return mPreviousMovementResults;
|
||||
}
|
||||
|
||||
void PhysicsTaskScheduler::rayTest(const btVector3& rayFromWorld, const btVector3& rayToWorld, btCollisionWorld::RayResultCallback& resultCallback) const
|
||||
{
|
||||
MaybeSharedLock<std::shared_timed_mutex> lock(mCollisionWorldMutex, mThreadSafeBullet);
|
||||
mCollisionWorld->rayTest(rayFromWorld, rayToWorld, resultCallback);
|
||||
}
|
||||
|
||||
void PhysicsTaskScheduler::convexSweepTest(const btConvexShape* castShape, const btTransform& from, const btTransform& to, btCollisionWorld::ConvexResultCallback& resultCallback) const
|
||||
{
|
||||
MaybeSharedLock<std::shared_timed_mutex> lock(mCollisionWorldMutex, mThreadSafeBullet);
|
||||
mCollisionWorld->convexSweepTest(castShape, from, to, resultCallback);
|
||||
}
|
||||
|
||||
void PhysicsTaskScheduler::contactTest(btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback)
|
||||
{
|
||||
std::shared_lock<std::shared_timed_mutex> lock(mCollisionWorldMutex);
|
||||
mCollisionWorld->contactTest(colObj, resultCallback);
|
||||
}
|
||||
|
||||
boost::optional<btVector3> PhysicsTaskScheduler::getHitPoint(const btTransform& from, btCollisionObject* target)
|
||||
{
|
||||
MaybeSharedLock<std::shared_timed_mutex> lock(mCollisionWorldMutex, mThreadSafeBullet);
|
||||
// target the collision object's world origin, this should be the center of the collision object
|
||||
btTransform rayTo;
|
||||
rayTo.setIdentity();
|
||||
rayTo.setOrigin(target->getWorldTransform().getOrigin());
|
||||
|
||||
btCollisionWorld::ClosestRayResultCallback cb(from.getOrigin(), rayTo.getOrigin());
|
||||
|
||||
mCollisionWorld->rayTestSingle(from, rayTo, target, target->getCollisionShape(), target->getWorldTransform(), cb);
|
||||
if (!cb.hasHit())
|
||||
// didn't hit the target. this could happen if point is already inside the collision box
|
||||
return boost::none;
|
||||
return {cb.m_hitPointWorld};
|
||||
}
|
||||
|
||||
void PhysicsTaskScheduler::aabbTest(const btVector3& aabbMin, const btVector3& aabbMax, btBroadphaseAabbCallback& callback)
|
||||
{
|
||||
std::shared_lock<std::shared_timed_mutex> lock(mCollisionWorldMutex);
|
||||
mCollisionWorld->getBroadphase()->aabbTest(aabbMin, aabbMax, callback);
|
||||
}
|
||||
|
||||
void PhysicsTaskScheduler::getAabb(const btCollisionObject* obj, btVector3& min, btVector3& max)
|
||||
{
|
||||
std::shared_lock<std::shared_timed_mutex> lock(mCollisionWorldMutex);
|
||||
obj->getCollisionShape()->getAabb(obj->getWorldTransform(), min, max);
|
||||
}
|
||||
|
||||
void PhysicsTaskScheduler::setCollisionFilterMask(btCollisionObject* collisionObject, int collisionFilterMask)
|
||||
{
|
||||
std::unique_lock<std::shared_timed_mutex> lock(mCollisionWorldMutex);
|
||||
collisionObject->getBroadphaseHandle()->m_collisionFilterMask = collisionFilterMask;
|
||||
}
|
||||
|
||||
void PhysicsTaskScheduler::addCollisionObject(btCollisionObject* collisionObject, int collisionFilterGroup, int collisionFilterMask)
|
||||
{
|
||||
std::unique_lock<std::shared_timed_mutex> lock(mCollisionWorldMutex);
|
||||
mCollisionWorld->addCollisionObject(collisionObject, collisionFilterGroup, collisionFilterMask);
|
||||
}
|
||||
|
||||
void PhysicsTaskScheduler::removeCollisionObject(btCollisionObject* collisionObject)
|
||||
{
|
||||
std::unique_lock<std::shared_timed_mutex> lock(mCollisionWorldMutex);
|
||||
mCollisionWorld->removeCollisionObject(collisionObject);
|
||||
}
|
||||
|
||||
void PhysicsTaskScheduler::updateSingleAabb(std::weak_ptr<PtrHolder> ptr)
|
||||
{
|
||||
if (mDeferAabbUpdate)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mUpdateAabbMutex);
|
||||
mUpdateAabb.insert(std::move(ptr));
|
||||
}
|
||||
else
|
||||
{
|
||||
std::unique_lock<std::shared_timed_mutex> lock(mCollisionWorldMutex);
|
||||
updatePtrAabb(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
bool PhysicsTaskScheduler::getLineOfSight(const std::weak_ptr<Actor>& actor1, const std::weak_ptr<Actor>& actor2)
|
||||
{
|
||||
std::unique_lock<std::shared_timed_mutex> lock(mLOSCacheMutex);
|
||||
|
||||
auto actorPtr1 = actor1.lock();
|
||||
auto actorPtr2 = actor2.lock();
|
||||
if (!actorPtr1 || !actorPtr2)
|
||||
return false;
|
||||
|
||||
auto req = LOSRequest(actor1, actor2);
|
||||
auto result = std::find(mLOSCache.begin(), mLOSCache.end(), req);
|
||||
if (result == mLOSCache.end())
|
||||
{
|
||||
req.mResult = hasLineOfSight(actorPtr1.get(), actorPtr2.get());
|
||||
if (mLOSCacheExpiry >= 0)
|
||||
mLOSCache.push_back(req);
|
||||
return req.mResult;
|
||||
}
|
||||
result->mAge = 0;
|
||||
return result->mResult;
|
||||
}
|
||||
|
||||
void PhysicsTaskScheduler::refreshLOSCache()
|
||||
{
|
||||
std::shared_lock<std::shared_timed_mutex> lock(mLOSCacheMutex);
|
||||
int job = 0;
|
||||
int numLOS = mLOSCache.size();
|
||||
while ((job = mNextLOS.fetch_add(1, std::memory_order_relaxed)) < numLOS)
|
||||
{
|
||||
auto& req = mLOSCache[job];
|
||||
auto actorPtr1 = req.mActors[0].lock();
|
||||
auto actorPtr2 = req.mActors[1].lock();
|
||||
|
||||
if (req.mAge++ > mLOSCacheExpiry || !actorPtr1 || !actorPtr2)
|
||||
req.mStale = true;
|
||||
else
|
||||
req.mResult = hasLineOfSight(actorPtr1.get(), actorPtr2.get());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PhysicsTaskScheduler::updateAabbs()
|
||||
{
|
||||
std::unique_lock<std::shared_timed_mutex> lock1(mCollisionWorldMutex, std::defer_lock);
|
||||
std::unique_lock<std::mutex> lock2(mUpdateAabbMutex, std::defer_lock);
|
||||
std::lock(lock1, lock2);
|
||||
std::for_each(mUpdateAabb.begin(), mUpdateAabb.end(),
|
||||
[this](const std::weak_ptr<PtrHolder>& ptr) { updatePtrAabb(ptr); });
|
||||
mUpdateAabb.clear();
|
||||
}
|
||||
|
||||
void PhysicsTaskScheduler::updatePtrAabb(const std::weak_ptr<PtrHolder>& ptr)
|
||||
{
|
||||
if (const auto p = ptr.lock())
|
||||
{
|
||||
if (const auto actor = std::dynamic_pointer_cast<Actor>(p))
|
||||
{
|
||||
actor->commitPositionChange();
|
||||
mCollisionWorld->updateSingleAabb(actor->getCollisionObject());
|
||||
}
|
||||
else if (const auto object = std::dynamic_pointer_cast<Object>(p))
|
||||
{
|
||||
object->commitPositionChange();
|
||||
mCollisionWorld->updateSingleAabb(object->getCollisionObject());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void PhysicsTaskScheduler::worker()
|
||||
{
|
||||
std::shared_lock<std::shared_timed_mutex> lock(mSimulationMutex);
|
||||
while (!mQuit)
|
||||
{
|
||||
if (!mNewFrame)
|
||||
mHasJob.wait(lock, [&]() { return mQuit || mNewFrame; });
|
||||
|
||||
if (mDeferAabbUpdate)
|
||||
mPreStepBarrier->wait();
|
||||
|
||||
int job = 0;
|
||||
while ((job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs)
|
||||
{
|
||||
MaybeSharedLock<std::shared_timed_mutex> lockColWorld(mCollisionWorldMutex, mThreadSafeBullet);
|
||||
if(const auto actor = mActorsFrameData[job].mActor.lock())
|
||||
{
|
||||
if (mRemainingSteps)
|
||||
MovementSolver::move(mActorsFrameData[job], mPhysicsDt, mCollisionWorld.get(), *mWorldFrameData);
|
||||
else
|
||||
{
|
||||
auto& actorData = mActorsFrameData[job];
|
||||
handleFall(actorData, mAdvanceSimulation);
|
||||
mMovementResults[actorData.mPtr] = interpolateMovements(actorData, mTimeAccum, mPhysicsDt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mPostStepBarrier->wait();
|
||||
|
||||
if (!mRemainingSteps)
|
||||
{
|
||||
if (mLOSCacheExpiry >= 0)
|
||||
refreshLOSCache();
|
||||
mPostSimBarrier->wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PhysicsTaskScheduler::updateActorsPositions()
|
||||
{
|
||||
std::unique_lock<std::shared_timed_mutex> lock(mCollisionWorldMutex);
|
||||
for (auto& actorData : mActorsFrameData)
|
||||
{
|
||||
if(const auto actor = actorData.mActor.lock())
|
||||
{
|
||||
if (actorData.mPosition == actor->getPosition())
|
||||
actor->setPosition(actorData.mPosition, false); // update previous position to make sure interpolation is correct
|
||||
else
|
||||
{
|
||||
actorData.mPositionChanged = true;
|
||||
actor->setPosition(actorData.mPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PhysicsTaskScheduler::udpateActorsAabbs()
|
||||
{
|
||||
std::unique_lock<std::shared_timed_mutex> lock(mCollisionWorldMutex);
|
||||
for (const auto& actorData : mActorsFrameData)
|
||||
if (actorData.mPositionChanged)
|
||||
{
|
||||
if(const auto actor = actorData.mActor.lock())
|
||||
mCollisionWorld->updateSingleAabb(actor->getCollisionObject());
|
||||
}
|
||||
}
|
||||
|
||||
bool PhysicsTaskScheduler::hasLineOfSight(const Actor* actor1, const Actor* actor2)
|
||||
{
|
||||
btVector3 pos1 = Misc::Convert::toBullet(actor1->getCollisionObjectPosition() + osg::Vec3f(0,0,actor1->getHalfExtents().z() * 0.9)); // eye level
|
||||
btVector3 pos2 = Misc::Convert::toBullet(actor2->getCollisionObjectPosition() + osg::Vec3f(0,0,actor2->getHalfExtents().z() * 0.9));
|
||||
|
||||
btCollisionWorld::ClosestRayResultCallback resultCallback(pos1, pos2);
|
||||
resultCallback.m_collisionFilterGroup = 0xFF;
|
||||
resultCallback.m_collisionFilterMask = CollisionType_World|CollisionType_HeightMap|CollisionType_Door;
|
||||
|
||||
MaybeSharedLock<std::shared_timed_mutex> lockColWorld(mCollisionWorldMutex, mThreadSafeBullet);
|
||||
mCollisionWorld->rayTest(pos1, pos2, resultCallback);
|
||||
|
||||
return !resultCallback.hasHit();
|
||||
}
|
||||
|
||||
void PhysicsTaskScheduler::syncComputation()
|
||||
{
|
||||
while (mRemainingSteps--)
|
||||
{
|
||||
for (auto& actorData : mActorsFrameData)
|
||||
MovementSolver::move(actorData, mPhysicsDt, mCollisionWorld.get(), *mWorldFrameData);
|
||||
|
||||
updateActorsPositions();
|
||||
}
|
||||
|
||||
for (auto& actorData : mActorsFrameData)
|
||||
{
|
||||
handleFall(actorData, mAdvanceSimulation);
|
||||
mMovementResults[actorData.mPtr] = interpolateMovements(actorData, mTimeAccum, mPhysicsDt);
|
||||
updateMechanics(actorData);
|
||||
}
|
||||
udpateActorsAabbs();
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
#ifndef OPENMW_MWPHYSICS_MTPHYSICS_H
|
||||
#define OPENMW_MWPHYSICS_MTPHYSICS_H
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <thread>
|
||||
#include <shared_mutex>
|
||||
|
||||
#include <boost/optional/optional.hpp>
|
||||
#include <BulletCollision/CollisionDispatch/btCollisionWorld.h>
|
||||
|
||||
#include "physicssystem.hpp"
|
||||
#include "ptrholder.hpp"
|
||||
|
||||
namespace Misc
|
||||
{
|
||||
class Barrier;
|
||||
}
|
||||
|
||||
namespace MWPhysics
|
||||
{
|
||||
class PhysicsTaskScheduler
|
||||
{
|
||||
public:
|
||||
PhysicsTaskScheduler(float physicsDt, std::shared_ptr<btCollisionWorld> collisionWorld);
|
||||
~PhysicsTaskScheduler();
|
||||
|
||||
/// @brief move actors taking into account desired movements and collisions
|
||||
/// @param numSteps how much simulation step to run
|
||||
/// @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 PtrPositionList& moveActors(int numSteps, float timeAccum, std::vector<ActorFrameData>&& actorsData, CollisionMap& standingCollisions, bool skip);
|
||||
|
||||
// Thread safe wrappers
|
||||
void rayTest(const btVector3& rayFromWorld, const btVector3& rayToWorld, btCollisionWorld::RayResultCallback& resultCallback) const;
|
||||
void convexSweepTest(const btConvexShape* castShape, const btTransform& from, const btTransform& to, btCollisionWorld::ConvexResultCallback& resultCallback) const;
|
||||
void contactTest(btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback);
|
||||
boost::optional<btVector3> getHitPoint(const btTransform& from, btCollisionObject* target);
|
||||
void aabbTest(const btVector3& aabbMin, const btVector3& aabbMax, btBroadphaseAabbCallback& callback);
|
||||
void getAabb(const btCollisionObject* obj, btVector3& min, btVector3& max);
|
||||
void setCollisionFilterMask(btCollisionObject* collisionObject, int collisionFilterMask);
|
||||
void addCollisionObject(btCollisionObject* collisionObject, int collisionFilterGroup, int collisionFilterMask);
|
||||
void removeCollisionObject(btCollisionObject* collisionObject);
|
||||
void updateSingleAabb(std::weak_ptr<PtrHolder> ptr);
|
||||
bool getLineOfSight(const std::weak_ptr<Actor>& actor1, const std::weak_ptr<Actor>& actor2);
|
||||
|
||||
private:
|
||||
void syncComputation();
|
||||
void worker();
|
||||
void updateActorsPositions();
|
||||
void udpateActorsAabbs();
|
||||
bool hasLineOfSight(const Actor* actor1, const Actor* actor2);
|
||||
void refreshLOSCache();
|
||||
void updateAabbs();
|
||||
void updatePtrAabb(const std::weak_ptr<PtrHolder>& ptr);
|
||||
|
||||
std::unique_ptr<WorldFrameData> mWorldFrameData;
|
||||
std::vector<ActorFrameData> mActorsFrameData;
|
||||
PtrPositionList mMovementResults;
|
||||
PtrPositionList mPreviousMovementResults;
|
||||
const float mPhysicsDt;
|
||||
float mTimeAccum;
|
||||
std::shared_ptr<btCollisionWorld> mCollisionWorld;
|
||||
std::vector<LOSRequest> mLOSCache;
|
||||
std::set<std::weak_ptr<PtrHolder>, std::owner_less<std::weak_ptr<PtrHolder>>> mUpdateAabb;
|
||||
|
||||
// TODO: use std::experimental::flex_barrier or std::barrier once it becomes a thing
|
||||
std::unique_ptr<Misc::Barrier> mPreStepBarrier;
|
||||
std::unique_ptr<Misc::Barrier> mPostStepBarrier;
|
||||
std::unique_ptr<Misc::Barrier> mPostSimBarrier;
|
||||
|
||||
int mNumThreads;
|
||||
int mNumJobs;
|
||||
int mRemainingSteps;
|
||||
int mLOSCacheExpiry;
|
||||
bool mDeferAabbUpdate;
|
||||
bool mNewFrame;
|
||||
bool mAdvanceSimulation;
|
||||
bool mThreadSafeBullet;
|
||||
bool mQuit;
|
||||
std::atomic<int> mNextJob;
|
||||
std::atomic<int> mNextLOS;
|
||||
std::vector<std::thread> mThreads;
|
||||
|
||||
mutable std::shared_timed_mutex mSimulationMutex;
|
||||
mutable std::shared_timed_mutex mCollisionWorldMutex;
|
||||
mutable std::shared_timed_mutex mLOSCacheMutex;
|
||||
mutable std::mutex mUpdateAabbMutex;
|
||||
std::condition_variable_any mHasJob;
|
||||
};
|
||||
|
||||
}
|
||||
#endif
|
@ -0,0 +1,51 @@
|
||||
#ifndef OPENMW_BARRIER_H
|
||||
#define OPENMW_BARRIER_H
|
||||
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
|
||||
namespace Misc
|
||||
{
|
||||
/// @brief Synchronize several threads
|
||||
class Barrier
|
||||
{
|
||||
public:
|
||||
using BarrierCallback = std::function<void(void)>;
|
||||
/// @param count number of threads to wait on
|
||||
/// @param func callable to be executed once after all threads have met
|
||||
Barrier(int count, BarrierCallback&& func) : mThreadCount(count), mRendezvousCount(0), mGeneration(0)
|
||||
, mFunc(std::forward<BarrierCallback>(func))
|
||||
{}
|
||||
|
||||
/// @brief stop execution of threads until count distinct threads reach this point
|
||||
void wait()
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mMutex);
|
||||
|
||||
++mRendezvousCount;
|
||||
const int currentGeneration = mGeneration;
|
||||
if (mRendezvousCount == mThreadCount)
|
||||
{
|
||||
++mGeneration;
|
||||
mRendezvousCount = 0;
|
||||
mFunc();
|
||||
mRendezvous.notify_all();
|
||||
}
|
||||
else
|
||||
{
|
||||
mRendezvous.wait(lock, [&]() { return mGeneration != currentGeneration; });
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
int mThreadCount;
|
||||
int mRendezvousCount;
|
||||
int mGeneration;
|
||||
mutable std::mutex mMutex;
|
||||
std::condition_variable mRendezvous;
|
||||
BarrierCallback mFunc;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
@ -0,0 +1,37 @@
|
||||
Physics Settings
|
||||
################
|
||||
|
||||
async num threads
|
||||
-----------------
|
||||
|
||||
:Type: integer
|
||||
:Range: >= 0
|
||||
:Default: 0
|
||||
|
||||
Determines how many threads will be spawned to compute physics update in the background (that is, process actors movement). A value of 0 means that the update will be performed in the main thread.
|
||||
A value greater than 1 requires the Bullet library be compiled with multithreading support. If that's not the case, a warning will be written in ``openmw.log`` and a value of 1 will be used.
|
||||
|
||||
lineofsight keep inactive cache
|
||||
-------------------------------
|
||||
|
||||
:Type: integer
|
||||
:Range: >= -1
|
||||
:Default: 0
|
||||
|
||||
The line of sight determines if 2 actors can see each other (without taking into account game mechanics such as invisibility or sneaking). It is used by some scripts (the getLOS function), by the AI (to determine if an actor should start combat or chase an opponent) and for functionnalities such as greetings or turning NPC head toward an object.
|
||||
This parameters determine for how long a cache of request should be kept warm. It depends on :ref:`async num threads` being > 0, otherwise a value of -1 will be used. If a request is not found in the cache, it is always fulfilled immediately. In case Bullet is compiled without multithreading support, non-cached requests involve blocking the async thread(s), which might hurt performance.
|
||||
A value of -1 means no caching.
|
||||
A value of 0 means that for as long as a request is made (after the first one), it will be preemptively "refreshed" in the async thread, without blocking neither the main thread nor the async thread.
|
||||
Any value > 0 is the number of frames for which the values are kept in cache even if the results was not requested again.
|
||||
If Bullet is compiled with multithreading support, requests are non blocking, it is better to set this parameter to -1.
|
||||
|
||||
defer aabb update
|
||||
-----------------
|
||||
|
||||
:Type: boolean
|
||||
:Range: True/False
|
||||
:Default: True
|
||||
|
||||
Axis-aligned bounding box (aabb for short) are used by Bullet for collision detection. They should be updated anytime a physical object is modified (for instance moved) for collision detection to be correct.
|
||||
This parameter control wether the update should be done as soon as the object is modified (the default), which involves blocking the async thread(s), or queue the modifications to update them as a batch before the collision detections. It depends on :ref:`async num threads` being > 0, otherwise it will be disabled.
|
||||
Disabling this parameter is intended as an aid for debugging collisions detection issues.
|
Loading…
Reference in New Issue