From 3983e421155448f520969ff532890ccfd6be58ae Mon Sep 17 00:00:00 2001 From: Matt <5415177+ZehMatt@users.noreply.github.com> Date: Sat, 5 Apr 2025 17:36:25 +0300 Subject: [PATCH] Load the save previews in the background --- src/openrct2-ui/windows/LoadSave.cpp | 66 +++-- src/openrct2/Context.cpp | 11 + src/openrct2/Context.h | 3 + src/openrct2/core/BackgroundWorker.hpp | 325 +++++++++++++++++++++++++ src/openrct2/libopenrct2.vcxproj | 3 +- 5 files changed, 393 insertions(+), 15 deletions(-) create mode 100644 src/openrct2/core/BackgroundWorker.hpp diff --git a/src/openrct2-ui/windows/LoadSave.cpp b/src/openrct2-ui/windows/LoadSave.cpp index f82076e2e7..2de63cc7fe 100644 --- a/src/openrct2-ui/windows/LoadSave.cpp +++ b/src/openrct2-ui/windows/LoadSave.cpp @@ -135,6 +135,7 @@ namespace OpenRCT2::Ui::Windows LoadSaveAction action; LoadSaveType type; ParkPreview _preview; + BackgroundWorker::Job _previewLoadJob; bool ShowPreviews() { @@ -334,24 +335,53 @@ namespace OpenRCT2::Ui::Windows return; auto path = _listItems[selected_list_item].path; - auto fs = FileStream(path, FileMode::open); - ClassifiedFileInfo info; - if (!TryClassifyFile(&fs, &info) || info.Type != ::FileType::park) - return; + auto& bgWorker = GetContext()->GetBackgroundWorker(); - try + if (_previewLoadJob.isValid()) { - auto& objectRepository = GetContext()->GetObjectRepository(); - auto parkImporter = ParkImporter::CreateParkFile(objectRepository); - parkImporter->LoadFromStream(&fs, false, true, path.c_str()); - _preview = parkImporter->GetParkPreview(); + _previewLoadJob.cancel(); } - catch (const std::exception& e) + + _previewLoadJob = bgWorker.addJob( + [path]() { + try + { + auto fs = FileStream(path, FileMode::open); + + ClassifiedFileInfo info; + if (!TryClassifyFile(&fs, &info) || info.Type != ::FileType::park) + return ParkPreview{}; + + auto& objectRepository = GetContext()->GetObjectRepository(); + auto parkImporter = ParkImporter::CreateParkFile(objectRepository); + parkImporter->LoadFromStream(&fs, false, true, path.c_str()); + return parkImporter->GetParkPreview(); + } + catch (const std::exception& e) + { + LOG_ERROR("Could not get preview:", e.what()); + return ParkPreview{}; + } + }, + [](const ParkPreview preview) { + auto* windowMgr = GetContext()->GetUiContext()->GetWindowManager(); + auto* wnd = windowMgr->FindByClass(WindowClass::Loadsave); + if (wnd == nullptr) + { + return; + } + auto* loadSaveWnd = static_cast(wnd); + loadSaveWnd->UpdateParkPreview(preview); + }); + } + + void UpdateParkPreview(const ParkPreview& preview) + { + _preview = preview; + if (ShowPreviews()) { - LOG_ERROR("Could not get preview:", e.what()); - _preview = {}; - return; + Invalidate(); } } @@ -395,8 +425,16 @@ namespace OpenRCT2::Ui::Windows GfxDrawSpriteSolid(dpi, ImageId(SPR_G2_LOGO_MONO_DITHERED), imagePos, colour); auto textPos = imagePos + ScreenCoordsXY(kPreviewWidth / 2, kPreviewHeight / 2 - 6); + + // NOTE: Can't simplify this as the compiler complains about different enumeration types. + StringId previewText = STR_NO_PREVIEW_AVAILABLE; + if (_previewLoadJob.isValid()) + { + previewText = STR_LOADING_GENERIC; + } + DrawTextBasic( - dpi, textPos, STR_NO_PREVIEW_AVAILABLE, {}, + dpi, textPos, previewText, {}, { ColourWithFlags{ COLOUR_WHITE }.withFlag(ColourFlag::withOutline, true), TextAlignment::CENTRE }); return; } diff --git a/src/openrct2/Context.cpp b/src/openrct2/Context.cpp index 1f1ea7c2ab..ef710f029a 100644 --- a/src/openrct2/Context.cpp +++ b/src/openrct2/Context.cpp @@ -160,6 +160,8 @@ namespace OpenRCT2 std::thread::id _mainThreadId{}; Timer _forcedUpdateTimer; + BackgroundWorker _backgroundWorker; + public: // Singleton of Context. // Remove this when GetContext() is no longer called so that @@ -1333,6 +1335,8 @@ namespace OpenRCT2 _ticksAccumulator -= kGameUpdateTimeMS; } + _backgroundWorker.dispatchCompleted(); + ContextHandleInput(); WindowUpdateAll(); @@ -1366,6 +1370,8 @@ namespace OpenRCT2 tweener.PostTick(); } + _backgroundWorker.dispatchCompleted(); + ContextHandleInput(); WindowUpdateAll(); @@ -1550,6 +1556,11 @@ namespace OpenRCT2 { return _timeScale; } + + BackgroundWorker& GetBackgroundWorker() override + { + return _backgroundWorker; + } }; Context* Context::Instance = nullptr; diff --git a/src/openrct2/Context.h b/src/openrct2/Context.h index 737b6415e8..bb866af868 100644 --- a/src/openrct2/Context.h +++ b/src/openrct2/Context.h @@ -9,6 +9,7 @@ #pragma once +#include "core/BackgroundWorker.hpp" #include "core/StringTypes.h" #include "interface/WindowClasses.h" #include "localisation/StringIdType.h" @@ -177,6 +178,8 @@ namespace OpenRCT2 virtual void SetTimeScale(float newScale) = 0; virtual float GetTimeScale() const = 0; + + virtual BackgroundWorker& GetBackgroundWorker() = 0; }; [[nodiscard]] std::unique_ptr CreateContext(); diff --git a/src/openrct2/core/BackgroundWorker.hpp b/src/openrct2/core/BackgroundWorker.hpp new file mode 100644 index 0000000000..cb35352719 --- /dev/null +++ b/src/openrct2/core/BackgroundWorker.hpp @@ -0,0 +1,325 @@ +/***************************************************************************** + * Copyright (c) 2014-2025 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace OpenRCT2 +{ + namespace Detail + { + template + struct ResultType; + + template + struct ResultType + { + using type = std::invoke_result_t; + }; + + template + struct ResultType + { + using type = std::invoke_result_t; + }; + + class JobBase + { + public: + virtual ~JobBase() = default; + + virtual void run() = 0; + virtual void dispatch() = 0; + + void cancel() + { + _stopSource.store(true); + _valid.store(false); + } + + bool isCompleted() const + { + return _completed.load(); + } + + bool isValid() const + { + return _valid.load(); + } + + protected: + std::atomic_bool _valid{ true }; + std::atomic_bool _completed{ false }; + std::atomic_bool _stopSource{ false }; + }; + + template + class JobImpl final : public JobBase + { + public: + using WorkFunc = std::function; + using CompletionFunc = std::function; + + JobImpl(WorkFunc work, CompletionFunc completion) + : _workFn(std::move(work)) + , _completionFn(std::move(completion)) + { + } + + void run() override + { + if (!_stopSource.load()) + { + _result.emplace(_workFn(_stopSource)); + _completed.store(true); + } + } + + void dispatch() override + { + if (!_stopSource.load() && _completed.load() && _completionFn) + { + _completionFn(std::move(_result.value())); + _valid.store(false); + } + } + + private: + WorkFunc _workFn; + CompletionFunc _completionFn; + std::optional _result; + }; + + template<> + class JobImpl final : public JobBase + { + public: + using WorkFunc = std::function; + using CompletionFunc = std::function; + + JobImpl(WorkFunc work, CompletionFunc completion) + : _workFn(std::move(work)) + , _completionFn(std::move(completion)) + { + } + + void run() override + { + if (!_stopSource.load()) + { + _workFn(_stopSource); + _completed.store(true); + } + } + + void dispatch() override + { + if (!_stopSource.load() && _completed.load() && _completionFn) + { + _completionFn(); + } + } + + private: + WorkFunc _workFn; + CompletionFunc _completionFn; + }; + + } // namespace Detail + + class BackgroundWorker + { + public: + class Job + { + public: + Job() = default; + explicit Job(std::shared_ptr job) + : _jobRef(job) + { + } + + bool isValid() const + { + auto job = _jobRef.lock(); + return job && job->isValid(); + } + + void cancel() + { + if (auto job = _jobRef.lock()) + { + job->cancel(); + } + } + + private: + std::weak_ptr _jobRef; + }; + + public: + BackgroundWorker() + { + const auto threadsAvailable = std::max(std::thread::hardware_concurrency(), 1u); + + // NOTE: We don't want to use all available threads, this is for background work only. + // Adjust the number of threads if needed. + const auto numThreads = std::min(threadsAvailable, 2u); + + for (auto i = 0u; i < numThreads; ++i) + { + _workThreads.emplace_back([this] { processJobs(); }); + } + } + + ~BackgroundWorker() + { + { + std::lock_guard lock(_mtx); + _shouldStop = true; + } + _cv.notify_all(); + for (auto& thread : _workThreads) + { + if (thread.joinable()) + { + thread.join(); + } + } + } + + template + Job addJob(WorkFunc&& work, CompletionFunc&& completion) + { + static_assert( + std::is_invocable_v || std::is_invocable_v, + "Work function must be callable with or without stop_token"); + + constexpr bool expectsToken = std::is_invocable_v; + using Result = typename Detail::ResultType>::type; + + const auto wrappedFunc = [wf = std::forward(work)](std::atomic_bool& token) { + if constexpr (std::is_invocable_v) + { + return wf(token); + } + else + { + return wf(); + } + }; + + if constexpr (std::is_void_v) + { + static_assert(std::is_invocable_v, "Completion function must take no arguments for void work"); + } + else + { + static_assert(std::is_invocable_v, "Completion function must match work result type"); + } + + auto job = std::make_shared>( + std::move(wrappedFunc), std::forward(completion)); + + { + std::lock_guard lock(_mtx); + _jobs.push_back(job); + _pending.push_back(job); + } + _cv.notify_one(); + + return Job(job); + } + + void dispatchCompleted() + { + std::vector> completed; + + { + std::lock_guard lock(_mtx); + _jobs.erase( + std::remove_if( + _jobs.begin(), _jobs.end(), + [&](const auto& job) { + // Check if job is completed and valid + if (job->isCompleted() && job->isValid()) + { + completed.push_back(std::move(job)); + return true; + } + // Remove invalid jobs + if (!job->isValid()) + { + return true; + } + // Keep the job in the list. + return false; + }), + _jobs.end()); + } + + for (auto& job : completed) + { + job->dispatch(); + } + } + + bool empty() const + { + std::lock_guard lock(_mtx); + return _jobs.empty(); + } + + size_t size() const + { + std::lock_guard lock(_mtx); + return _jobs.size(); + } + + private: + void processJobs() + { + while (true) + { + std::shared_ptr job; + { + std::unique_lock lock(_mtx); + _cv.wait(lock, [this] { return !_pending.empty() || _shouldStop; }); + + if (_shouldStop) + { + break; + } + + job = _pending.front(); + _pending.pop_front(); + } + job->run(); + } + } + + mutable std::mutex _mtx; + std::vector _workThreads; + std::condition_variable _cv; + std::atomic_bool _shouldStop{ false }; + std::vector> _jobs; + std::deque> _pending; + }; + +} // namespace OpenRCT2 diff --git a/src/openrct2/libopenrct2.vcxproj b/src/openrct2/libopenrct2.vcxproj index ca356e22bb..1329c65536 100644 --- a/src/openrct2/libopenrct2.vcxproj +++ b/src/openrct2/libopenrct2.vcxproj @@ -179,6 +179,7 @@ + @@ -1153,4 +1154,4 @@ - + \ No newline at end of file