diff options
| author | JosJuice <josjuice@gmail.com> | 2025-11-22 21:52:17 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-22 21:52:17 +0100 |
| commit | 30dbcb2f808093ed93107297b4e76c27687cef84 (patch) | |
| tree | 3b102f540bdb53ff55baf0a66e2cda56939746ce | |
| parent | 6464ed9e6b158c57d25e5e2e140ed761faa0fc5c (diff) | |
| parent | 5a6fce31b2fcfefd83509c8898fac6dc327b68bf (diff) | |
Merge pull request #14128 from jordan-woyak/fix-run-on-object-race
DolphinQt/QtUtils: Simplify RunOnObject and eliminate Common::Event race. Introduce Common::OneShotEvent class.
| -rw-r--r-- | Source/Core/Common/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | Source/Core/Common/Functional.h | 14 | ||||
| -rw-r--r-- | Source/Core/Common/OneShotEvent.h | 55 | ||||
| -rw-r--r-- | Source/Core/Common/ScopeGuard.h | 5 | ||||
| -rw-r--r-- | Source/Core/DolphinLib.props | 1 | ||||
| -rw-r--r-- | Source/Core/DolphinQt/QtUtils/RunOnObject.h | 70 |
6 files changed, 87 insertions, 59 deletions
diff --git a/Source/Core/Common/CMakeLists.txt b/Source/Core/Common/CMakeLists.txt index e833893a9d..2447d1ed40 100644 --- a/Source/Core/Common/CMakeLists.txt +++ b/Source/Core/Common/CMakeLists.txt @@ -118,6 +118,7 @@ add_library(common NandPaths.h Network.cpp Network.h + OneShotEvent.h PcapFile.cpp PcapFile.h Profiler.cpp diff --git a/Source/Core/Common/Functional.h b/Source/Core/Common/Functional.h index cdb44ebb0c..7f96664041 100644 --- a/Source/Core/Common/Functional.h +++ b/Source/Core/Common/Functional.h @@ -4,6 +4,7 @@ #pragma once #include <concepts> +#include <functional> #include <memory> #include <type_traits> @@ -51,4 +52,17 @@ private: std::unique_ptr<FuncBase> m_ptr; }; +// A functor type with an invocable non-type template parameter. +// e.g. Providing a function pointer will create a functor type that invokes said function. +// It allows using function pointers in contexts that expect a type, e.g. as a "deleter". +template <auto Invocable> +struct InvokerOf +{ + template <typename... Args> + constexpr auto operator()(Args... args) const + { + return std::invoke(Invocable, std::forward<Args>(args)...); + } +}; + } // namespace Common diff --git a/Source/Core/Common/OneShotEvent.h b/Source/Core/Common/OneShotEvent.h new file mode 100644 index 0000000000..312848ed91 --- /dev/null +++ b/Source/Core/Common/OneShotEvent.h @@ -0,0 +1,55 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <chrono> +#include <memory> +#include <semaphore> + +#include "Common/Functional.h" + +namespace Common +{ + +// A one-time-use single-producer single-consumer thread synchronization class. +// Safe when `Set` will cause a `Wait`ing thread to immediately destruct the event itself. +class OneShotEvent +{ +public: + // One thread should call Set() exactly once. + void Set() { m_semaphore.release(1); } + + // One thread may call Wait() once or WaitFor() until it returns true. + + void Wait() { m_semaphore.acquire(); } + + template <typename Rep, typename Period> + bool WaitFor(const std::chrono::duration<Rep, Period>& rel_time) + { + return m_semaphore.try_acquire_for(rel_time); + } + +private: + std::binary_semaphore m_semaphore{0}; +}; + +// Invokes Set() on the given object upon destruction. +template <typename EventType> +class ScopedSetter +{ +public: + ScopedSetter() = default; + explicit ScopedSetter(EventType* ptr) : m_ptr{ptr} {} + + // Forgets the object without invoking Set(). + void Dismiss() { m_ptr.release(); } + +private: + // Leveraging unique_ptr conveniently makes this class move-only. + // It does no actual deletion, just calls Set(). + using NonOwningSetOnDeletePtr = std::unique_ptr<EventType, InvokerOf<&EventType::Set>>; + NonOwningSetOnDeletePtr m_ptr; +}; + +} // namespace Common diff --git a/Source/Core/Common/ScopeGuard.h b/Source/Core/Common/ScopeGuard.h index 2e313396a9..9e419f4d87 100644 --- a/Source/Core/Common/ScopeGuard.h +++ b/Source/Core/Common/ScopeGuard.h @@ -13,10 +13,7 @@ class ScopeGuard final public: ScopeGuard(Callable&& finalizer) : m_finalizer(std::forward<Callable>(finalizer)) {} - ScopeGuard(ScopeGuard&& other) : m_finalizer(std::move(other.m_finalizer)) - { - other.m_finalizer = nullptr; - } + ScopeGuard(ScopeGuard&& other) : m_finalizer(std::move(other.m_finalizer)) { other.Dismiss(); } ~ScopeGuard() { Exit(); } void Dismiss() { m_finalizer.reset(); } diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 3cfac5838c..f4a9c04a92 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -149,6 +149,7 @@ <ClInclude Include="Common\Mutex.h" /> <ClInclude Include="Common\NandPaths.h" /> <ClInclude Include="Common\Network.h" /> + <ClInclude Include="Common\OneShotEvent.h" /> <ClInclude Include="Common\PcapFile.h" /> <ClInclude Include="Common\Profiler.h" /> <ClInclude Include="Common\Projection.h" /> diff --git a/Source/Core/DolphinQt/QtUtils/RunOnObject.h b/Source/Core/DolphinQt/QtUtils/RunOnObject.h index 73e6c05bee..dfa6bbbf91 100644 --- a/Source/Core/DolphinQt/QtUtils/RunOnObject.h +++ b/Source/Core/DolphinQt/QtUtils/RunOnObject.h @@ -3,74 +3,34 @@ #pragma once -#include <QCoreApplication> -#include <QEvent> -#include <QPointer> -#include <QThread> +#include <concepts> +#include <functional> #include <optional> -#include <type_traits> -#include <utility> -#include "Common/Event.h" -#include "DolphinQt/QtUtils/QueueOnObject.h" +#include <QMetaObject> -class QObject; +#include "Common/OneShotEvent.h" // QWidget and subclasses are not thread-safe! This helper takes arbitrary code from any thread, // safely runs it on the appropriate GUI thread, waits for it to finish, and returns the result. // -// If the target object is destructed before the code gets to run, the QPointer will be nulled and -// the function will return nullopt. +// If the target object is destructed before the code gets to run the function will return nullopt. -template <typename F> -auto RunOnObject(QObject* object, F&& functor) +auto RunOnObject(QObject* object, std::invocable<> auto&& functor) { - using OptionalResultT = std::optional<std::invoke_result_t<F>>; + Common::OneShotEvent task_complete; + std::optional<decltype(functor())> result; - // If we queue up a functor on the current thread, it won't run until we return to the event loop, - // which means waiting for it to finish will never complete. Instead, run it immediately. - if (object->thread() == QThread::currentThread()) - return OptionalResultT(functor()); + QMetaObject::invokeMethod( + object, [&, guard = Common::ScopedSetter{&task_complete}] { result = functor(); }); - class FnInvokeEvent : public QEvent - { - public: - FnInvokeEvent(F&& functor, QObject* obj, Common::Event& event, OptionalResultT& result) - : QEvent(QEvent::None), m_func(std::move(functor)), m_obj(obj), m_event(event), - m_result(result) - { - } - - ~FnInvokeEvent() - { - if (m_obj) - { - m_result = m_func(); - } - else - { - // is already nullopt - } - m_event.Set(); - } - - private: - F m_func; - QPointer<QObject> m_obj; - Common::Event& m_event; - OptionalResultT& m_result; - }; - - Common::Event event{}; - OptionalResultT result = std::nullopt; - QCoreApplication::postEvent(object, - new FnInvokeEvent(std::forward<F>(functor), object, event, result)); - event.Wait(); + // Wait for the lambda to go out of scope. The result may or may not have been assigned. + task_complete.Wait(); return result; } -template <typename Base, typename Type, typename Receiver> -auto RunOnObject(Receiver* obj, Type Base::* func) +template <std::derived_from<QObject> Receiver> +auto RunOnObject(Receiver* obj, auto Receiver::* func) { - return RunOnObject(obj, [obj, func] { return (obj->*func)(); }); + return RunOnObject(obj, std::bind(func, obj)); } |
