diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index 51a6d9e2ba..bf4d89d7e4 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -1,6 +1,11 @@ #include "Gamelist.h" +#include #include +#include +#include +#include +#include #include "utils/FileSystemUtil.h" #include "FileData.h" @@ -9,6 +14,11 @@ #include "Settings.h" #include "SystemData.h" #include +#include + +// Async gamelist write infrastructure +static std::mutex sGamelistWriteMutex; +static std::vector> sGamelistPendingWrites; FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType type) { @@ -292,6 +302,7 @@ void updateGamelist(SystemData* system) if(!pathNode) { LOG(LogError) << "<" << tag << "> node contains no child!"; + fileNode = nextNode; continue; } @@ -325,22 +336,70 @@ void updateGamelist(SystemData* system) // now write the file if (numUpdated > 0) { - const auto startTs = std::chrono::system_clock::now(); - //make sure the folders leading up to this path exist (or the write will fail) std::string xmlWritePath(system->getGamelistPath(true)); Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(xmlWritePath)); LOG(LogInfo) << "Added/Updated " << numUpdated << " entities in '" << xmlReadPath << "'"; - if (!doc.save_file(xmlWritePath.c_str())) { - LOG(LogError) << "Error saving gamelist.xml to \"" << xmlWritePath << "\" (for system " << system->getName() << ")!"; + if (!Settings::getInstance()->getBool("AsyncFileIO")) + { + const auto startTs = std::chrono::system_clock::now(); + if (!doc.save_file(xmlWritePath.c_str())) + LOG(LogError) << "Error saving gamelist.xml to \"" << xmlWritePath << "\" (for system " << system->getName() << ")!"; + const auto endTs = std::chrono::system_clock::now(); + LOG(LogInfo) << "Saved gamelist.xml for system \"" << system->getName() << "\" in " << std::chrono::duration_cast(endTs - startTs).count() << " ms"; } + else + { + // Serialize the XML document to a string on the main thread, + // then write the string to file on a background thread to + // avoid blocking the UI on slow NAS I/O. + std::stringstream ss; + doc.save(ss); + std::string xmlContent = ss.str(); + std::string sysName = system->getName(); - const auto endTs = std::chrono::system_clock::now(); - LOG(LogInfo) << "Saved gamelist.xml for system \"" << system->getName() << "\" in " << std::chrono::duration_cast(endTs - startTs).count() << " ms"; + { + // Clean up finished futures + std::lock_guard lock(sGamelistWriteMutex); + sGamelistPendingWrites.erase( + std::remove_if(sGamelistPendingWrites.begin(), sGamelistPendingWrites.end(), + [](std::future& f) { + return f.wait_for(std::chrono::seconds(0)) == std::future_status::ready; + }), + sGamelistPendingWrites.end()); + + sGamelistPendingWrites.push_back(std::async(std::launch::async, + [xmlContent, xmlWritePath, sysName]() { + const auto startTs = std::chrono::system_clock::now(); + + std::ofstream outFile(xmlWritePath, std::ios::out | std::ios::trunc); + if (outFile.is_open()) { + outFile << xmlContent; + outFile.close(); + } else { + LOG(LogError) << "Error saving gamelist.xml to \"" << xmlWritePath << "\" (for system " << sysName << ")!"; + } + + const auto endTs = std::chrono::system_clock::now(); + LOG(LogInfo) << "Saved gamelist.xml for system \"" << sysName << "\" in " << std::chrono::duration_cast(endTs - startTs).count() << " ms"; + })); + } + } } }else{ LOG(LogError) << "Found no root folder for system \"" << system->getName() << "\"!"; } } + +void waitForGamelistWrites() +{ + std::lock_guard lock(sGamelistWriteMutex); + for (auto& f : sGamelistPendingWrites) + { + if (f.valid()) + f.wait(); + } + sGamelistPendingWrites.clear(); +} diff --git a/es-app/src/Gamelist.h b/es-app/src/Gamelist.h index d9502a196d..377fe4b1b7 100644 --- a/es-app/src/Gamelist.h +++ b/es-app/src/Gamelist.h @@ -10,4 +10,8 @@ void parseGamelist(SystemData* system); // Writes currently loaded metadata for a SystemData to gamelist.xml. void updateGamelist(SystemData* system); +// Blocks until all pending async gamelist writes have completed. +// Must be called before process exit to avoid data loss. +void waitForGamelistWrites(); + #endif // ES_APP_GAME_LIST_H diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index b3150db4a5..442f18462c 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -451,6 +451,11 @@ void GuiMenu::openOtherSettings() s->addWithLabel("PARSE GAMESLISTS ONLY", parse_gamelists); s->addSaveFunc([parse_gamelists] { Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); }); + auto async_file_io = std::make_shared(mWindow); + async_file_io->setState(Settings::getInstance()->getBool("AsyncFileIO")); + s->addWithLabel("ASYNC FILE IO", async_file_io); + s->addSaveFunc([async_file_io] { Settings::getInstance()->setBool("AsyncFileIO", async_file_io->getState()); }); + auto local_art = std::make_shared(mWindow); local_art->setState(Settings::getInstance()->getBool("LocalArt")); s->addWithLabel("SEARCH FOR LOCAL ART", local_art); diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index eb0b92cacf..a5abf7fede 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -8,6 +8,7 @@ #include "views/ViewController.h" #include "CollectionSystemManager.h" #include "EmulationStation.h" +#include "Gamelist.h" #include "InputManager.h" #include "Log.h" #include "MameNames.h" @@ -17,6 +18,7 @@ #include "Settings.h" #include "SystemData.h" #include "SystemScreenSaver.h" +#include "components/VideoVlcComponent.h" #include #include #include @@ -482,8 +484,14 @@ int main(int argc, char* argv[]) InputManager::getInstance()->deinit(); window.deinit(); + // Join the VLC cleanup worker and release the VLC instance. Must happen after + // window.deinit() so all VideoVlcComponents are destroyed and their final + // stopVideo() cleanup tasks are already posted to the queue. + VideoVlcComponent::deinit(); + MameNames::deinit(); CollectionSystemManager::deinit(); + waitForGamelistWrites(); SystemData::deleteSystems(); // call this ONLY when linking with FreeImage as a static library diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index ecac95c3bd..da55f7efa8 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -221,9 +221,9 @@ void DetailedGameListView::updateInfoPanel() //mDescription.setText(""); fadingOut = true; }else{ - mThumbnail.setImage(file->getThumbnailPath()); - mMarquee.setImage(file->getMarqueePath()); - mImage.setImage(file->getImagePath()); + mThumbnail.setImageAsync(file->getThumbnailPath()); + mImage.setImageAsync(file->getImagePath()); + mMarquee.setImageAsync(file->getMarqueePath()); mDescription.setText(file->metadata.get("desc")); mDescContainer.reset(); diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index c013fa3284..10aa7b7434 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -323,9 +323,9 @@ void GridGameListView::updateInfoPanel() } mVideoPlaying = true; - mVideo->setImage(file->getThumbnailPath()); - mMarquee.setImage(file->getMarqueePath()); - mImage.setImage(file->getImagePath()); + mVideo->setImageAsync(file->getThumbnailPath()); + mMarquee.setImageAsync(file->getMarqueePath()); + mImage.setImageAsync(file->getImagePath()); mDescription.setText(file->metadata.get("desc")); mDescContainer.reset(); diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index e26715c774..632b2de0a0 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -265,10 +265,10 @@ void VideoGameListView::updateInfoPanel() } mVideoPlaying = true; - mVideo->setImage(file->getThumbnailPath()); - mThumbnail.setImage(file->getThumbnailPath()); - mMarquee.setImage(file->getMarqueePath()); - mImage.setImage(file->getImagePath()); + mVideo->setImageAsync(file->getThumbnailPath()); + mThumbnail.setImageAsync(file->getThumbnailPath()); + mMarquee.setImageAsync(file->getMarqueePath()); + mImage.setImageAsync(file->getImagePath()); mDescription.setText(file->metadata.get("desc")); mDescContainer.reset(); diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index aeb1057e33..436c95a7e7 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -58,6 +58,7 @@ void Settings::setDefaults() mBoolMap["BackgroundJoystickInput"] = false; mBoolMap["ParseGamelistOnly"] = false; + mBoolMap["AsyncFileIO"] = false; mBoolMap["ShowHiddenFiles"] = false; mBoolMap["DrawFramerate"] = false; mBoolMap["ShowExit"] = true; diff --git a/es-core/src/components/IList.h b/es-core/src/components/IList.h index 67f0ee22f9..4458ed7fc0 100644 --- a/es-core/src/components/IList.h +++ b/es-core/src/components/IList.h @@ -240,8 +240,13 @@ class IList : public GuiComponent if(mScrollVelocity == 0 || size() < 2) return; - mScrollCursorAccumulator += deltaTime; - mScrollTierAccumulator += deltaTime; + // Cap the delta time used for scrolling to prevent multiple scroll jumps + // after a long-blocking frame (e.g., slow NAS I/O) + const int maxScrollDelta = mTierList.tiers[mScrollTier].scrollDelay; + int scrollDelta = (deltaTime > maxScrollDelta) ? maxScrollDelta : deltaTime; + + mScrollCursorAccumulator += scrollDelta; + mScrollTierAccumulator += scrollDelta; // we delay scrolling until after scroll tier has updated so isScrolling() returns accurately during onCursorChanged callbacks // we don't just do scroll tier first because it would not catch the scrollDelay == tier length case diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 823ceb570d..dd6ca48ad7 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -4,6 +4,7 @@ #include "Log.h" #include "Settings.h" #include "ThemeData.h" +#include Vector2i ImageComponent::getTextureSize() const { @@ -21,7 +22,8 @@ Vector2f ImageComponent::getSize() const ImageComponent::ImageComponent(Window* window, bool forceLoad, bool dynamic) : GuiComponent(window), mTargetIsMax(false), mTargetIsMin(false), mFlipX(false), mFlipY(false), mTargetSize(0, 0), mColorShift(0xFFFFFFFF), mColorShiftEnd(0xFFFFFFFF), mColorGradientHorizontal(true), mForceLoad(forceLoad), mDynamic(dynamic), - mFadeOpacity(0), mFading(false), mRotateByTargetSize(false), mTopLeftCrop(0.0f, 0.0f), mBottomRightCrop(1.0f, 1.0f) + mFadeOpacity(0), mFading(false), mRotateByTargetSize(false), mTopLeftCrop(0.0f, 0.0f), mBottomRightCrop(1.0f, 1.0f), + mAsyncPending(false) { updateColors(); } @@ -132,9 +134,13 @@ void ImageComponent::setDefaultImage(std::string path) void ImageComponent::setImage(std::string path, bool tile) { - if(path.empty() || !ResourceManager::getInstance()->fileExists(path)) + mAsyncPending = false; + + // Skip fileExists() — it calls stat64 which blocks on NAS paths. + // TextureData::load() handles missing files gracefully (returns empty texture). + if(path.empty()) { - if(mDefaultPath.empty() || !ResourceManager::getInstance()->fileExists(mDefaultPath)) + if(mDefaultPath.empty()) mTexture.reset(); else mTexture = TextureResource::get(mDefaultPath, tile, mForceLoad, mDynamic); @@ -158,9 +164,71 @@ void ImageComponent::setImage(const char* path, size_t length, bool tile) void ImageComponent::setImage(const std::shared_ptr& texture) { mTexture = texture; + mAsyncPending = false; resize(); } +void ImageComponent::setImageAsync(std::string path, bool tile) +{ + if (!Settings::getInstance()->getBool("AsyncFileIO")) + { + setImage(path, tile); + return; + } + + mAsyncPending = false; + mTexturePath = path; + + // Skip the fileExists() check used by setImage() — it calls stat64 which blocks + // on NAS. Instead, just hand the path to TextureResource and let the background + // thread's load() handle missing files gracefully. + if(path.empty()) + { + if(mDefaultPath.empty()) + mTexture.reset(); + else + mTexture = TextureResource::get(mDefaultPath, tile, mForceLoad, mDynamic, false); + } else { + mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic, false); + } + + if(mTexture) + { + // Check if the texture is already loaded (e.g. cache hit) + if(mTexture->updateTextureSize()) + { + LOG(LogDebug) << "setImageAsync: immediate load for " << path; + resize(); + } + else + { + // Texture is loading in background - resize() will be called from update() when ready + LOG(LogDebug) << "setImageAsync: queued async for " << path; + mAsyncPending = true; + mAsyncStartTime = SDL_GetTicks(); + } + } + else + { + LOG(LogDebug) << "setImageAsync: no texture for " << path; + } +} + +void ImageComponent::update(int deltaTime) +{ + GuiComponent::update(deltaTime); + + if(mAsyncPending && mTexture) + { + if(mTexture->updateTextureSize()) + { + LOG(LogDebug) << "ImageComponent::update: async load complete for " << mTexturePath << " size=" << mTexture->getSize().x() << "x" << mTexture->getSize().y() << " time=" << (SDL_GetTicks() - mAsyncStartTime) << "ms"; + mAsyncPending = false; + resize(); + } + } +} + void ImageComponent::setResize(float width, float height) { mTargetSize = Vector2f(width, height); @@ -325,7 +393,7 @@ void ImageComponent::render(const Transform4x4f& parentTrans) Transform4x4f trans = parentTrans * getTransform(); Renderer::setMatrix(trans); - if(mTexture && mOpacity > 0) + if(mTexture && mOpacity > 0 && !mAsyncPending) { if(Settings::getInstance()->getBool("DebugImage")) { Vector2f targetSizePos = (mTargetSize - mSize) * mOrigin * -1; @@ -346,6 +414,21 @@ void ImageComponent::render(const Transform4x4f& parentTrans) mTexture.reset(); } } + else if(mTexture && mAsyncPending) + { + // Resolve mAsyncPending for failed loads even when update() isn't being called + // (e.g. game list rendered in background during system carousel transition). + if(mTexture->updateTextureSize()) + { + mAsyncPending = false; + } + else + { + static int skipCount = 0; + if(++skipCount % 60 == 1) // log every ~1 second at 60fps + LOG(LogDebug) << "render: skipping due to mAsyncPending for " << mTexturePath << " mSize=" << mSize.x() << "x" << mSize.y() << " opacity=" << (int)mOpacity; + } + } GuiComponent::renderChildren(trans); } diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index ef6ab47c42..5bbc12d097 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -23,9 +23,14 @@ class ImageComponent : public GuiComponent //Use an already existing texture. void setImage(const std::shared_ptr& texture); + //Loads the image asynchronously in a background thread. The image will fade in when ready. + void setImageAsync(std::string path, bool tile = false); + void onSizeChanged() override; void setOpacity(unsigned char opacity) override; + void update(int deltaTime) override; + // Resize the image to fit this size. If one axis is zero, scale that axis to maintain aspect ratio. // If both are non-zero, potentially break the aspect ratio. If both are zero, no resizing. // Can be set before or after an image is loaded. @@ -77,6 +82,7 @@ class ImageComponent : public GuiComponent virtual std::vector getHelpPrompts() override; std::shared_ptr getTexture() { return mTexture; }; + bool isAsyncPending() const { return mAsyncPending; }; private: Vector2f mTargetSize; @@ -97,6 +103,7 @@ class ImageComponent : public GuiComponent bool mColorGradientHorizontal; std::string mDefaultPath; + std::string mTexturePath; std::shared_ptr mTexture; unsigned char mFadeOpacity; @@ -104,6 +111,8 @@ class ImageComponent : public GuiComponent bool mForceLoad; bool mDynamic; bool mRotateByTargetSize; + bool mAsyncPending; + int mAsyncStartTime; Vector2f mTopLeftCrop; Vector2f mBottomRightCrop; diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index 98ea4d882a..b5a35ff72e 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -1,6 +1,7 @@ #include "components/VideoComponent.h" #include "resources/ResourceManager.h" +#include "Settings.h" #include "utils/FileSystemUtil.h" #include "PowerSaver.h" #include "ThemeData.h" @@ -114,14 +115,9 @@ bool VideoComponent::setVideo(std::string path) // Store the path mVideoPath = fullPath; - // If the file exists then set the new video - if (!fullPath.empty() && ResourceManager::getInstance()->fileExists(fullPath)) - { - // Return true to show that we are going to attempt to play a video - return true; - } - // Return false to show that no video will be displayed - return false; + // Return true if there's a path to attempt; missing files are handled + // gracefully by VLC, avoiding a blocking stat() call on NAS paths. + return !fullPath.empty(); } void VideoComponent::setImage(std::string path) @@ -135,6 +131,21 @@ void VideoComponent::setImage(std::string path) mStaticImagePath = path; } +void VideoComponent::setImageAsync(std::string path) +{ + // Check that the image has changed + if (path == mStaticImagePath) + return; + + if (!Settings::getInstance()->getBool("AsyncFileIO")) + mStaticImage.setImage(path); + else + mStaticImage.setImageAsync(path); + + mFadeIn = 0.0f; + mStaticImagePath = path; +} + void VideoComponent::setDefaultVideo() { setVideo(mConfig.defaultVideoPath); @@ -221,6 +232,13 @@ void VideoComponent::handleStartDelay() // Only play if any delay has timed out if (mStartDelayed) { + // If the snapshot image is still loading, keep pushing the deadline forward + // so the delay doesn't expire before the user has had a chance to see it. + if (mConfig.showSnapshotDelay && mStaticImage.isAsyncPending()) + { + mStartTime = SDL_GetTicks() + mConfig.startDelay; + return; + } if (mStartTime > SDL_GetTicks()) { // Timeout not yet completed @@ -228,8 +246,6 @@ void VideoComponent::handleStartDelay() } // Completed mStartDelayed = false; - // Clear the playing flag so startVideo works - mIsPlaying = false; startVideo(); } } @@ -240,8 +256,8 @@ void VideoComponent::handleLooping() void VideoComponent::startVideoWithDelay() { - // If not playing then either start the video or initiate the delay - if (!mIsPlaying) + // If not playing and not already preparing to play, start the process + if (!mIsPlaying && mPlayingVideoPath.empty()) { // Set the video that we are going to be playing so we don't attempt to restart it mPlayingVideoPath = mVideoPath; @@ -259,7 +275,6 @@ void VideoComponent::startVideoWithDelay() mFadeIn = 0.0f; mStartTime = SDL_GetTicks() + mConfig.startDelay; } - mIsPlaying = true; } } @@ -267,6 +282,10 @@ void VideoComponent::update(int deltaTime) { manageState(); + // mStaticImage is not a child, so we must pump its update manually + // to let its async-load polling (mAsyncPending) resolve. + mStaticImage.update(deltaTime); + // If the video start is delayed and there is less than the fade time then set the image fade // accordingly if (mStartDelayed) @@ -298,8 +317,8 @@ void VideoComponent::manageState() // is not active and the component is visible bool show = mShowing && !mScreensaverActive && !mDisable && mVisible; - // See if we're already playing - if (mIsPlaying) + // See if we're already playing (or mid-parse preparing to play) + if (mIsPlaying || mPlayingVideoPath.length() > 0) { // If we are not on display then stop the video from playing if (!show) @@ -317,7 +336,7 @@ void VideoComponent::manageState() } } // Need to recheck variable rather than 'else' because it may be modified above - if (!mIsPlaying) + if (!mIsPlaying && mPlayingVideoPath.empty()) { // If we are on display then see if we should start the video if (show && !mVideoPath.empty()) diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h index 1c574a1fae..cd2bf49a05 100644 --- a/es-core/src/components/VideoComponent.h +++ b/es-core/src/components/VideoComponent.h @@ -31,6 +31,8 @@ class VideoComponent : public GuiComponent bool setVideo(std::string path); // Loads a static image that is displayed if the video cannot be played void setImage(std::string path); + // Loads a static image asynchronously in a background thread + void setImageAsync(std::string path); // Configures the component to show the default video void setDefaultVideo(); diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index 4ef7500d05..cb00d55180 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -17,6 +17,66 @@ typedef SSIZE_T ssize_t; libvlc_instance_t* VideoVlcComponent::mVLC = NULL; +// Persistent worker thread statics for non-blocking VLC cleanup +std::thread VideoVlcComponent::sCleanupThread; +std::mutex VideoVlcComponent::sCleanupMutex; +std::condition_variable VideoVlcComponent::sCleanupCond; +std::deque> VideoVlcComponent::sCleanupQueue; +bool VideoVlcComponent::sCleanupRunning = false; +bool VideoVlcComponent::sCleanupExit = false; + +void VideoVlcComponent::cleanupWorker() +{ + while (true) + { + std::function task; + { + std::unique_lock lock(sCleanupMutex); + sCleanupCond.wait(lock, [] { return !sCleanupQueue.empty() || sCleanupExit; }); + if (sCleanupQueue.empty()) + break; // exit flag set and no remaining work + task = std::move(sCleanupQueue.front()); + sCleanupQueue.pop_front(); + } + task(); + } +} + +void VideoVlcComponent::postCleanupTask(std::function task) +{ + { + std::lock_guard lock(sCleanupMutex); + // Lazily start the persistent worker thread on first use + if (!sCleanupRunning) + { + sCleanupRunning = true; + sCleanupThread = std::thread(cleanupWorker); + // Thread is kept joinable — deinit() will join it on shutdown. + } + sCleanupQueue.push_back(std::move(task)); + } + sCleanupCond.notify_one(); +} + +void VideoVlcComponent::deinit() +{ + // Signal the worker thread to exit once the queue is drained + { + std::lock_guard lock(sCleanupMutex); + sCleanupExit = true; + } + sCleanupCond.notify_one(); + + if (sCleanupRunning && sCleanupThread.joinable()) + sCleanupThread.join(); + + if (mVLC) + { + libvlc_release(mVLC); + mVLC = nullptr; + } +} + // VLC prepares to render a video frame. static void *lock(void *data, void **p_pixels) { struct VideoContext *c = (struct VideoContext *)data; @@ -40,9 +100,10 @@ static void display(void* /*data*/, void* /*id*/) { VideoVlcComponent::VideoVlcComponent(Window* window, std::string subtitles) : VideoComponent(window), - mMediaPlayer(nullptr) + mMediaPlayer(nullptr), + mMediaParsing(false) { - memset(&mContext, 0, sizeof(mContext)); + mContext = nullptr; // Get an empty texture for rendering the video mTexture = TextureResource::get(""); @@ -137,12 +198,15 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) if (!isVisible()) return; + // Poll for async media parsing completion each frame + handleParsing(); + VideoComponent::render(parentTrans); Transform4x4f trans = parentTrans * getTransform(); GuiComponent::renderChildren(trans); Renderer::setMatrix(trans); - if (mIsPlaying && mContext.valid) + if (mIsPlaying && mContext && mContext->valid) { const unsigned int fadeIn = (unsigned int)(Math::clamp(0.0f, mFadeIn, 1.0f) * 255.0f); const unsigned int color = Renderer::convertColor((fadeIn << 24) | (fadeIn << 16) | (fadeIn << 8) | 255); @@ -158,7 +222,7 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) vertices[i].pos.round(); // Build a texture for the video frame - mTexture->initFromPixels((unsigned char*)mContext.surface->pixels, mContext.surface->w, mContext.surface->h); + mTexture->initFromPixels((unsigned char*)mContext->surface->pixels, mContext->surface->w, mContext->surface->h); mTexture->bind(); // Render it @@ -172,23 +236,25 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) void VideoVlcComponent::setupContext() { - if (!mContext.valid) + if (!mContext) { // Create an RGBA surface to render the video into - mContext.surface = SDL_CreateRGBSurface(SDL_SWSURFACE, (int)mVideoWidth, (int)mVideoHeight, 32, 0xff000000, 0x00ff0000, 0x0000ff00, 0x000000ff); - mContext.mutex = SDL_CreateMutex(); - mContext.valid = true; + mContext = new VideoContext(); + mContext->surface = SDL_CreateRGBSurface(SDL_SWSURFACE, (int)mVideoWidth, (int)mVideoHeight, 32, 0xff000000, 0x00ff0000, 0x0000ff00, 0x000000ff); + mContext->mutex = SDL_CreateMutex(); + mContext->valid = true; resize(); } } void VideoVlcComponent::freeContext() { - if (mContext.valid) + if (mContext) { - SDL_FreeSurface(mContext.surface); - SDL_DestroyMutex(mContext.mutex); - mContext.valid = false; + SDL_FreeSurface(mContext->surface); + SDL_DestroyMutex(mContext->mutex); + delete mContext; + mContext = nullptr; } } @@ -233,7 +299,7 @@ void VideoVlcComponent::handleLooping() void VideoVlcComponent::startVideo() { - if (!mIsPlaying) { + if (!mIsPlaying && !mMediaParsing) { mVideoWidth = 0; mVideoHeight = 0; @@ -252,76 +318,92 @@ void VideoVlcComponent::startVideo() mMedia = libvlc_media_new_path(mVLC, path.c_str()); if (mMedia) { - unsigned track_count; - // Get the media metadata so we can find the aspect ratio + // Start async parse — we will poll for completion in handleParsing() libvlc_media_parse_with_options(mMedia, libvlc_media_fetch_local, -1); - while (libvlc_media_get_parsed_status(mMedia) == 0) - ; - libvlc_media_track_t** tracks; - track_count = libvlc_media_tracks_get(mMedia, &tracks); - for (unsigned track = 0; track < track_count; ++track) - { - if (tracks[track]->i_type == libvlc_track_video) - { - mVideoWidth = tracks[track]->video->i_width; - mVideoHeight = tracks[track]->video->i_height; - break; - } - } - libvlc_media_tracks_release(tracks, track_count); + mMediaParsing = true; + } + } + } +} + +void VideoVlcComponent::handleParsing() +{ + if (!mMediaParsing || !mMedia) + return; + + // Poll — not yet parsed, come back next frame + if (libvlc_media_get_parsed_status(mMedia) == 0) + return; + + mMediaParsing = false; + onMediaParsed(); +} + +void VideoVlcComponent::onMediaParsed() +{ + unsigned track_count; + libvlc_media_track_t** tracks; + track_count = libvlc_media_tracks_get(mMedia, &tracks); + for (unsigned track = 0; track < track_count; ++track) + { + if (tracks[track]->i_type == libvlc_track_video) + { + mVideoWidth = tracks[track]->video->i_width; + mVideoHeight = tracks[track]->video->i_height; + break; + } + } + libvlc_media_tracks_release(tracks, track_count); - // Make sure we found a valid video track - if ((mVideoWidth > 0) && (mVideoHeight > 0)) + // Make sure we found a valid video track + if ((mVideoWidth > 0) && (mVideoHeight > 0)) + { + if (mScreensaverMode) + { + std::string resolution = Settings::getInstance()->getString("VlcScreenSaverResolution"); + if(resolution != "original") { + float scale = 1; + if (resolution == "low") + // 25% of screen resolution + scale = 0.25; + if (resolution == "medium") + // 50% of screen resolution + scale = 0.5; + if (resolution == "high") + // 75% of screen resolution + scale = 0.75; + + Vector2f resizeScale((Renderer::getScreenWidth() / (float)mVideoWidth) * scale, (Renderer::getScreenHeight() / (float)mVideoHeight) * scale); + + if(resizeScale.x() < resizeScale.y()) { - if (mScreensaverMode) - { - std::string resolution = Settings::getInstance()->getString("VlcScreenSaverResolution"); - if(resolution != "original") { - float scale = 1; - if (resolution == "low") - // 25% of screen resolution - scale = 0.25; - if (resolution == "medium") - // 50% of screen resolution - scale = 0.5; - if (resolution == "high") - // 75% of screen resolution - scale = 0.75; - - Vector2f resizeScale((Renderer::getScreenWidth() / (float)mVideoWidth) * scale, (Renderer::getScreenHeight() / (float)mVideoHeight) * scale); - - if(resizeScale.x() < resizeScale.y()) - { - mVideoWidth = (unsigned int) (mVideoWidth * resizeScale.x()); - mVideoHeight = (unsigned int) (mVideoHeight * resizeScale.x()); - }else{ - mVideoWidth = (unsigned int) (mVideoWidth * resizeScale.y()); - mVideoHeight = (unsigned int) (mVideoHeight * resizeScale.y()); - } - } - } - else - { - remove(getTitlePath().c_str()); - } - PowerSaver::pause(); - setupContext(); - - // Setup the media player - mMediaPlayer = libvlc_media_player_new_from_media(mMedia); - - setMuteMode(); - - libvlc_media_player_play(mMediaPlayer); - libvlc_video_set_callbacks(mMediaPlayer, lock, unlock, display, (void*)&mContext); - libvlc_video_set_format(mMediaPlayer, "RGBA", (int)mVideoWidth, (int)mVideoHeight, (int)mVideoWidth * 4); - - // Update the playing state - mIsPlaying = true; - mFadeIn = 0.0f; + mVideoWidth = (unsigned int) (mVideoWidth * resizeScale.x()); + mVideoHeight = (unsigned int) (mVideoHeight * resizeScale.x()); + }else{ + mVideoWidth = (unsigned int) (mVideoWidth * resizeScale.y()); + mVideoHeight = (unsigned int) (mVideoHeight * resizeScale.y()); } } } + else + { + remove(getTitlePath().c_str()); + } + PowerSaver::pause(); + setupContext(); + + // Setup the media player + mMediaPlayer = libvlc_media_player_new_from_media(mMedia); + + setMuteMode(); + + libvlc_media_player_play(mMediaPlayer); + libvlc_video_set_callbacks(mMediaPlayer, lock, unlock, display, (void*)mContext); + libvlc_video_set_format(mMediaPlayer, "RGBA", (int)mVideoWidth, (int)mVideoHeight, (int)mVideoWidth * 4); + + // Update the playing state + mIsPlaying = true; + mFadeIn = 0.0f; } } @@ -329,15 +411,45 @@ void VideoVlcComponent::stopVideo() { mIsPlaying = false; mStartDelayed = false; - // Release the media player so it stops calling back to us + mPlayingVideoPath = ""; + // If we were mid-parse with no player yet, cancel the parse on a background + // thread so the blocking libvlc_media_parse_stop call doesn't stall the UI. + if (mMediaParsing && mMedia) + { + libvlc_media_t* media = mMedia; + mMedia = nullptr; + mMediaParsing = false; + + postCleanupTask([media]() { + libvlc_media_parse_stop(media); + libvlc_media_release(media); + }); + return; + } + mMediaParsing = false; + // Release the media player on a background thread so the blocking + // libvlc_media_player_stop call doesn't freeze the UI. if (mMediaPlayer) { - libvlc_media_player_stop(mMediaPlayer); - libvlc_media_player_release(mMediaPlayer); - libvlc_media_release(mMedia); - mMediaPlayer = NULL; - freeContext(); - PowerSaver::resume(); + libvlc_media_player_t* player = mMediaPlayer; + libvlc_media_t* media = mMedia; + VideoContext* context = mContext; + + mMediaPlayer = nullptr; + mMedia = nullptr; + mContext = nullptr; + + postCleanupTask([player, media, context]() { + libvlc_media_player_stop(player); + libvlc_media_player_release(player); + libvlc_media_release(media); + if (context) { + SDL_FreeSurface(context->surface); + SDL_DestroyMutex(context->mutex); + delete context; + } + PowerSaver::resume(); + }); } } diff --git a/es-core/src/components/VideoVlcComponent.h b/es-core/src/components/VideoVlcComponent.h index b3974faf3a..bf7182db84 100644 --- a/es-core/src/components/VideoVlcComponent.h +++ b/es-core/src/components/VideoVlcComponent.h @@ -3,6 +3,11 @@ #define ES_CORE_COMPONENTS_VIDEO_VLC_COMPONENT_H #include "VideoComponent.h" +#include +#include +#include +#include +#include struct SDL_mutex; struct SDL_Surface; @@ -47,6 +52,10 @@ class VideoVlcComponent : public VideoComponent // Never breaks the aspect ratio. setMaxSize() and setResize() are mutually exclusive. void setMaxSize(float width, float height) override; + // Signal the cleanup worker to exit and wait for it to finish. + // Must be called after all VideoVlcComponent instances are destroyed. + static void deinit(); + private: // Calculates the correct mSize from our resizing information (set by setResize/setMaxSize). // Used internally whenever the resizing parameters or texture change. @@ -62,12 +71,30 @@ class VideoVlcComponent : public VideoComponent void setupContext(); void freeContext(); + // Called each frame to check if async media parsing has completed; + // once done, extracts track info and starts playback. + void handleParsing(); + // Second half of startVideo — runs after parsing finishes. + void onMediaParsed(); + + // Post a cleanup task to the shared background worker thread. + static void postCleanupTask(std::function task); + + static void cleanupWorker(); + private: static libvlc_instance_t* mVLC; + static std::thread sCleanupThread; + static std::mutex sCleanupMutex; + static std::condition_variable sCleanupCond; + static std::deque> sCleanupQueue; + static bool sCleanupRunning; + static bool sCleanupExit; libvlc_media_t* mMedia; libvlc_media_player_t* mMediaPlayer; - VideoContext mContext; + VideoContext* mContext; std::shared_ptr mTexture; + bool mMediaParsing; }; #endif // ES_CORE_COMPONENTS_VIDEO_VLC_COMPONENT_H diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index 33382b6d8d..8b8de9d18c 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -13,7 +13,7 @@ #define DPI 96 TextureData::TextureData(bool tile) : mTile(tile), mTextureID(0), mDataRGBA(nullptr), mScalable(false), - mWidth(0), mHeight(0), mSourceWidth(0.0f), mSourceHeight(0.0f) + mWidth(0), mHeight(0), mSourceWidth(0.0f), mSourceHeight(0.0f), mLoadFailed(false) { } @@ -136,15 +136,22 @@ bool TextureData::load() else retval = initImageFromMemory((const unsigned char*)data.ptr.get(), data.length); } + if (!retval) + { + std::unique_lock lock(mMutex); + mLoadFailed = true; + } return retval; } -bool TextureData::isLoaded() +TextureData::LoadStatus TextureData::loadStatus() { std::unique_lock lock(mMutex); if (mDataRGBA || (mTextureID != 0)) - return true; - return false; + return LoadStatus::LOADED; + if (mLoadFailed) + return LoadStatus::FAILED; + return LoadStatus::LOADING; } bool TextureData::uploadAndBind() diff --git a/es-core/src/resources/TextureData.h b/es-core/src/resources/TextureData.h index 44dc92623c..912bbb0f4b 100644 --- a/es-core/src/resources/TextureData.h +++ b/es-core/src/resources/TextureData.h @@ -21,10 +21,18 @@ class TextureData bool initImageFromMemory(const unsigned char* fileData, size_t length); bool initFromRGBA(const unsigned char* dataRGBA, size_t width, size_t height); + enum class LoadStatus + { + LOADING, // not yet loaded or in progress + LOADED, // pixel data is in RAM or VRAM + FAILED // load() was attempted and permanently failed (e.g. file not found) + }; + // Read the data into memory if necessary bool load(); - bool isLoaded(); + // Returns the current load state under a single mutex acquisition. + LoadStatus loadStatus(); // Upload the texture to VRAM if necessary and bind. Returns true if bound ok or // false if either not loaded @@ -59,6 +67,7 @@ class TextureData float mSourceHeight; bool mScalable; bool mReloadable; + bool mLoadFailed; }; #endif // ES_CORE_RESOURCES_TEXTURE_DATA_H diff --git a/es-core/src/resources/TextureDataManager.cpp b/es-core/src/resources/TextureDataManager.cpp index 8eccdbdd3d..b3dd87cf52 100644 --- a/es-core/src/resources/TextureDataManager.cpp +++ b/es-core/src/resources/TextureDataManager.cpp @@ -39,6 +39,8 @@ void TextureDataManager::remove(const TextureResource* key) auto it = mTextureLookup.find(key); if (it != mTextureLookup.cend()) { + // Cancel any pending async load for this texture + mLoader->remove(*(*it).second); // Remove the list entry mTextures.erase((*it).second); // And the lookup @@ -62,8 +64,10 @@ std::shared_ptr TextureDataManager::get(const TextureResource* key, // Store it back in the lookup mTextureLookup[key] = mTextures.cbegin(); - // Make sure it's loaded or queued for loading - if (enableLoading && !tex->isLoaded()) + // Make sure it's loaded or queued for loading. + // Skip textures whose load previously failed (e.g. file not found) so + // we don't keep re-queuing them every frame they are rendered. + if (enableLoading && tex->loadStatus() == TextureData::LoadStatus::LOADING) load(tex); } return tex; @@ -84,7 +88,12 @@ size_t TextureDataManager::getTotalSize() { size_t total = 0; for (auto tex : mTextures) - total += tex->width() * tex->height() * 4; + { + // Only count textures whose dimensions are known — calling width()/height() + // on an unloaded texture triggers a synchronous load(). + if (tex->loadStatus() == TextureData::LoadStatus::LOADED) + total += tex->width() * tex->height() * 4; + } return total; } @@ -103,8 +112,8 @@ size_t TextureDataManager::getQueueSize() void TextureDataManager::load(std::shared_ptr tex, bool block) { - // See if it's already loaded - if (tex->isLoaded()) + // See if it's already loaded or has permanently failed + if (tex->loadStatus() != TextureData::LoadStatus::LOADING) return; // Not loaded. Make sure there is room size_t max_texture = (size_t)Settings::getInstance()->getInt("MaxVRAM") * 1024 * 1024; @@ -117,12 +126,15 @@ void TextureDataManager::load(std::shared_ptr tex, bool block) { if (size < max_texture) break; - //size -= (*it)->getVRAMUsage(); + // Only evict textures that actually have data in RAM or VRAM. + // Evicting a LOADING texture frees no memory (getVRAMUsage()==0) and cancels + // its pending background load — which then gets immediately re-queued on the + // next bind(), triggering another eviction cycle. Skipping these breaks the + // evict→cancel→re-queue→evict cascade that causes non-stop texture flickering. + if ((*it)->loadStatus() != TextureData::LoadStatus::LOADED) + continue; (*it)->releaseVRAM(); (*it)->releaseRAM(); - // It may be already in the loader queue. In this case it wouldn't have been using - // any VRAM yet but it will be. Remove it from the loader queue - mLoader->remove(*it); size = TextureResource::getTotalMemUsage(); } } @@ -139,12 +151,15 @@ TextureLoader::TextureLoader() : mExit(false) TextureLoader::~TextureLoader() { - // Just abort any waiting texture - mTextureDataQ.clear(); - mTextureDataLookup.clear(); - - // Exit the thread - mExit = true; + // Clear the queue and signal exit atomically under the mutex so there is no + // race with the background thread's condition_variable wait (which holds the + // mutex while sleeping). + { + std::unique_lock lock(mMutex); + mTextureDataQ.clear(); + mTextureDataLookup.clear(); + mExit = true; + } mEvent.notify_one(); mThread->join(); delete mThread; @@ -186,8 +201,8 @@ void TextureLoader::threadProc() void TextureLoader::load(std::shared_ptr textureData) { - // Make sure it's not already loaded - if (!textureData->isLoaded()) + // Make sure it's not already loaded and hasn't permanently failed + if (textureData->loadStatus() == TextureData::LoadStatus::LOADING) { std::unique_lock lock(mMutex); // Remove it from the queue if it is already there @@ -220,12 +235,15 @@ void TextureLoader::remove(std::shared_ptr textureData) size_t TextureLoader::getQueueSize() { // Gets the amount of video memory that will be used once all textures in - // the queue are loaded + // the queue are loaded. Only count textures whose dimensions are already + // known — calling width()/height() on an unloaded texture triggers a + // synchronous load() which blocks the main thread on NAS I/O. size_t mem = 0; std::unique_lock lock(mMutex); for (auto tex : mTextureDataQ) { - mem += tex->width() * tex->height() * 4; + if (tex->loadStatus() == TextureData::LoadStatus::LOADED) + mem += tex->width() * tex->height() * 4; } return mem; } diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index 975e6f70d4..aac6b237b4 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -7,7 +7,7 @@ TextureDataManager TextureResource::sTextureDataManager; std::map< TextureResource::TextureKeyType, std::weak_ptr > TextureResource::sTextureMap; std::set TextureResource::sAllTextures; -TextureResource::TextureResource(const std::string& path, bool tile, bool dynamic) : mTextureData(nullptr), mSize(0.0f, 0.0f), mSourceSize(0.0f, 0.0f), mForceLoad(false) +TextureResource::TextureResource(const std::string& path, bool tile, bool dynamic, bool block) : mTextureData(nullptr), mSize(0.0f, 0.0f), mSourceSize(0.0f, 0.0f), mForceLoad(false) { // Create a texture data object for this texture if (!path.empty()) @@ -19,20 +19,24 @@ TextureResource::TextureResource(const std::string& path, bool tile, bool dynami { data = sTextureDataManager.add(this, tile); data->initFromPath(path); - // Force the texture manager to load it using a blocking load - sTextureDataManager.load(data, true); + // Load the texture - either blocking or async via the background thread + sTextureDataManager.load(data, block); } else { mTextureData = std::shared_ptr(new TextureData(tile)); data = mTextureData; data->initFromPath(path); - // Load it so we can read the width/height + // Non-dynamic textures are always loaded immediately (blocking) data->load(); } - mSize = Vector2i((int)data->width(), (int)data->height()); - mSourceSize = Vector2f(data->sourceWidth(), data->sourceHeight()); + if (block || !dynamic) + { + mSize = Vector2i((int)data->width(), (int)data->height()); + mSourceSize = Vector2f(data->sourceWidth(), data->sourceHeight()); + } + // When block=false && dynamic, sizes stay at (0,0) until updateTextureSize() is called } else { @@ -100,11 +104,16 @@ bool TextureResource::bind() } } -std::shared_ptr TextureResource::get(const std::string& path, bool tile, bool forceLoad, bool dynamic) +std::shared_ptr TextureResource::get(const std::string& path, bool tile, bool forceLoad, bool dynamic, bool block) { std::shared_ptr& rm = ResourceManager::getInstance(); - const std::string canonicalPath = Utils::FileSystem::getCanonicalPath(path); + // Avoid getCanonicalPath here — it calls stat64/lstat64 on every path component + // to resolve symlinks, which blocks the main thread for tens of ms per call on NAS. + // Use getGenericPath (pure string normalization) instead. Built-in resource paths + // starting with ":/" are passed through as-is (matching getCanonicalPath behavior). + const std::string canonicalPath = (path.size() >= 2 && path[0] == ':' && path[1] == '/') + ? path : Utils::FileSystem::getGenericPath(path); if(canonicalPath.empty()) { std::shared_ptr tex(new TextureResource("", tile, false)); @@ -122,8 +131,10 @@ std::shared_ptr TextureResource::get(const std::string& path, b // need to create it std::shared_ptr tex; - tex = std::shared_ptr(new TextureResource(key.first, tile, dynamic)); - std::shared_ptr data = sTextureDataManager.get(tex.get()); + tex = std::shared_ptr(new TextureResource(key.first, tile, dynamic, block)); + // When block=false, pass enableLoading=false to avoid re-triggering a synchronous + // load — the constructor already queued it for async loading via TextureLoader. + std::shared_ptr data = sTextureDataManager.get(tex.get(), block); // is it an SVG? if(key.first.substr(key.first.size() - 4, std::string::npos) != ".svg") @@ -164,6 +175,42 @@ Vector2f TextureResource::getSourceImageSize() const return mSourceSize; } +bool TextureResource::updateTextureSize() +{ + // If sizes are already known, nothing to do + if (mSize != Vector2i(0, 0)) + return true; + + // Get the texture data without triggering a load + std::shared_ptr data; + if (mTextureData != nullptr) + data = mTextureData; + else + data = sTextureDataManager.get(this, false); + + if (!data) + return false; + + switch (data->loadStatus()) + { + case TextureData::LoadStatus::LOADED: + // The background thread has finished — read dimensions now + // (safe because LOADED guarantees data is available via mutex ordering) + mSize = Vector2i((int)data->width(), (int)data->height()); + mSourceSize = Vector2f(data->sourceWidth(), data->sourceHeight()); + return true; + + case TextureData::LoadStatus::FAILED: + // Permanently failed (e.g. file not found). Treat as done so callers + // don't keep polling. mSize stays (0,0) — image simply won't display. + return true; + + case TextureData::LoadStatus::LOADING: + default: + return false; + } +} + bool TextureResource::isInitialized() const { return true; @@ -208,7 +255,7 @@ bool TextureResource::unload() else data = mTextureData; - if (data != nullptr && data->isLoaded()) + if (data != nullptr && data->loadStatus() == TextureData::LoadStatus::LOADED) { data->releaseVRAM(); data->releaseRAM(); @@ -223,7 +270,7 @@ void TextureResource::reload() { // For dynamically loaded textures the texture manager will load them on demand. // For manually loaded textures we have to reload them here - if (mTextureData && !mTextureData->isLoaded()) + if (mTextureData && mTextureData->loadStatus() == TextureData::LoadStatus::LOADING) mTextureData->load(); // Uncomment this 2 lines in future release in order to reload texture VRAM exactly as it was before diff --git a/es-core/src/resources/TextureResource.h b/es-core/src/resources/TextureResource.h index c3c7e9dd29..e54a374df9 100644 --- a/es-core/src/resources/TextureResource.h +++ b/es-core/src/resources/TextureResource.h @@ -16,7 +16,7 @@ class TextureData; class TextureResource : public IReloadable { public: - static std::shared_ptr get(const std::string& path, bool tile = false, bool forceLoad = false, bool dynamic = true); + static std::shared_ptr get(const std::string& path, bool tile = false, bool forceLoad = false, bool dynamic = true, bool block = true); void initFromPixels(const unsigned char* dataRGBA, size_t width, size_t height); virtual void initFromMemory(const char* file, size_t length); @@ -24,6 +24,10 @@ class TextureResource : public IReloadable void rasterizeAt(size_t width, size_t height); Vector2f getSourceImageSize() const; + // Check if asynchronously-loaded texture data is ready and update cached sizes. + // Returns true if sizes are available (texture loaded), false if still pending. + bool updateTextureSize(); + virtual ~TextureResource(); bool isInitialized() const; @@ -36,7 +40,7 @@ class TextureResource : public IReloadable static size_t getTotalTextureSize(); // returns the number of bytes that would be used if all textures were in memory protected: - TextureResource(const std::string& path, bool tile, bool dynamic); + TextureResource(const std::string& path, bool tile, bool dynamic, bool block = true); virtual bool unload(); virtual void reload(); diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 94be3a58bd..21088786f2 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -605,13 +605,13 @@ namespace Utils if(!exists(path)) return true; + // try to remove file bool removed = (unlink(path.c_str()) == 0); - - // if removed, let's remove it from the index if (removed) + { + const std::unique_lock lock(mutex); pathExistsIndex[_path] = false; - - // try to remove file + } return removed; } // removeFile @@ -629,6 +629,7 @@ namespace Utils // try to create directory if(mkdir(path.c_str(), 0755) == 0) { + const std::unique_lock lock(mutex); pathExistsIndex[_path] = true; return true; } @@ -642,9 +643,11 @@ namespace Utils // try to create directory again now that the parent should exist bool created = (mkdir(path.c_str(), 0755) == 0); - if(created) + if (created) + { + const std::unique_lock lock(mutex); pathExistsIndex[_path] = true; - + } return created; } // createDirectory @@ -653,17 +656,27 @@ namespace Utils bool exists(const std::string& _path) { - const std::unique_lock lock(mutex); + // Fast path: return cached result without doing any I/O. + { + const std::unique_lock lock(mutex); + auto it = pathExistsIndex.find(_path); + if (it != pathExistsIndex.cend()) + return it->second; + } + + // Slow path: stat outside the lock so a blocking NAS call on the + // background texture-loader thread doesn't stall the main thread + // when it tries to acquire the same mutex (e.g. Scripting::fireEvent). + const std::string path = getGenericPath(_path); + struct stat64 info; + bool result = (stat64(path.c_str(), &info) == 0); - if(pathExistsIndex.find(_path) == pathExistsIndex.cend()) { - const std::string path = getGenericPath(_path); - struct stat64 info; - // check if stat64 succeeded - pathExistsIndex[_path] = (stat64(path.c_str(), &info) == 0); + const std::unique_lock lock(mutex); + pathExistsIndex[_path] = result; } - return pathExistsIndex.at(_path); + return result; } // exists