// SPDX-License-Identifier: MIT // // ES-DE // Screensaver.cpp // // Screensaver, supporting the following types: // Dim, black, slideshow, video. // #include "Screensaver.h" #include "FileData.h" #include "Log.h" #include "SystemData.h" #include "UIModeController.h" #include "components/VideoFFmpegComponent.h" #include "resources/Font.h" #include "utils/FileSystemUtil.h" #include "utils/StringUtil.h" #include "views/GamelistView.h" #include "views/ViewController.h" #include #include #if defined(_WIN64) #include #endif #define IMAGES_FADE_IN_TIME 450.0f Screensaver::Screensaver() : mRenderer {Renderer::getInstance()} , mWindow {Window::getInstance()} , mImageScreensaver {nullptr} , mVideoScreensaver {nullptr} , mCurrentGame {nullptr} , mPreviousGame {nullptr} , mTimer {0} , mMediaSwapTime {0} , mScreensaverActive {false} , mTriggerNextGame {false} , mHasMediaFiles {false} , mFallbackScreensaver {false} , mOpacity {0.0f} , mDimValue {1.0} , mRectangleFadeIn {50} , mTextFadeIn {0} , mSaturationAmount {1.0} { mWindow->setScreensaver(this); } void Screensaver::startScreensaver(bool generateMediaList) { ViewController::getInstance()->pauseViewVideos(); mScreensaverType = Settings::getInstance()->getString("ScreensaverType"); // In case there is an invalid entry in the es_settings.xml file. if (mScreensaverType != "dim" && mScreensaverType != "black" && mScreensaverType != "slideshow" && mScreensaverType != "video") { mScreensaverType = "dim"; } std::string path; mScreensaverActive = true; mHasMediaFiles = false; mFallbackScreensaver = false; mOpacity = 0.0f; // Keep a reference to the default fonts, so they don't keep getting destroyed/recreated. if (mGameOverlayFont.empty()) { mGameOverlayFont.push_back(Font::get(FONT_SIZE_SMALL)); mGameOverlayFont.push_back(Font::get(FONT_SIZE_MEDIUM)); mGameOverlayFont.push_back(Font::get(FONT_SIZE_LARGE)); } // Set mPreviousGame which will be used to avoid showing the same game again during // the random selection. if ((mScreensaverType == "slideshow" || mScreensaverType == "video") && mCurrentGame != nullptr) mPreviousGame = mCurrentGame; if (mScreensaverType == "slideshow") { if (generateMediaList) { mImageFiles.clear(); mFilesInventory.clear(); mImageCustomFiles.clear(); mCustomFilesInventory.clear(); } mMediaSwapTime = Settings::getInstance()->getInt("ScreensaverSwapImageTimeout"); // Load a random image. if (Settings::getInstance()->getBool("ScreensaverSlideshowCustomImages")) { if (generateMediaList) generateCustomImageList(); pickRandomCustomImage(path); // We've cycled through all games, so start from the beginning again. if (mImageCustomFiles.size() == 0 && mCustomFilesInventory.size() > 0) mImageCustomFiles.insert(mImageCustomFiles.begin(), mCustomFilesInventory.begin(), mCustomFilesInventory.end()); if (mImageCustomFiles.size() > 0) mHasMediaFiles = true; // Custom images are not tied to the game list. mCurrentGame = nullptr; } else { if (generateMediaList) generateImageList(); pickRandomImage(path); } // We've cycled through all games, so start from the beginning again. if (mImageFiles.size() == 0 && mFilesInventory.size() > 0) mImageFiles.insert(mImageFiles.begin(), mFilesInventory.begin(), mFilesInventory.end()); if (mImageFiles.size() > 0) mHasMediaFiles = true; // Don't attempt to render the screensaver if there are no images available, but // do flag it as running. This way render() will fade to a black screen, i.e. it // will activate the 'Black' screensaver type. if (mImageFiles.size() > 0 || mImageCustomFiles.size() > 0) { if (Settings::getInstance()->getBool("ScreensaverSlideshowGameInfo")) generateOverlayInfo(); if (!mImageScreensaver) mImageScreensaver = std::make_unique(false, false); mTimer = 0; mImageScreensaver->setImage(path); mImageScreensaver->setOrigin(0.5f, 0.5f); mImageScreensaver->setPosition(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f); if (Settings::getInstance()->getBool("ScreensaverStretchImages")) mImageScreensaver->setResize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); else mImageScreensaver->setMaxSize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); } mTimer = 0; return; } else if (!mVideoScreensaver && (mScreensaverType == "video")) { if (generateMediaList) { mVideoFiles.clear(); mFilesInventory.clear(); } mMediaSwapTime = Settings::getInstance()->getInt("ScreensaverSwapVideoTimeout"); // Load a random video. if (generateMediaList) generateVideoList(); pickRandomVideo(path); // We've cycled through all games, so start from the beginning again. if (mVideoFiles.size() == 0 && mFilesInventory.size() > 0) mVideoFiles.insert(mVideoFiles.begin(), mFilesInventory.begin(), mFilesInventory.end()); if (mVideoFiles.size() > 0) mHasMediaFiles = true; if (!path.empty() && Utils::FileSystem::exists(path)) { if (Settings::getInstance()->getBool("ScreensaverVideoGameInfo")) generateOverlayInfo(); mVideoScreensaver = std::make_unique(); mVideoScreensaver->setOrigin(0.5f, 0.5f); mVideoScreensaver->setPosition(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f); if (Settings::getInstance()->getBool("ScreensaverStretchVideos")) mVideoScreensaver->setResize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); else mVideoScreensaver->setMaxSize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); mVideoScreensaver->setVideo(path); mVideoScreensaver->setScreensaverMode(true); mVideoScreensaver->startVideoPlayer(); mTimer = 0; return; } } // No videos or images, just use a standard screensaver. mCurrentGame = nullptr; } void Screensaver::stopScreensaver() { mImageScreensaver.reset(); mVideoScreensaver.reset(); mScreensaverActive = false; mDimValue = 1.0f; mRectangleFadeIn = 50; mTextFadeIn = 0; mSaturationAmount = 1.0f; if (mGameOverlay) mGameOverlay.reset(); ViewController::getInstance()->startViewVideos(); } void Screensaver::nextGame() { stopScreensaver(); startScreensaver(false); } void Screensaver::launchGame() { if (mCurrentGame != nullptr) { // If the game is inside a folder where a folder link entry is present, then jump to // that folder instead of to the actual game file. Also check the complete hierarchy in // case folder link entries are set on multiple levels. FileData* entry {mCurrentGame}; FileData* selectGame {mCurrentGame}; FileData* launchFolder {nullptr}; while (entry != nullptr) { entry = entry->getParent(); if (entry != nullptr && entry->metadata.get("folderlink") != "") launchFolder = entry; } if (launchFolder != nullptr) selectGame = launchFolder; // Launching game ViewController::getInstance()->triggerGameLaunch(mCurrentGame); ViewController::getInstance()->goToGamelist(mCurrentGame->getSystem()); GamelistView* view { ViewController::getInstance()->getGamelistView(mCurrentGame->getSystem()).get()}; view->setCursor(selectGame); view->stopListScrolling(); ViewController::getInstance()->cancelViewTransitions(); ViewController::getInstance()->pauseViewVideos(); } } void Screensaver::goToGame() { if (mCurrentGame != nullptr) { FileData* entry {mCurrentGame}; FileData* launchFolder {nullptr}; while (entry != nullptr) { entry = entry->getParent(); if (entry != nullptr && entry->metadata.get("folderlink") != "") launchFolder = entry; } if (launchFolder != nullptr) mCurrentGame = launchFolder; // Go to the game in the gamelist view, but don't launch it. ViewController::getInstance()->goToGamelist(mCurrentGame->getSystem()); GamelistView* view { ViewController::getInstance()->getGamelistView(mCurrentGame->getSystem()).get()}; view->setCursor(mCurrentGame); view->stopListScrolling(); ViewController::getInstance()->cancelViewTransitions(); } } void Screensaver::renderScreensaver() { glm::mat4 trans {Renderer::getIdentity()}; mRenderer->setMatrix(trans); if (mVideoScreensaver && mScreensaverType == "video") { // Render a black background below the video. mRenderer->drawRect(0.0f, 0.0f, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF, 0x000000FF); mVideoScreensaver->render(trans); } else if (mImageScreensaver && mScreensaverType == "slideshow") { // Render a black background below the image. mRenderer->drawRect(0.0f, 0.0f, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF, 0x000000FF); // Leave a small gap without rendering during fade-in. if (mOpacity > 0.5f) { mImageScreensaver->setOpacity(mOpacity); mImageScreensaver->render(trans); } } if (mScreensaverType == "slideshow") { if (mHasMediaFiles) { if (Settings::getInstance()->getBool("ScreensaverSlideshowScanlines")) mRenderer->shaderPostprocessing(Renderer::Shader::SCANLINES); if (Settings::getInstance()->getBool("ScreensaverSlideshowGameInfo") && !Settings::getInstance()->getBool("ScreensaverSlideshowCustomImages") && mGameOverlay) { mRenderer->setMatrix(mRenderer->getIdentity()); if (mGameOverlayRectangleCoords.size() == 4) { mRenderer->drawRect( mGameOverlayRectangleCoords[0], mGameOverlayRectangleCoords[1], mGameOverlayRectangleCoords[2], mGameOverlayRectangleCoords[3], 0x00000000 | mRectangleFadeIn, 0x00000000 | mRectangleFadeIn); } mRectangleFadeIn = glm::clamp(mRectangleFadeIn + 6 + mRectangleFadeIn / 20, 0, 170); mGameOverlay.get()->setColor(0xFFFFFF00 | mTextFadeIn); if (mTextFadeIn > 50) mGameOverlayFont.at(0)->renderTextCache(mGameOverlay.get()); if (mTextFadeIn < 255) mTextFadeIn = glm::clamp(mTextFadeIn + 2 + mTextFadeIn / 6, 0, 255); } } else { mFallbackScreensaver = true; } } else if (mScreensaverType == "video") { if (mHasMediaFiles) { Renderer::postProcessingParams videoParameters; unsigned int shaders {0}; if (Settings::getInstance()->getBool("ScreensaverVideoScanlines")) shaders = Renderer::Shader::SCANLINES; if (Settings::getInstance()->getBool("ScreensaverVideoBlur")) { if (mRenderer->getScreenRotation() == 90 || mRenderer->getScreenRotation() == 270) shaders |= Renderer::Shader::BLUR_VERTICAL; else shaders |= Renderer::Shader::BLUR_HORIZONTAL; } // We run two passes to make the blur smoother. videoParameters.blurPasses = 2; videoParameters.blurStrength = 1.35f; if (shaders != 0) mRenderer->shaderPostprocessing(shaders, videoParameters); if (Settings::getInstance()->getBool("ScreensaverVideoGameInfo") && mGameOverlay) { mRenderer->setMatrix(mRenderer->getIdentity()); if (mGameOverlayRectangleCoords.size() == 4) { mRenderer->drawRect( mGameOverlayRectangleCoords[0], mGameOverlayRectangleCoords[1], mGameOverlayRectangleCoords[2], mGameOverlayRectangleCoords[3], 0x00000000 | mRectangleFadeIn, 0x00000000 | mRectangleFadeIn); } mRectangleFadeIn = glm::clamp(mRectangleFadeIn + 6 + mRectangleFadeIn / 20, 0, 170); mGameOverlay.get()->setColor(0xFFFFFF00 | mTextFadeIn); if (mTextFadeIn > 50) mGameOverlayFont.at(0)->renderTextCache(mGameOverlay.get()); if (mTextFadeIn < 255) mTextFadeIn = glm::clamp(mTextFadeIn + 2 + mTextFadeIn / 6, 0, 255); } } else { mFallbackScreensaver = true; } } if (mFallbackScreensaver || mScreensaverType == "dim") { Renderer::postProcessingParams dimParameters; dimParameters.dimming = mDimValue; dimParameters.saturation = mSaturationAmount; mRenderer->shaderPostprocessing(Renderer::Shader::CORE, dimParameters); if (mDimValue > 0.4) mDimValue = glm::clamp(mDimValue - 0.021f, 0.4f, 1.0f); if (mSaturationAmount > 0.0) mSaturationAmount = glm::clamp(mSaturationAmount - 0.035f, 0.0f, 1.0f); } else if (mScreensaverType == "black") { Renderer::postProcessingParams blackParameters; blackParameters.dimming = mDimValue; mRenderer->shaderPostprocessing(Renderer::Shader::CORE, blackParameters); if (mDimValue > 0.0) mDimValue = glm::clamp(mDimValue - 0.045f, 0.0f, 1.0f); } } void Screensaver::update(int deltaTime) { // Update the timer that swaps the media, unless the swap time is set to 0 (only // applicable for the video screensaver). This means that videos play to the end, // at which point the video player will trigger a skip to the next game. if (mMediaSwapTime != 0) { mTimer += deltaTime; if (mTimer > mMediaSwapTime) nextGame(); } if (mTriggerNextGame) { mTriggerNextGame = false; nextGame(); } // Fade-in for the video screensaver is handled in VideoComponent. if (mImageScreensaver && mOpacity < 1.0f) { mOpacity += static_cast(deltaTime) / IMAGES_FADE_IN_TIME; if (mOpacity > 1.0f) mOpacity = 1.0f; } if (mVideoScreensaver) mVideoScreensaver->update(deltaTime); } void Screensaver::generateImageList() { const bool favoritesOnly { Settings::getInstance()->getBool("ScreensaverSlideshowOnlyFavorites")}; for (auto it = SystemData::sSystemVector.cbegin(); // Line break. it != SystemData::sSystemVector.cend(); ++it) { // We only want nodes from game systems that are not collections. if (!(*it)->isGameSystem() || (*it)->isCollection()) continue; std::vector allFiles {(*it)->getRootFolder()->getFilesRecursive(GAME, true)}; for (auto it2 = allFiles.cbegin(); it2 != allFiles.cend(); ++it2) { // Only include games suitable for children if we're in Kid UI mode. if (UIModeController::getInstance()->isUIModeKid() && (*it2)->metadata.get("kidgame") != "true") continue; if (favoritesOnly && (*it2)->metadata.get("favorite") != "true") continue; std::string imagePath {(*it2)->getImagePath()}; if (imagePath != "") mImageFiles.push_back((*it2)); } } mFilesInventory.insert(mFilesInventory.begin(), mImageFiles.begin(), mImageFiles.end()); } void Screensaver::generateVideoList() { const bool favoritesOnly {Settings::getInstance()->getBool("ScreensaverVideoOnlyFavorites")}; for (auto it = SystemData::sSystemVector.cbegin(); // Line break. it != SystemData::sSystemVector.cend(); ++it) { // We only want nodes from game systems that are not collections. if (!(*it)->isGameSystem() || (*it)->isCollection()) continue; std::vector allFiles {(*it)->getRootFolder()->getFilesRecursive(GAME, true)}; for (auto it2 = allFiles.cbegin(); it2 != allFiles.cend(); ++it2) { // Only include games suitable for children if we're in Kid UI mode. if (UIModeController::getInstance()->isUIModeKid() && (*it2)->metadata.get("kidgame") != "true") continue; if (favoritesOnly && (*it2)->metadata.get("favorite") != "true") continue; std::string videoPath {(*it2)->getVideoPath()}; if (videoPath != "") mVideoFiles.push_back((*it2)); } } mFilesInventory.insert(mFilesInventory.begin(), mVideoFiles.begin(), mVideoFiles.end()); } void Screensaver::generateCustomImageList() { std::string imageDir {Utils::FileSystem::expandHomePath( Settings::getInstance()->getString("ScreensaverSlideshowCustomDir"))}; if (imageDir.empty()) imageDir = Utils::FileSystem::getAppDataDirectory() + "/screensavers/custom_slideshow"; // This makes it possible to set the custom image directory relative to the ES-DE binary // directory or the ROM directory. imageDir = Utils::String::replace(imageDir, "%ESPATH%", Utils::FileSystem::getExePath()); imageDir = Utils::String::replace(imageDir, "%ROMPATH%", FileData::getROMDirectory()); if (imageDir != "" && Utils::FileSystem::isDirectory(imageDir)) { const std::vector extList {".jpg", ".JPG", ".png", ".PNG", ".gif", ".GIF", ".webp", ".WEBP", ".svg", ".SVG"}; Utils::FileSystem::StringList dirContent {Utils::FileSystem::getDirContent( imageDir, Settings::getInstance()->getBool("ScreensaverSlideshowRecurse"))}; for (auto it = dirContent.begin(); it != dirContent.end(); ++it) { if (Utils::FileSystem::isRegularFile(*it)) { if (std::find(extList.cbegin(), extList.cend(), Utils::FileSystem::getExtension(*it)) != extList.cend()) mImageCustomFiles.push_back(*it); } } } else { LOG(LogWarning) << "Custom screensaver image directory \"" << imageDir << "\" does not exist"; } mCustomFilesInventory.insert(mCustomFilesInventory.begin(), mImageCustomFiles.begin(), mImageCustomFiles.end()); } void Screensaver::pickRandomImage(std::string& path) { mCurrentGame = nullptr; if (mImageFiles.size() == 0) return; if (mImageFiles.size() == 1) { mPreviousGame = nullptr; mCurrentGame = mImageFiles.front(); path = mImageFiles.front()->getImagePath(); mGameName = mImageFiles.front()->getName(); mSystemName = mImageFiles.front()->getSystem()->getFullName(); mCurrentGame = mImageFiles.front(); mImageFiles.clear(); return; } unsigned int index; do { // Get a random number in range. std::random_device randDev; // Mersenne Twister pseudorandom number generator. std::mt19937 engine {randDev()}; std::uniform_int_distribution uniform_dist {0, static_cast(mImageFiles.size()) - 1}; index = uniform_dist(engine); } while (mPreviousGame && mImageFiles.at(index) == mPreviousGame); path = mImageFiles.at(index)->getImagePath(); mGameName = mImageFiles.at(index)->getName(); mSystemName = mImageFiles.at(index)->getSystem()->getFullName(); mCurrentGame = mImageFiles.at(index); // Don't display the same image again until we've cycled through all entries. auto it = mImageFiles.begin() + index; mImageFiles.erase(it); } void Screensaver::pickRandomVideo(std::string& path) { mCurrentGame = nullptr; if (mVideoFiles.size() == 0) return; if (mVideoFiles.size() == 1) { mPreviousGame = nullptr; mCurrentGame = mVideoFiles.front(); path = mVideoFiles.front()->getVideoPath(); mGameName = mVideoFiles.front()->getName(); mSystemName = mVideoFiles.front()->getSystem()->getFullName(); mCurrentGame = mVideoFiles.front(); mVideoFiles.clear(); return; } unsigned int index; do { // Get a random number in range. std::random_device randDev; // Mersenne Twister pseudorandom number generator. std::mt19937 engine {randDev()}; std::uniform_int_distribution uniform_dist {0, static_cast(mVideoFiles.size()) - 1}; index = uniform_dist(engine); } while (mPreviousGame && mVideoFiles.at(index) == mPreviousGame); path = mVideoFiles.at(index)->getVideoPath(); mGameName = mVideoFiles.at(index)->getName(); mSystemName = mVideoFiles.at(index)->getSystem()->getFullName(); mCurrentGame = mVideoFiles.at(index); // Don't play the same video again until we've cycled through all entries. auto it = mVideoFiles.begin() + index; mVideoFiles.erase(it); } void Screensaver::pickRandomCustomImage(std::string& path) { if (mImageCustomFiles.size() == 0) return; if (mImageCustomFiles.size() == 1) { mPreviousCustomImage = mImageCustomFiles.front(); path = mImageCustomFiles.front(); mImageCustomFiles.clear(); return; } unsigned int index; do { // Get a random number in range. std::random_device randDev; // Mersenne Twister pseudorandom number generator. std::mt19937 engine {randDev()}; std::uniform_int_distribution uniform_dist { 0, static_cast(mImageCustomFiles.size()) - 1}; index = uniform_dist(engine); } while (mPreviousCustomImage != "" && mImageCustomFiles.at(index) == mPreviousCustomImage); path = mImageCustomFiles.at(index); mPreviousCustomImage = path; mGameName = ""; mSystemName = ""; // Don't display the same image again until we've cycled through all entries. auto it = mImageCustomFiles.begin() + index; mImageCustomFiles.erase(it); } void Screensaver::generateOverlayInfo() { if (mGameName == "" || mSystemName == "") return; float posX {mRenderer->getScreenWidth() * 0.023f}; float posY {mRenderer->getScreenHeight() * 0.02f}; const bool favoritesOnly { (mScreensaverType == "video" && Settings::getInstance()->getBool("ScreensaverVideoOnlyFavorites")) || (mScreensaverType == "slideshow" && Settings::getInstance()->getBool("ScreensaverSlideshowOnlyFavorites"))}; std::string favoriteChar; // Don't add the favorites character if only displaying favorite games. if (!favoritesOnly && mCurrentGame && mCurrentGame->getFavorite()) favoriteChar.append(" ").append(ViewController::FAVORITE_CHAR); const std::string gameName {Utils::String::toUpper(mGameName) + favoriteChar}; const std::string systemName {Utils::String::toUpper(mSystemName)}; const std::string overlayText {gameName + "\n" + systemName}; mGameOverlay = std::unique_ptr( mGameOverlayFont.at(0)->buildTextCache(overlayText, posX, posY, 0xFFFFFFFF)); float textSizeX {0.0f}; float textSizeY {mGameOverlayFont[0].get()->sizeText(overlayText).y}; // There is a weird issue with sizeText() where the X size value is returned // as too large if there are two rows in a string and the second row is longer // than the first row. Possibly it's the newline character that is somehow // injected in the size calculation. Regardless, this workaround is working // fine for the time being. if (mGameOverlayFont[0].get()->sizeText(gameName).x > mGameOverlayFont[0].get()->sizeText(systemName).x) textSizeX = mGameOverlayFont[0].get()->sizeText(gameName).x; else textSizeX = mGameOverlayFont[0].get()->sizeText(systemName).x; float marginX {mRenderer->getScreenWidth() * 0.01f}; mGameOverlayRectangleCoords.clear(); mGameOverlayRectangleCoords.push_back(posX - marginX); mGameOverlayRectangleCoords.push_back(posY); mGameOverlayRectangleCoords.push_back(textSizeX + marginX * 2.0f); mGameOverlayRectangleCoords.push_back(textSizeY); }