mirror of
https://gitlab.com/OpenMW/openmw.git
synced 2025-01-09 12:42:11 +00:00
469 lines
16 KiB
C++
469 lines
16 KiB
C++
#include "cellpreloader.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <limits>
|
|
#include <span>
|
|
|
|
#include <osg/Stats>
|
|
|
|
#include <components/debug/debuglog.hpp>
|
|
#include <components/esm3/loadcell.hpp>
|
|
#include <components/loadinglistener/reporter.hpp>
|
|
#include <components/misc/constants.hpp>
|
|
#include <components/misc/resourcehelpers.hpp>
|
|
#include <components/misc/strings/algorithm.hpp>
|
|
#include <components/misc/strings/lower.hpp>
|
|
#include <components/resource/bulletshapemanager.hpp>
|
|
#include <components/resource/keyframemanager.hpp>
|
|
#include <components/resource/resourcesystem.hpp>
|
|
#include <components/resource/scenemanager.hpp>
|
|
#include <components/terrain/view.hpp>
|
|
#include <components/terrain/world.hpp>
|
|
#include <components/vfs/manager.hpp>
|
|
|
|
#include "../mwrender/landmanager.hpp"
|
|
|
|
#include "cellstore.hpp"
|
|
#include "class.hpp"
|
|
|
|
namespace MWWorld
|
|
{
|
|
namespace
|
|
{
|
|
bool contains(std::span<const PositionCellGrid> positions, const PositionCellGrid& contained, float tolerance)
|
|
{
|
|
const float squaredTolerance = tolerance * tolerance;
|
|
const auto predicate = [&](const PositionCellGrid& v) {
|
|
return (contained.mPosition - v.mPosition).length2() < squaredTolerance
|
|
&& contained.mCellBounds == v.mCellBounds;
|
|
};
|
|
return std::ranges::any_of(positions, predicate);
|
|
}
|
|
|
|
bool contains(
|
|
std::span<const PositionCellGrid> container, std::span<const PositionCellGrid> contained, float tolerance)
|
|
{
|
|
const auto predicate = [&](const PositionCellGrid& v) { return contains(container, v, tolerance); };
|
|
return std::ranges::all_of(contained, predicate);
|
|
}
|
|
}
|
|
|
|
struct ListModelsVisitor
|
|
{
|
|
bool operator()(const MWWorld::ConstPtr& ptr)
|
|
{
|
|
ptr.getClass().getModelsToPreload(ptr, mOut);
|
|
|
|
return true;
|
|
}
|
|
|
|
std::vector<std::string_view>& mOut;
|
|
};
|
|
|
|
/// Worker thread item: preload models in a cell.
|
|
class PreloadItem : public SceneUtil::WorkItem
|
|
{
|
|
public:
|
|
/// Constructor to be called from the main thread.
|
|
explicit PreloadItem(MWWorld::CellStore* cell, Resource::SceneManager* sceneManager,
|
|
Resource::BulletShapeManager* bulletShapeManager, Resource::KeyframeManager* keyframeManager,
|
|
Terrain::World* terrain, MWRender::LandManager* landManager, bool preloadInstances)
|
|
: mIsExterior(cell->getCell()->isExterior())
|
|
, mCellLocation(cell->getCell()->getExteriorCellLocation())
|
|
, mCellId(cell->getCell()->getId())
|
|
, mSceneManager(sceneManager)
|
|
, mBulletShapeManager(bulletShapeManager)
|
|
, mKeyframeManager(keyframeManager)
|
|
, mTerrain(terrain)
|
|
, mLandManager(landManager)
|
|
, mPreloadInstances(preloadInstances)
|
|
, mAbort(false)
|
|
{
|
|
mTerrainView = mTerrain->createView();
|
|
|
|
ListModelsVisitor visitor{ mMeshes };
|
|
cell->forEachConst(visitor);
|
|
}
|
|
|
|
void abort() override { mAbort = true; }
|
|
|
|
/// Preload work to be called from the worker thread.
|
|
void doWork() override
|
|
{
|
|
if (mIsExterior)
|
|
{
|
|
try
|
|
{
|
|
mTerrain->cacheCell(mTerrainView.get(), mCellLocation.mX, mCellLocation.mY);
|
|
mPreloadedObjects.insert(mLandManager->getLand(mCellLocation));
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
Log(Debug::Warning) << "Failed to cache terrain for exterior cell " << mCellLocation << ": "
|
|
<< e.what();
|
|
}
|
|
}
|
|
|
|
std::string mesh;
|
|
std::string kfname;
|
|
for (std::string_view path : mMeshes)
|
|
{
|
|
if (mAbort)
|
|
break;
|
|
|
|
try
|
|
{
|
|
const VFS::Manager& vfs = *mSceneManager->getVFS();
|
|
mesh = Misc::ResourceHelpers::correctMeshPath(path);
|
|
mesh = Misc::ResourceHelpers::correctActorModelPath(mesh, &vfs);
|
|
|
|
if (!vfs.exists(mesh))
|
|
continue;
|
|
|
|
size_t slashpos = mesh.find_last_of("/\\");
|
|
if (slashpos != std::string::npos && slashpos != mesh.size() - 1)
|
|
{
|
|
if (Misc::StringUtils::toLower(mesh[slashpos + 1]) == 'x'
|
|
&& Misc::StringUtils::ciEndsWith(mesh, ".nif"))
|
|
{
|
|
kfname = mesh;
|
|
kfname.replace(kfname.size() - 4, 4, ".kf");
|
|
if (vfs.exists(kfname))
|
|
mPreloadedObjects.insert(mKeyframeManager->get(kfname));
|
|
}
|
|
}
|
|
mPreloadedObjects.insert(mSceneManager->getTemplate(mesh));
|
|
if (mPreloadInstances)
|
|
mPreloadedObjects.insert(mBulletShapeManager->cacheInstance(mesh));
|
|
else
|
|
mPreloadedObjects.insert(mBulletShapeManager->getShape(mesh));
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
Log(Debug::Warning) << "Failed to preload mesh \"" << path << "\" from cell " << mCellId << ": "
|
|
<< e.what();
|
|
}
|
|
}
|
|
}
|
|
|
|
private:
|
|
bool mIsExterior;
|
|
ESM::ExteriorCellLocation mCellLocation;
|
|
ESM::RefId mCellId;
|
|
std::vector<std::string_view> mMeshes;
|
|
Resource::SceneManager* mSceneManager;
|
|
Resource::BulletShapeManager* mBulletShapeManager;
|
|
Resource::KeyframeManager* mKeyframeManager;
|
|
Terrain::World* mTerrain;
|
|
MWRender::LandManager* mLandManager;
|
|
bool mPreloadInstances;
|
|
|
|
std::atomic<bool> mAbort;
|
|
|
|
osg::ref_ptr<Terrain::View> mTerrainView;
|
|
|
|
// keep a ref to the loaded objects to make sure it stays loaded as long as this cell is in the preloaded state
|
|
std::set<osg::ref_ptr<const osg::Object>> mPreloadedObjects;
|
|
};
|
|
|
|
class TerrainPreloadItem : public SceneUtil::WorkItem
|
|
{
|
|
public:
|
|
explicit TerrainPreloadItem(const std::vector<osg::ref_ptr<Terrain::View>>& views, Terrain::World* world,
|
|
std::span<const PositionCellGrid> preloadPositions)
|
|
: mAbort(false)
|
|
, mTerrainViews(views)
|
|
, mWorld(world)
|
|
, mPreloadPositions(preloadPositions.begin(), preloadPositions.end())
|
|
{
|
|
}
|
|
|
|
void doWork() override
|
|
{
|
|
for (unsigned int i = 0; i < mTerrainViews.size() && i < mPreloadPositions.size() && !mAbort; ++i)
|
|
{
|
|
mTerrainViews[i]->reset();
|
|
mWorld->preload(mTerrainViews[i], mPreloadPositions[i].mPosition, mPreloadPositions[i].mCellBounds,
|
|
mAbort, mLoadingReporter);
|
|
}
|
|
mLoadingReporter.complete();
|
|
}
|
|
|
|
void abort() override { mAbort = true; }
|
|
|
|
void wait(Loading::Listener& listener) const { mLoadingReporter.wait(listener); }
|
|
|
|
private:
|
|
std::atomic<bool> mAbort;
|
|
std::vector<osg::ref_ptr<Terrain::View>> mTerrainViews;
|
|
Terrain::World* mWorld;
|
|
std::vector<PositionCellGrid> mPreloadPositions;
|
|
Loading::Reporter mLoadingReporter;
|
|
};
|
|
|
|
/// Worker thread item: update the resource system's cache, effectively deleting unused entries.
|
|
class UpdateCacheItem : public SceneUtil::WorkItem
|
|
{
|
|
public:
|
|
UpdateCacheItem(Resource::ResourceSystem* resourceSystem, double referenceTime)
|
|
: mReferenceTime(referenceTime)
|
|
, mResourceSystem(resourceSystem)
|
|
{
|
|
}
|
|
|
|
void doWork() override { mResourceSystem->updateCache(mReferenceTime); }
|
|
|
|
private:
|
|
double mReferenceTime;
|
|
Resource::ResourceSystem* mResourceSystem;
|
|
};
|
|
|
|
CellPreloader::CellPreloader(Resource::ResourceSystem* resourceSystem,
|
|
Resource::BulletShapeManager* bulletShapeManager, Terrain::World* terrain, MWRender::LandManager* landManager)
|
|
: mResourceSystem(resourceSystem)
|
|
, mBulletShapeManager(bulletShapeManager)
|
|
, mTerrain(terrain)
|
|
, mLandManager(landManager)
|
|
, mExpiryDelay(0.0)
|
|
, mPreloadInstances(true)
|
|
, mLastResourceCacheUpdate(0.0)
|
|
, mLoadedTerrainTimestamp(0.0)
|
|
{
|
|
}
|
|
|
|
CellPreloader::~CellPreloader()
|
|
{
|
|
clearAllTasks();
|
|
}
|
|
|
|
void CellPreloader::preload(CellStore& cell, double timestamp)
|
|
{
|
|
if (!mWorkQueue)
|
|
{
|
|
Log(Debug::Error) << "Error: can't preload, no work queue set";
|
|
return;
|
|
}
|
|
if (cell.getState() == CellStore::State_Unloaded)
|
|
{
|
|
Log(Debug::Error) << "Error: can't preload objects for unloaded cell";
|
|
return;
|
|
}
|
|
|
|
PreloadMap::iterator found = mPreloadCells.find(&cell);
|
|
if (found != mPreloadCells.end())
|
|
{
|
|
// already preloaded, nothing to do other than updating the timestamp
|
|
found->second.mTimeStamp = timestamp;
|
|
return;
|
|
}
|
|
|
|
while (mPreloadCells.size() >= mMaxCacheSize)
|
|
{
|
|
// throw out oldest cell to make room
|
|
PreloadMap::iterator oldestCell = mPreloadCells.begin();
|
|
double oldestTimestamp = std::numeric_limits<double>::max();
|
|
double threshold = 1.0; // seconds
|
|
for (PreloadMap::iterator it = mPreloadCells.begin(); it != mPreloadCells.end(); ++it)
|
|
{
|
|
if (it->second.mTimeStamp < oldestTimestamp)
|
|
{
|
|
oldestTimestamp = it->second.mTimeStamp;
|
|
oldestCell = it;
|
|
}
|
|
}
|
|
|
|
if (oldestTimestamp + threshold < timestamp)
|
|
{
|
|
oldestCell->second.mWorkItem->abort();
|
|
mPreloadCells.erase(oldestCell);
|
|
++mEvicted;
|
|
}
|
|
else
|
|
return;
|
|
}
|
|
|
|
osg::ref_ptr<PreloadItem> item(new PreloadItem(&cell, mResourceSystem->getSceneManager(), mBulletShapeManager,
|
|
mResourceSystem->getKeyframeManager(), mTerrain, mLandManager, mPreloadInstances));
|
|
mWorkQueue->addWorkItem(item);
|
|
|
|
mPreloadCells.emplace(&cell, PreloadEntry(timestamp, item));
|
|
++mAdded;
|
|
}
|
|
|
|
void CellPreloader::notifyLoaded(CellStore* cell)
|
|
{
|
|
PreloadMap::iterator found = mPreloadCells.find(cell);
|
|
if (found != mPreloadCells.end())
|
|
{
|
|
if (found->second.mWorkItem)
|
|
{
|
|
found->second.mWorkItem->abort();
|
|
found->second.mWorkItem = nullptr;
|
|
}
|
|
|
|
mPreloadCells.erase(found);
|
|
++mLoaded;
|
|
}
|
|
}
|
|
|
|
void CellPreloader::clear()
|
|
{
|
|
for (PreloadMap::iterator it = mPreloadCells.begin(); it != mPreloadCells.end();)
|
|
{
|
|
if (it->second.mWorkItem)
|
|
{
|
|
it->second.mWorkItem->abort();
|
|
it->second.mWorkItem = nullptr;
|
|
}
|
|
|
|
mPreloadCells.erase(it++);
|
|
}
|
|
}
|
|
|
|
void CellPreloader::updateCache(double timestamp)
|
|
{
|
|
for (PreloadMap::iterator it = mPreloadCells.begin(); it != mPreloadCells.end();)
|
|
{
|
|
if (mPreloadCells.size() >= mMinCacheSize && it->second.mTimeStamp < timestamp - mExpiryDelay)
|
|
{
|
|
if (it->second.mWorkItem)
|
|
{
|
|
it->second.mWorkItem->abort();
|
|
it->second.mWorkItem = nullptr;
|
|
}
|
|
mPreloadCells.erase(it++);
|
|
++mExpired;
|
|
}
|
|
else
|
|
++it;
|
|
}
|
|
|
|
if (timestamp - mLastResourceCacheUpdate > 1.0 && (!mUpdateCacheItem || mUpdateCacheItem->isDone()))
|
|
{
|
|
// the resource cache is cleared from the worker thread so that we're not holding up the main thread with
|
|
// delete operations
|
|
mUpdateCacheItem = new UpdateCacheItem(mResourceSystem, timestamp);
|
|
mWorkQueue->addWorkItem(mUpdateCacheItem, true);
|
|
mLastResourceCacheUpdate = timestamp;
|
|
}
|
|
|
|
if (mTerrainPreloadItem && mTerrainPreloadItem->isDone())
|
|
{
|
|
mLoadedTerrainPositions = mTerrainPreloadPositions;
|
|
mLoadedTerrainTimestamp = timestamp;
|
|
}
|
|
}
|
|
|
|
void CellPreloader::setExpiryDelay(double expiryDelay)
|
|
{
|
|
mExpiryDelay = expiryDelay;
|
|
}
|
|
|
|
void CellPreloader::setPreloadInstances(bool preload)
|
|
{
|
|
mPreloadInstances = preload;
|
|
}
|
|
|
|
void CellPreloader::setWorkQueue(osg::ref_ptr<SceneUtil::WorkQueue> workQueue)
|
|
{
|
|
mWorkQueue = workQueue;
|
|
}
|
|
|
|
void CellPreloader::syncTerrainLoad(Loading::Listener& listener)
|
|
{
|
|
if (mTerrainPreloadItem != nullptr && !mTerrainPreloadItem->isDone())
|
|
mTerrainPreloadItem->wait(listener);
|
|
}
|
|
|
|
void CellPreloader::abortTerrainPreloadExcept(const PositionCellGrid* exceptPos)
|
|
{
|
|
if (exceptPos != nullptr && contains(mTerrainPreloadPositions, *exceptPos, Constants::CellSizeInUnits))
|
|
return;
|
|
if (mTerrainPreloadItem && !mTerrainPreloadItem->isDone())
|
|
{
|
|
mTerrainPreloadItem->abort();
|
|
mTerrainPreloadItem->waitTillDone();
|
|
}
|
|
setTerrainPreloadPositions({});
|
|
}
|
|
|
|
void CellPreloader::setTerrainPreloadPositions(std::span<const PositionCellGrid> positions)
|
|
{
|
|
if (positions.empty())
|
|
{
|
|
mTerrainPreloadPositions.clear();
|
|
mLoadedTerrainPositions.clear();
|
|
}
|
|
else if (contains(mTerrainPreloadPositions, positions, 128.f))
|
|
return;
|
|
if (mTerrainPreloadItem && !mTerrainPreloadItem->isDone())
|
|
return;
|
|
else
|
|
{
|
|
if (mTerrainViews.size() > positions.size())
|
|
mTerrainViews.resize(positions.size());
|
|
else if (mTerrainViews.size() < positions.size())
|
|
{
|
|
for (unsigned int i = mTerrainViews.size(); i < positions.size(); ++i)
|
|
mTerrainViews.emplace_back(mTerrain->createView());
|
|
}
|
|
|
|
mTerrainPreloadPositions.assign(positions.begin(), positions.end());
|
|
if (!positions.empty())
|
|
{
|
|
mTerrainPreloadItem = new TerrainPreloadItem(mTerrainViews, mTerrain, positions);
|
|
mWorkQueue->addWorkItem(mTerrainPreloadItem);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool CellPreloader::isTerrainLoaded(const PositionCellGrid& position, double referenceTime) const
|
|
{
|
|
return mLoadedTerrainTimestamp + mResourceSystem->getSceneManager()->getExpiryDelay() > referenceTime
|
|
&& contains(mLoadedTerrainPositions, position, Constants::CellSizeInUnits);
|
|
}
|
|
|
|
void CellPreloader::setTerrain(Terrain::World* terrain)
|
|
{
|
|
if (terrain != mTerrain)
|
|
{
|
|
clearAllTasks();
|
|
mTerrain = terrain;
|
|
}
|
|
}
|
|
|
|
void CellPreloader::clearAllTasks()
|
|
{
|
|
if (mTerrainPreloadItem)
|
|
{
|
|
mTerrainPreloadItem->abort();
|
|
mTerrainPreloadItem->waitTillDone();
|
|
mTerrainPreloadItem = nullptr;
|
|
}
|
|
|
|
if (mUpdateCacheItem)
|
|
{
|
|
mUpdateCacheItem->waitTillDone();
|
|
mUpdateCacheItem = nullptr;
|
|
}
|
|
|
|
for (PreloadMap::iterator it = mPreloadCells.begin(); it != mPreloadCells.end(); ++it)
|
|
it->second.mWorkItem->abort();
|
|
|
|
for (PreloadMap::iterator it = mPreloadCells.begin(); it != mPreloadCells.end(); ++it)
|
|
it->second.mWorkItem->waitTillDone();
|
|
|
|
mPreloadCells.clear();
|
|
}
|
|
|
|
void CellPreloader::reportStats(unsigned int frameNumber, osg::Stats& stats) const
|
|
{
|
|
stats.setAttribute(frameNumber, "CellPreloader Count", mPreloadCells.size());
|
|
stats.setAttribute(frameNumber, "CellPreloader Added", mAdded);
|
|
stats.setAttribute(frameNumber, "CellPreloader Evicted", mEvicted);
|
|
stats.setAttribute(frameNumber, "CellPreloader Loaded", mLoaded);
|
|
stats.setAttribute(frameNumber, "CellPreloader Expired", mExpired);
|
|
}
|
|
}
|