1
0
mirror of https://github.com/OpenRCT2/OpenRCT2 synced 2026-01-15 11:03:00 +01:00

Load the save previews in the background

This commit is contained in:
Matt
2025-04-05 17:36:25 +03:00
committed by GitHub
parent cd86eb659c
commit 3983e42115
5 changed files with 393 additions and 15 deletions

View File

@@ -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<LoadSaveWindow*>(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;
}

View File

@@ -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;

View File

@@ -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<IContext> CreateContext();

View File

@@ -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 <algorithm>
#include <atomic>
#include <condition_variable>
#include <deque>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <stop_token>
#include <thread>
#include <type_traits>
#include <vector>
namespace OpenRCT2
{
namespace Detail
{
template<typename TFn, typename TWantsToken>
struct ResultType;
template<typename TFn>
struct ResultType<TFn, std::true_type>
{
using type = std::invoke_result_t<TFn, std::atomic_bool&>;
};
template<typename TFn>
struct ResultType<TFn, std::false_type>
{
using type = std::invoke_result_t<TFn>;
};
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<typename Result>
class JobImpl final : public JobBase
{
public:
using WorkFunc = std::function<Result(std::atomic_bool&)>;
using CompletionFunc = std::function<void(Result)>;
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> _result;
};
template<>
class JobImpl<void> final : public JobBase
{
public:
using WorkFunc = std::function<void(std::atomic_bool&)>;
using CompletionFunc = std::function<void()>;
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<Detail::JobBase> 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<Detail::JobBase> _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<typename WorkFunc, typename CompletionFunc>
Job addJob(WorkFunc&& work, CompletionFunc&& completion)
{
static_assert(
std::is_invocable_v<WorkFunc, std::atomic_bool&> || std::is_invocable_v<WorkFunc>,
"Work function must be callable with or without stop_token");
constexpr bool expectsToken = std::is_invocable_v<WorkFunc, std::atomic_bool&>;
using Result = typename Detail::ResultType<WorkFunc, std::integral_constant<bool, expectsToken>>::type;
const auto wrappedFunc = [wf = std::forward<WorkFunc>(work)](std::atomic_bool& token) {
if constexpr (std::is_invocable_v<WorkFunc, std::atomic_bool&>)
{
return wf(token);
}
else
{
return wf();
}
};
if constexpr (std::is_void_v<Result>)
{
static_assert(std::is_invocable_v<CompletionFunc>, "Completion function must take no arguments for void work");
}
else
{
static_assert(std::is_invocable_v<CompletionFunc, Result>, "Completion function must match work result type");
}
auto job = std::make_shared<Detail::JobImpl<Result>>(
std::move(wrappedFunc), std::forward<CompletionFunc>(completion));
{
std::lock_guard lock(_mtx);
_jobs.push_back(job);
_pending.push_back(job);
}
_cv.notify_one();
return Job(job);
}
void dispatchCompleted()
{
std::vector<std::shared_ptr<Detail::JobBase>> 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<Detail::JobBase> 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<std::thread> _workThreads;
std::condition_variable _cv;
std::atomic_bool _shouldStop{ false };
std::vector<std::shared_ptr<Detail::JobBase>> _jobs;
std::deque<std::shared_ptr<Detail::JobBase>> _pending;
};
} // namespace OpenRCT2

View File

@@ -179,6 +179,7 @@
<ClInclude Include="config\IniWriter.hpp" />
<ClInclude Include="Context.h" />
<ClInclude Include="core\Algorithm.hpp" />
<ClInclude Include="core\BackgroundWorker.hpp" />
<ClInclude Include="core\BitSet.hpp" />
<ClInclude Include="core\CallingConventions.h" />
<ClInclude Include="core\ChecksumStream.h" />
@@ -1153,4 +1154,4 @@
</ClCompile>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>
</Project>