#include "cellpreloader.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwrender/landmanager.hpp" #include "cellstore.hpp" #include "class.hpp" namespace MWWorld { namespace { bool contains(std::span 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 container, std::span 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& 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 mMeshes; Resource::SceneManager* mSceneManager; Resource::BulletShapeManager* mBulletShapeManager; Resource::KeyframeManager* mKeyframeManager; Terrain::World* mTerrain; MWRender::LandManager* mLandManager; bool mPreloadInstances; std::atomic mAbort; osg::ref_ptr 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> mPreloadedObjects; }; class TerrainPreloadItem : public SceneUtil::WorkItem { public: explicit TerrainPreloadItem(const std::vector>& views, Terrain::World* world, std::span 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 mAbort; std::vector> mTerrainViews; Terrain::World* mWorld; std::vector 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::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 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 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 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); } }