From 0df8869f596a3a4a28129cbfbc499890df78c376 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sun, 1 Jun 2025 22:16:21 +0300 Subject: [PATCH 1/9] Cache computed supported directions Invalidated when animation sources are cleared or added --- apps/openmw/mwrender/animation.cpp | 37 +++++++++++++++++++----------- apps/openmw/mwrender/animation.hpp | 1 + 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index f07a325f7c..98a2246cb5 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -722,6 +722,7 @@ namespace MWRender mAnimSources.push_back(animsrc); + mSupportedDirections.clear(); for (const std::string& group : mAnimSources.back()->getTextKeys().getGroups()) mSupportedAnimations.insert(group); @@ -795,6 +796,7 @@ namespace MWRender mAccumCtrl = nullptr; mSupportedAnimations.clear(); + mSupportedDirections.clear(); mAnimSources.clear(); mAnimVelocities.clear(); @@ -2012,20 +2014,29 @@ namespace MWRender std::span prefixes) const { MWWorld::MovementDirectionFlags result = 0; - for (const std::string_view animation : mSupportedAnimations) + for (const std::string_view prefix : prefixes) { - if (std::find_if( - prefixes.begin(), prefixes.end(), [&](std::string_view v) { return animation.starts_with(v); }) - == prefixes.end()) - continue; - if (animation.ends_with("forward")) - result |= MWWorld::MovementDirectionFlag_Forward; - else if (animation.ends_with("back")) - result |= MWWorld::MovementDirectionFlag_Back; - else if (animation.ends_with("left")) - result |= MWWorld::MovementDirectionFlag_Left; - else if (animation.ends_with("right")) - result |= MWWorld::MovementDirectionFlag_Right; + auto it = std::find_if(mSupportedDirections.begin(), mSupportedDirections.end(), + [prefix](const auto& direction) { return direction.first == prefix; }); + if (it == mSupportedDirections.end()) + { + mSupportedDirections.emplace_back(prefix, 0); + it = mSupportedDirections.end() - 1; + for (const std::string_view animation : mSupportedAnimations) + { + if (!animation.starts_with(prefix)) + continue; + if (animation.ends_with("forward")) + it->second |= MWWorld::MovementDirectionFlag_Forward; + else if (animation.ends_with("back")) + it->second |= MWWorld::MovementDirectionFlag_Back; + else if (animation.ends_with("left")) + it->second |= MWWorld::MovementDirectionFlag_Left; + else if (animation.ends_with("right")) + it->second |= MWWorld::MovementDirectionFlag_Right; + } + } + result |= it->second; } return result; } diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index b6cb6f333c..8f7637804d 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -181,6 +181,7 @@ namespace MWRender AnimSourceList mAnimSources; std::unordered_set mSupportedAnimations; + mutable std::vector> mSupportedDirections; osg::ref_ptr mInsert; From a00e5ec828cefd3e30a19c09d02180bd54171a6f Mon Sep 17 00:00:00 2001 From: Florian Heberer Date: Sun, 9 Jun 2024 01:45:08 +0200 Subject: [PATCH 2/9] Add documentation for update function of Generic Object Cache --- components/resource/objectcache.hpp | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/components/resource/objectcache.hpp b/components/resource/objectcache.hpp index e619b7102c..8fdc199e93 100644 --- a/components/resource/objectcache.hpp +++ b/components/resource/objectcache.hpp @@ -53,28 +53,48 @@ namespace Resource class GenericObjectCache : public osg::Referenced { public: - // Update last usage timestamp using referenceTime for each cache time if they are not nullptr and referenced - // from somewhere else. Remove items with last usage > expiryTime. Note: last usage might be updated from other - // places so nullptr or not references elsewhere items are not always removed. + /* + * @brief Updates usage timestamps and removes expired items + * + * Updates the lastUsage timestamp of cached items that have external references. + * Initializes lastUsage timestamp for new items. + * Removes items that haven't been referenced for longer than expiryDelay. + * + * @param referenceTime the timestamp indicating when the item was most recently used + * @param expiryDelay the delay after which the cache entry for an item expires + */ void update(double referenceTime, double expiryDelay) { std::vector> objectsToRemove; + { const double expiryTime = referenceTime - expiryDelay; + std::lock_guard lock(mMutex); + std::erase_if(mItems, [&](auto& v) { Item& item = v.second; + + // update last usage timestamp if item is being referenced externally + // or initialize if not set if ((item.mValue != nullptr && item.mValue->referenceCount() > 1) || item.mLastUsage == 0) item.mLastUsage = referenceTime; + + // skip items that have been accessed since expiryTime if (item.mLastUsage > expiryTime) return false; + ++mExpired; + + // just mark for removal here so objects can be removed in bulk outside the lock if (item.mValue != nullptr) objectsToRemove.push_back(std::move(item.mValue)); + return true; }); } - // note, actual unref happens outside of the lock + + // remove expired items from cache objectsToRemove.clear(); } From 6d74a4607c05629f662b9605dd85dba2a294ae33 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Thu, 10 Jul 2025 17:25:27 +0300 Subject: [PATCH 3/9] Restore some omitted object cache remarks that seemed useful --- components/resource/objectcache.hpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/components/resource/objectcache.hpp b/components/resource/objectcache.hpp index 8fdc199e93..2ff25c92f1 100644 --- a/components/resource/objectcache.hpp +++ b/components/resource/objectcache.hpp @@ -56,20 +56,22 @@ namespace Resource /* * @brief Updates usage timestamps and removes expired items * - * Updates the lastUsage timestamp of cached items that have external references. + * Updates the lastUsage timestamp of cached non-nullptr items that have external references. * Initializes lastUsage timestamp for new items. * Removes items that haven't been referenced for longer than expiryDelay. * + * \note + * Last usage might be updated from other places so nullptr items + * that are not referenced elsewhere are not always removed. + * * @param referenceTime the timestamp indicating when the item was most recently used * @param expiryDelay the delay after which the cache entry for an item expires */ void update(double referenceTime, double expiryDelay) { std::vector> objectsToRemove; - { const double expiryTime = referenceTime - expiryDelay; - std::lock_guard lock(mMutex); std::erase_if(mItems, [&](auto& v) { @@ -93,7 +95,6 @@ namespace Resource return true; }); } - // remove expired items from cache objectsToRemove.clear(); } From 931555c7ffdf6a39b47e2ea494893f044bf8045d Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Tue, 15 Jul 2025 21:57:12 +0200 Subject: [PATCH 4/9] Remove int conversions from pathfinding --- apps/openmw/mwmechanics/aicombat.cpp | 6 +++--- apps/openmw/mwmechanics/aiwander.cpp | 4 ++-- apps/openmw/mwmechanics/pathfinding.cpp | 14 +++++++------- apps/openmw/mwmechanics/pathfinding.hpp | 6 +++--- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 1b0b0e5a89..a6f9935194 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -393,13 +393,13 @@ namespace MWMechanics osg::Vec3f localPos = actor.getRefData().getPosition().asVec3(); coords.toLocal(localPos); - int closestPointIndex = PathFinder::getClosestPoint(pathgrid, localPos); - for (int i = 0; i < static_cast(pathgrid->mPoints.size()); i++) + size_t closestPointIndex = PathFinder::getClosestPoint(pathgrid, localPos); + for (size_t i = 0; i < pathgrid->mPoints.size(); i++) { if (i != closestPointIndex && getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, i)) { - points.push_back(pathgrid->mPoints[static_cast(i)]); + points.push_back(pathgrid->mPoints[i]); } } diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 464d83ad46..3cc7aac838 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -827,7 +827,7 @@ namespace MWMechanics if (pathgrid == nullptr || pathgrid->mPoints.empty()) return; - int index = PathFinder::getClosestPoint(pathgrid, PathFinder::makeOsgVec3(dest)); + size_t index = PathFinder::getClosestPoint(pathgrid, PathFinder::makeOsgVec3(dest)); getPathGridGraph(pathgrid).getNeighbouringPoints(index, points); } @@ -860,7 +860,7 @@ namespace MWMechanics const osg::Vec3f npcPos = converter.toLocalVec3(mInitialActorPosition); // Find closest pathgrid point - int closestPointIndex = PathFinder::getClosestPoint(pathgrid, npcPos); + size_t closestPointIndex = PathFinder::getClosestPoint(pathgrid, npcPos); // mAllowedNodes for this actor with pathgrid point indexes based on mDistance // and if the point is connected to the closest current point diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index 192cbdfe22..dc9d8e4061 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -25,15 +25,15 @@ namespace { // Chooses a reachable end pathgrid point. start is assumed reachable. - std::pair getClosestReachablePoint( - const ESM::Pathgrid* grid, const MWMechanics::PathgridGraph* graph, const osg::Vec3f& pos, int start) + std::pair getClosestReachablePoint( + const ESM::Pathgrid* grid, const MWMechanics::PathgridGraph* graph, const osg::Vec3f& pos, size_t start) { assert(grid && !grid->mPoints.empty()); float closestDistanceBetween = std::numeric_limits::max(); float closestDistanceReachable = std::numeric_limits::max(); - int closestIndex = 0; - int closestReachableIndex = 0; + size_t closestIndex = 0; + size_t closestReachableIndex = 0; // TODO: if this full scan causes performance problems mapping pathgrid // points to a quadtree may help for (size_t counter = 0; counter < grid->mPoints.size(); counter++) @@ -62,7 +62,7 @@ namespace // allowed nodes if not. Hence a path needs to be created even if the start // and the end points are the same. - return std::pair(closestReachableIndex, closestReachableIndex == closestIndex); + return { closestReachableIndex, closestReachableIndex == closestIndex }; } float sqrDistance(const osg::Vec2f& lhs, const osg::Vec2f& rhs) @@ -197,10 +197,10 @@ namespace MWMechanics // point right behind the wall that is closer than any pathgrid // point outside the wall osg::Vec3f startPointInLocalCoords(converter.toLocalVec3(startPoint)); - int startNode = getClosestPoint(pathgrid, startPointInLocalCoords); + size_t startNode = getClosestPoint(pathgrid, startPointInLocalCoords); osg::Vec3f endPointInLocalCoords(converter.toLocalVec3(endPoint)); - std::pair endNode + std::pair endNode = getClosestReachablePoint(pathgrid, &pathgridGraph, endPointInLocalCoords, startNode); // if it's shorter for actor to travel from start to end, than to travel from either diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index 0f688686cd..94242404e4 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -178,16 +178,16 @@ namespace MWMechanics // // NOTE: pos is expected to be in local coordinates, as is grid->mPoints // - static int getClosestPoint(const ESM::Pathgrid* grid, const osg::Vec3f& pos) + static size_t getClosestPoint(const ESM::Pathgrid* grid, const osg::Vec3f& pos) { assert(grid && !grid->mPoints.empty()); float distanceBetween = distanceSquared(grid->mPoints[0], pos); - int closestIndex = 0; + size_t closestIndex = 0; // TODO: if this full scan causes performance problems mapping pathgrid // points to a quadtree may help - for (unsigned int counter = 1; counter < grid->mPoints.size(); counter++) + for (size_t counter = 1; counter < grid->mPoints.size(); counter++) { float potentialDistBetween = distanceSquared(grid->mPoints[counter], pos); if (potentialDistBetween < distanceBetween) From bd6254b6dff5feb0196e08a5638d776577cb8d4e Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Thu, 17 Jul 2025 08:22:26 +0200 Subject: [PATCH 5/9] Ensure corpses of actors that have moved cells are cleaned up --- apps/openmw/mwworld/cellstore.cpp | 42 +++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index 61618c001e..75ee14f627 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -1263,26 +1263,48 @@ namespace MWWorld } } - for (CellRefList::List::iterator it(get().mList.begin()); - it != get().mList.end(); ++it) + // Actors need to respawn here even if they've been moved to another cell + for (LiveCellRefBase& base : get().mList) { - Ptr ptr = getCurrentPtr(&*it); + Ptr ptr = getCurrentPtr(&base); clearCorpse(ptr, mStore); ptr.getClass().respawn(ptr); } - for (CellRefList::List::iterator it(get().mList.begin()); - it != get().mList.end(); ++it) + for (LiveCellRefBase& base : get().mList) { - Ptr ptr = getCurrentPtr(&*it); + Ptr ptr = getCurrentPtr(&base); clearCorpse(ptr, mStore); ptr.getClass().respawn(ptr); } - forEachType([](Ptr ptr) { - // no need to clearCorpse, handled as part of get() + for (LiveCellRefBase& base : get().mList) + { + Ptr ptr = getCurrentPtr(&base); if (!ptr.mRef->isDeleted()) ptr.getClass().respawn(ptr); - return true; - }); + } + for (const auto& [base, _] : mMovedHere) + { + switch (base->getType()) + { + case ESM::Creature::sRecordId: + case ESM::NPC::sRecordId: + case ESM::CreatureLevList::sRecordId: + { + MWWorld::Ptr ptr(base, this); + if (ptr.mRef->isDeleted()) + continue; + // Remove actors that have been dead a while, but don't belong here and didn't get hit by the + // logic above + if (ptr.getClass().isActor()) + clearCorpse(ptr, mStore); + else // Respawn lists in their new position + ptr.getClass().respawn(ptr); + break; + } + default: + break; + } + } } } From 67961213055070f31abb01d1b3a3b264b34aa73b Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Thu, 17 Jul 2025 20:18:57 +0300 Subject: [PATCH 6/9] Use zlib directly for BSA file decompression (#8569) --- components/bsa/ba2dx10file.cpp | 49 ++++++++++------------------ components/bsa/ba2gnrlfile.cpp | 32 +++++------------- components/bsa/compressedbsafile.cpp | 39 +++++++++------------- 3 files changed, 42 insertions(+), 78 deletions(-) diff --git a/components/bsa/ba2dx10file.cpp b/components/bsa/ba2dx10file.cpp index a438121d5b..afb2c0a4aa 100644 --- a/components/bsa/ba2dx10file.cpp +++ b/components/bsa/ba2dx10file.cpp @@ -1,29 +1,12 @@ #include "ba2dx10file.hpp" +#include #include +#include #include #include -#include - -#if defined(_MSC_VER) -// why is this necessary? These are included with /external:I -#pragma warning(push) -#pragma warning(disable : 4706) -#pragma warning(disable : 4702) -#include -#include -#include -#include -#pragma warning(pop) -#else -#include -#include -#include -#include -#endif - -#include +#include #include #include @@ -644,11 +627,16 @@ namespace Bsa size_t headerSize = (header.ddspf.fourCC == ESM::fourCC("DX10") ? sizeof(DDSHeaderDX10) : sizeof(DDSHeader)); size_t textureSize = sizeof(uint32_t) + headerSize; //"DDS " + header + uint32_t maxPackedChunkSize = 0; for (const auto& textureChunk : fileRecord.texturesChunks) + { textureSize += textureChunk.size; + maxPackedChunkSize = std::max(textureChunk.packedSize, maxPackedChunkSize); + } auto memoryStreamPtr = std::make_unique(textureSize); char* buff = memoryStreamPtr->getRawData(); + std::vector inputBuffer(maxPackedChunkSize); uint32_t dds = ESM::fourCC("DDS "); buff = (char*)std::memcpy(buff, &dds, sizeof(uint32_t)) + sizeof(uint32_t); @@ -658,25 +646,22 @@ namespace Bsa // append chunks for (const auto& c : fileRecord.texturesChunks) { + const uint32_t inputSize = c.packedSize != 0 ? c.packedSize : c.size; + Files::IStreamPtr streamPtr = Files::openConstrainedFileStream(mFilepath, c.offset, inputSize); if (c.packedSize != 0) { - Files::IStreamPtr streamPtr = Files::openConstrainedFileStream(mFilepath, c.offset, c.packedSize); - std::istream* fileStream = streamPtr.get(); + streamPtr->read(inputBuffer.data(), c.packedSize); + uLongf destSize = static_cast(c.size); + int ec = ::uncompress(reinterpret_cast(memoryStreamPtr->getRawData() + offset), &destSize, + reinterpret_cast(inputBuffer.data()), static_cast(c.packedSize)); - boost::iostreams::filtering_streambuf inputStreamBuf; - inputStreamBuf.push(boost::iostreams::zlib_decompressor()); - inputStreamBuf.push(*fileStream); - - boost::iostreams::basic_array_sink sr(memoryStreamPtr->getRawData() + offset, c.size); - boost::iostreams::copy(inputStreamBuf, sr); + if (ec != Z_OK) + fail("zlib uncompress failed: " + std::string(::zError(ec))); } // uncompressed chunk else { - Files::IStreamPtr streamPtr = Files::openConstrainedFileStream(mFilepath, c.offset, c.size); - std::istream* fileStream = streamPtr.get(); - - fileStream->read(memoryStreamPtr->getRawData() + offset, c.size); + streamPtr->read(memoryStreamPtr->getRawData() + offset, c.size); } offset += c.size; } diff --git a/components/bsa/ba2gnrlfile.cpp b/components/bsa/ba2gnrlfile.cpp index 75e7305245..f169440208 100644 --- a/components/bsa/ba2gnrlfile.cpp +++ b/components/bsa/ba2gnrlfile.cpp @@ -1,27 +1,11 @@ #include "ba2gnrlfile.hpp" +#include #include #include #include -#include - -#if defined(_MSC_VER) -// why is this necessary? These are included with /external:I -#pragma warning(push) -#pragma warning(disable : 4706) -#pragma warning(disable : 4702) -#include -#include -#include -#pragma warning(pop) -#else -#include -#include -#include -#endif - -#include +#include #include #include @@ -223,12 +207,14 @@ namespace Bsa auto memoryStreamPtr = std::make_unique(fileRecord.size); if (fileRecord.packedSize) { - boost::iostreams::filtering_streambuf inputStreamBuf; - inputStreamBuf.push(boost::iostreams::zlib_decompressor()); - inputStreamBuf.push(*streamPtr); + std::vector buffer(inputSize); + streamPtr->read(buffer.data(), inputSize); + uLongf destSize = static_cast(fileRecord.size); + int ec = ::uncompress(reinterpret_cast(memoryStreamPtr->getRawData()), &destSize, + reinterpret_cast(buffer.data()), static_cast(buffer.size())); - boost::iostreams::basic_array_sink sr(memoryStreamPtr->getRawData(), fileRecord.size); - boost::iostreams::copy(inputStreamBuf, sr); + if (ec != Z_OK) + fail("zlib uncompress failed: " + std::string(::zError(ec))); } else { diff --git a/components/bsa/compressedbsafile.cpp b/components/bsa/compressedbsafile.cpp index 8426c5965c..655a4d2844 100644 --- a/components/bsa/compressedbsafile.cpp +++ b/components/bsa/compressedbsafile.cpp @@ -24,27 +24,13 @@ */ #include "compressedbsafile.hpp" +#include #include #include #include #include - -#if defined(_MSC_VER) -#pragma warning(push) -#pragma warning(disable : 4706) -#pragma warning(disable : 4702) -#include -#include -#include -#pragma warning(pop) -#else -#include -#include -#include -#endif - -#include +#include #include #include @@ -292,19 +278,26 @@ namespace Bsa if (compressed) { + std::vector buffer(size); + streamPtr->read(buffer.data(), size); + if (mHeader.mVersion != Version_SSE) { - boost::iostreams::filtering_streambuf inputStreamBuf; - inputStreamBuf.push(boost::iostreams::zlib_decompressor()); - inputStreamBuf.push(*streamPtr); + uLongf destSize = static_cast(resultSize); + int ec = ::uncompress(reinterpret_cast(memoryStreamPtr->getRawData()), &destSize, + reinterpret_cast(buffer.data()), static_cast(buffer.size())); - boost::iostreams::basic_array_sink sr(memoryStreamPtr->getRawData(), resultSize); - boost::iostreams::copy(inputStreamBuf, sr); + if (ec != Z_OK) + { + std::string message = "zlib uncompress failed for file "; + message.append(fileRecord.mName.begin(), fileRecord.mName.end()); + message += ": "; + message += ::zError(ec); + fail(message); + } } else { - auto buffer = std::vector(size); - streamPtr->read(buffer.data(), size); LZ4F_decompressionContext_t context = nullptr; LZ4F_createDecompressionContext(&context, LZ4F_VERSION); LZ4F_decompressOptions_t options = {}; From 93cb69b0127fc7d36ca91fa14d1a1b47ab9a5499 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Tue, 8 Jul 2025 22:36:43 +0300 Subject: [PATCH 7/9] Assume NIF controller data is already sorted (#8545) --- components/nif/data.cpp | 11 +++-------- components/nif/data.hpp | 4 ++-- components/nif/nifkey.hpp | 24 ++++++++++++++++-------- components/nif/particle.cpp | 12 ++++++++---- components/nifosg/controller.cpp | 3 ++- components/nifosg/controller.hpp | 11 ++++++----- 6 files changed, 37 insertions(+), 28 deletions(-) diff --git a/components/nif/data.cpp b/components/nif/data.cpp index 29b11bd806..a134749bdc 100644 --- a/components/nif/data.cpp +++ b/components/nif/data.cpp @@ -247,16 +247,11 @@ namespace Nif void NiVisData::read(NIFStream* nif) { - mKeys = std::make_shared>(); - uint32_t numKeys; - nif->read(numKeys); - for (size_t i = 0; i < numKeys; i++) + mKeys = std::make_shared>>(nif->get()); + for (auto& [time, value] : *mKeys) { - float time; - char value; nif->read(time); - nif->read(value); - (*mKeys)[time] = (value != 0); + value = nif->get() != 0; } } diff --git a/components/nif/data.hpp b/components/nif/data.hpp index 1ccd2919b7..4000055e8c 100644 --- a/components/nif/data.hpp +++ b/components/nif/data.hpp @@ -193,8 +193,8 @@ namespace Nif struct NiVisData : public Record { - // TODO: investigate possible use of BoolKeyMap - std::shared_ptr> mKeys; + // This is theoretically a "flat map" sorted by time + std::shared_ptr>> mKeys; void read(NIFStream* nif) override; }; diff --git a/components/nif/nifkey.hpp b/components/nif/nifkey.hpp index 5a755c8d0d..e32ef76d95 100644 --- a/components/nif/nifkey.hpp +++ b/components/nif/nifkey.hpp @@ -3,7 +3,7 @@ #ifndef OPENMW_COMPONENTS_NIF_NIFKEY_HPP #define OPENMW_COMPONENTS_NIF_NIFKEY_HPP -#include +#include #include #include "exception.hpp" @@ -46,7 +46,8 @@ namespace Nif template struct KeyMapT { - using MapType = std::map>; + // This is theoretically a "flat map" sorted by time + using MapType = std::vector>>; using ValueType = T; using KeyType = KeyT; @@ -78,8 +79,12 @@ namespace Nif uint32_t count; nif->read(count); - if (count != 0 || morph) - nif->read(mInterpolationType); + if (count == 0 && !morph) + return; + + nif->read(mInterpolationType); + + mKeys.reserve(count); KeyType key = {}; @@ -90,7 +95,7 @@ namespace Nif float time; nif->read(time); readValue(*nif, key); - mKeys[time] = key; + mKeys.emplace_back(time, key); } } else if (mInterpolationType == InterpolationType_Quadratic) @@ -100,7 +105,7 @@ namespace Nif float time; nif->read(time); readQuadratic(*nif, key); - mKeys[time] = key; + mKeys.emplace_back(time, key); } } else if (mInterpolationType == InterpolationType_TCB) @@ -115,8 +120,9 @@ namespace Nif nif->read(tcbKey.mBias); } generateTCBTangents(tcbKeys); - for (TCBKey& key : tcbKeys) - mKeys[key.mTime] = KeyType{ std::move(key.mValue), std::move(key.mInTan), std::move(key.mOutTan) }; + for (TCBKey& tcbKey : tcbKeys) + mKeys.emplace_back(std::move(tcbKey.mTime), + KeyType{ std::move(tcbKey.mValue), std::move(tcbKey.mInTan), std::move(tcbKey.mOutTan) }); } else if (mInterpolationType == InterpolationType_XYZ) { @@ -132,6 +138,8 @@ namespace Nif throw Nif::Exception("Unhandled interpolation type: " + std::to_string(mInterpolationType), nif->getFile().getFilename()); } + + // Note: NetImmerse does NOT sort keys or remove duplicates } private: diff --git a/components/nif/particle.cpp b/components/nif/particle.cpp index d81d423fb6..9249541717 100644 --- a/components/nif/particle.cpp +++ b/components/nif/particle.cpp @@ -676,12 +676,16 @@ namespace Nif void NiPSysEmitterCtlrData::read(NIFStream* nif) { + // TODO: this is not used in the official files and needs verification mFloatKeyList = std::make_shared(); + mFloatKeyList->read(nif); mVisKeyList = std::make_shared(); - uint32_t numVisKeys; - nif->read(numVisKeys); - for (size_t i = 0; i < numVisKeys; i++) - mVisKeyList->mKeys[nif->get()].mValue = nif->get() != 0; + mVisKeyList->mKeys.resize(nif->get()); + for (auto& [time, key] : mVisKeyList->mKeys) + { + nif->read(time); + key.mValue = nif->get() != 0; + } } void NiPSysCollider::read(NIFStream* nif) diff --git a/components/nifosg/controller.cpp b/components/nifosg/controller.cpp index 7e4c5da7a0..ee3d0dd45e 100644 --- a/components/nifosg/controller.cpp +++ b/components/nifosg/controller.cpp @@ -374,7 +374,8 @@ namespace NifOsg if (mData->empty()) return true; - auto iter = mData->upper_bound(time); + auto iter = std::upper_bound(mData->begin(), mData->end(), time, + [](float time, const std::pair& key) { return time < key.first; }); if (iter != mData->begin()) --iter; return iter->second; diff --git a/components/nifosg/controller.hpp b/components/nifosg/controller.hpp index 468668ce76..41c8027eea 100644 --- a/components/nifosg/controller.hpp +++ b/components/nifosg/controller.hpp @@ -54,7 +54,8 @@ namespace NifOsg return mLastHighKey; } - return mKeys->mKeys.lower_bound(time); + return std::lower_bound(mKeys->mKeys.begin(), mKeys->mKeys.end(), time, + [](const typename MapT::MapType::value_type& key, float t) { return key.first < t; }); } public: @@ -99,8 +100,8 @@ namespace NifOsg const typename MapT::MapType& keys = mKeys->mKeys; - if (time <= keys.begin()->first) - return keys.begin()->second.mValue; + if (time <= keys.front().first) + return keys.front().second.mValue; typename MapT::MapType::const_iterator it = retrieveKey(time); @@ -116,7 +117,7 @@ namespace NifOsg return interpolate(mLastLowKey->second, mLastHighKey->second, a, mKeys->mInterpolationType); } - return keys.rbegin()->second.mValue; + return keys.back().second.mValue; } bool empty() const { return !mKeys || mKeys->mKeys.empty(); } @@ -283,7 +284,7 @@ namespace NifOsg class VisController : public SceneUtil::NodeCallback, public SceneUtil::Controller { private: - std::shared_ptr> mData; + std::shared_ptr>> mData; BoolInterpolator mInterpolator; unsigned int mMask{ 0u }; From 4a0c998f53426ffff26bc1dd210fac803dc1499a Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Fri, 18 Jul 2025 00:13:01 +0300 Subject: [PATCH 8/9] Avoid zero division during animation interpolation --- components/nifosg/controller.hpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/components/nifosg/controller.hpp b/components/nifosg/controller.hpp index 41c8027eea..cceec279b2 100644 --- a/components/nifosg/controller.hpp +++ b/components/nifosg/controller.hpp @@ -112,7 +112,12 @@ namespace NifOsg mLastHighKey = it; mLastLowKey = --it; - float a = (time - mLastLowKey->first) / (mLastHighKey->first - mLastLowKey->first); + const float highTime = mLastHighKey->first; + const float lowTime = mLastLowKey->first; + if (highTime == lowTime) + return mLastLowKey->second.mValue; + + const float a = (time - lowTime) / (highTime - lowTime); return interpolate(mLastLowKey->second, mLastHighKey->second, a, mKeys->mInterpolationType); } From 7f28b33cf1d2a98b5903afbf71623506b2da9492 Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Sat, 27 May 2023 21:02:29 +0200 Subject: [PATCH 9/9] Object paging in ESM4 worldspaces --- apps/openmw/mwclass/esm4base.hpp | 17 +++-- apps/openmw/mwrender/objectpaging.cpp | 93 +++++++++++++++++++++++++-- apps/openmw/mwworld/scene.cpp | 11 +++- apps/openmw/mwworld/scene.hpp | 6 +- apps/openmw/mwworld/worldimp.cpp | 7 -- 5 files changed, 113 insertions(+), 21 deletions(-) diff --git a/apps/openmw/mwclass/esm4base.hpp b/apps/openmw/mwclass/esm4base.hpp index f13d6007cd..0e7888317e 100644 --- a/apps/openmw/mwclass/esm4base.hpp +++ b/apps/openmw/mwclass/esm4base.hpp @@ -55,6 +55,16 @@ namespace MWClass } return res; } + + // TODO: Figure out a better way to find markers and LOD meshes + inline bool isMarkerModel(std::string_view model) + { + return Misc::StringUtils::ciStartsWith(model, "marker"); + } + inline bool isLodModel(std::string_view model) + { + return Misc::StringUtils::ciEndsWith(model, "lod.nif"); + } } // Base for many ESM4 Classes @@ -100,11 +110,8 @@ namespace MWClass { std::string_view model = getClassModel(ptr); - // Hide meshes meshes/marker/* and *LOD.nif in ESM4 cells. It is a temporarty hack. - // Needed because otherwise LOD meshes are rendered on top of normal meshes. - // TODO: Figure out a better way find markers and LOD meshes; show LOD only outside of active grid. - if (model.empty() || Misc::StringUtils::ciStartsWith(model, "marker") - || Misc::StringUtils::ciEndsWith(model, "lod.nif")) + // TODO: There should be a better way to hide markers + if (ESM4Impl::isMarkerModel(model) || ESM4Impl::isLodModel(model)) return {}; return model; diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp index f45247398f..9f8b1ed86c 100644 --- a/apps/openmw/mwrender/objectpaging.cpp +++ b/apps/openmw/mwrender/objectpaging.cpp @@ -20,6 +20,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include #include #include @@ -36,12 +42,14 @@ #include "apps/openmw/mwbase/environment.hpp" #include "apps/openmw/mwbase/world.hpp" +#include "apps/openmw/mwclass/esm4base.hpp" #include "apps/openmw/mwworld/esmstore.hpp" #include "vismask.hpp" namespace MWRender { + namespace { bool typeFilter(int type, bool far) @@ -51,8 +59,14 @@ namespace MWRender case ESM::REC_STAT: case ESM::REC_ACTI: case ESM::REC_DOOR: + case ESM::REC_STAT4: + case ESM::REC_DOOR4: + case ESM::REC_TREE4: return true; case ESM::REC_CONT: + case ESM::REC_ACTI4: + case ESM::REC_CONT4: + case ESM::REC_FURN4: return !far; default: @@ -60,7 +74,16 @@ namespace MWRender } } - std::string getModel(int type, ESM::RefId id, const MWWorld::ESMStore& store) + template + std::string_view getEsm4Model(const Record& record) + { + if (MWClass::ESM4Impl::isMarkerModel(record->mModel)) + return {}; + else + return record->mModel; + } + + std::string_view getModel(int type, ESM::RefId id, const MWWorld::ESMStore& store) { switch (type) { @@ -72,6 +95,18 @@ namespace MWRender return store.get().searchStatic(id)->mModel; case ESM::REC_CONT: return store.get().searchStatic(id)->mModel; + case ESM::REC_STAT4: + return getEsm4Model(store.get().searchStatic(id)); + case ESM::REC_DOOR4: + return getEsm4Model(store.get().searchStatic(id)); + case ESM::REC_TREE4: + return getEsm4Model(store.get().searchStatic(id)); + case ESM::REC_ACTI4: + return getEsm4Model(store.get().searchStatic(id)); + case ESM::REC_CONT4: + return getEsm4Model(store.get().searchStatic(id)); + case ESM::REC_FURN4: + return getEsm4Model(store.get().searchStatic(id)); default: return {}; } @@ -494,6 +529,17 @@ namespace MWRender }; } + PagedCellRef makePagedCellRef(const ESM4::Reference& value) + { + return PagedCellRef{ + .mRefId = value.mBaseObj, + .mRefNum = value.mId, + .mPosition = value.mPos.asVec3(), + .mRotation = value.mPos.asRotationVec3(), + .mScale = value.mScale, + }; + } + std::map collectESM3References( float size, const osg::Vec2i& startCell, const MWWorld::ESMStore& store) { @@ -561,6 +607,45 @@ namespace MWRender } return refs; } + + std::map collectESM4References( + float size, const osg::Vec2i& startCell, ESM::RefId worldspace) + { + std::map refs; + const auto& store = MWBase::Environment::get().getWorld()->getStore(); + for (int cellX = startCell.x(); cellX < startCell.x() + size; ++cellX) + { + for (int cellY = startCell.y(); cellY < startCell.y() + size; ++cellY) + { + const ESM4::Cell* cell + = store.get().searchExterior(ESM::ExteriorCellLocation(cellX, cellY, worldspace)); + if (!cell) + continue; + for (const ESM4::Reference* ref4 : store.get().getByCell(cell->mId)) + { + if (ref4->mFlags & ESM4::Rec_Disabled) + continue; + int type = store.findStatic(ref4->mBaseObj); + if (!typeFilter(type, size >= 2)) + continue; + if (!ref4->mEsp.parent.isZeroOrUnset()) + { + const ESM4::Reference* parentRef + = store.get().searchStatic(ref4->mEsp.parent); + if (parentRef) + { + bool parentDisabled = parentRef->mFlags & ESM4::Rec_Disabled; + bool inversed = ref4->mEsp.flags & ESM4::EnableParent::Flag_Inversed; + if (parentDisabled != inversed) + continue; + } + } + refs.insert_or_assign(ref4->mId, makePagedCellRef(*ref4)); + } + } + } + return refs; + } } osg::ref_ptr ObjectPaging::createChunk(float size, const osg::Vec2f& center, bool activeGrid, @@ -578,7 +663,7 @@ namespace MWRender } else { - // TODO + refs = collectESM4References(size, startCell, mWorldspace); } if (activeGrid && !refs.empty()) @@ -648,12 +733,12 @@ namespace MWRender continue; const int type = store.findStatic(ref.mRefId); - VFS::Path::Normalized model = getModel(type, ref.mRefId, store); + VFS::Path::Normalized model(getModel(type, ref.mRefId, store)); if (model.empty()) continue; model = Misc::ResourceHelpers::correctMeshPath(model); - if (activeGrid && type != ESM::REC_STAT) + if (activeGrid && type != ESM::REC_STAT && type != ESM::REC_STAT4) { model = Misc::ResourceHelpers::correctActorModelPath(model, mSceneManager->getVFS()); if (Misc::getFileExtension(model) == "nif") diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index 478bdb5bb8..0c9a13bc47 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include "../mwbase/environment.hpp" @@ -511,7 +512,7 @@ namespace MWWorld if (cellVariant.isExterior()) { - if (const auto heightField = mPhysics->getHeightField(cellX, cellY)) + if (mPhysics->getHeightField(cellX, cellY) != nullptr) mNavigator.addWater( osg::Vec2i(cellX, cellY), ESM::Land::REAL_SIZE, waterLevel, navigatorUpdateGuard); } @@ -645,8 +646,11 @@ namespace MWWorld mHalfGridSize = halfGridSize; mCurrentGridCenter = osg::Vec2i(playerCellX, playerCellY); osg::Vec4i newGrid = gridCenterToBounds(mCurrentGridCenter); - mRendering.setActiveGrid(newGrid); + + // NOTE: setActiveGrid must be after enableTerrain, otherwise we set the grid in the old exterior worldspace mRendering.enableTerrain(true, playerCellIndex.mWorldspace); + mRendering.setActiveGrid(newGrid); + mPreloader->setTerrain(mRendering.getTerrain()); if (mRendering.pagingUnlockCache()) mPreloader->abortTerrainPreloadExcept(nullptr); @@ -1292,6 +1296,9 @@ namespace MWWorld void Scene::preloadTerrain(const osg::Vec3f& pos, ESM::RefId worldspace, bool sync) { + if (mRendering.getTerrain()->getWorldspace() != worldspace) + throw std::runtime_error("preloadTerrain can only work with the current exterior worldspace"); + ESM::ExteriorCellLocation cellPos = ESM::positionToExteriorCellLocation(pos.x(), pos.y(), worldspace); const PositionCellGrid position{ pos, gridCenterToBounds({ cellPos.mX, cellPos.mY }) }; mPreloader->abortTerrainPreloadExcept(&position); diff --git a/apps/openmw/mwworld/scene.hpp b/apps/openmw/mwworld/scene.hpp index 116e52e535..1fa7779e51 100644 --- a/apps/openmw/mwworld/scene.hpp +++ b/apps/openmw/mwworld/scene.hpp @@ -129,6 +129,9 @@ namespace MWWorld void preloadExteriorGrid(const osg::Vec3f& playerPos, const osg::Vec3f& predictedPos); void preloadFastTravelDestinations( const osg::Vec3f& playerPos, std::vector& exteriorPositions); + void preloadCellWithSurroundings(MWWorld::CellStore& cell); + void preloadCell(MWWorld::CellStore& cell); + void preloadTerrain(const osg::Vec3f& pos, ESM::RefId worldspace, bool sync = false); osg::Vec4i gridCenterToBounds(const osg::Vec2i& centerCell) const; osg::Vec2i getNewGridCenter(const osg::Vec3f& pos, const osg::Vec2i* currentGridCenter = nullptr) const; @@ -143,9 +146,6 @@ namespace MWWorld ~Scene(); - void preloadCellWithSurroundings(MWWorld::CellStore& cell); - void preloadCell(MWWorld::CellStore& cell); - void preloadTerrain(const osg::Vec3f& pos, ESM::RefId worldspace, bool sync = false); void reloadTerrain(); void playerMoved(const osg::Vec3f& pos); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index f608b8c781..97788669d5 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -517,13 +517,6 @@ namespace MWWorld mStore.checkPlayer(); mPlayer->readRecord(reader, type); - if (getPlayerPtr().isInCell()) - { - if (getPlayerPtr().getCell()->isExterior()) - mWorldScene->preloadTerrain(getPlayerPtr().getRefData().getPosition().asVec3(), - getPlayerPtr().getCell()->getCell()->getWorldSpace()); - mWorldScene->preloadCellWithSurroundings(*getPlayerPtr().getCell()); - } break; case ESM::REC_CSTA: // We need to rebuild the ESMStore index in order to be able to lookup dynamic records while loading the