185 lines
9.5 KiB
C++
185 lines
9.5 KiB
C++
/**
|
|
******************************************************************************
|
|
* Xenia : Xbox 360 Emulator Research Project *
|
|
******************************************************************************
|
|
* Copyright 2021 Ben Vanik. All rights reserved. *
|
|
* Released under the BSD license - see LICENSE in the root for more details. *
|
|
******************************************************************************
|
|
*/
|
|
|
|
#ifndef XENIA_UI_WINDOWED_APP_CONTEXT_H_
|
|
#define XENIA_UI_WINDOWED_APP_CONTEXT_H_
|
|
|
|
#include <cstdint>
|
|
#include <deque>
|
|
#include <functional>
|
|
#include <mutex>
|
|
#include <thread>
|
|
|
|
#include "xenia/base/assert.h"
|
|
|
|
namespace xe {
|
|
namespace ui {
|
|
|
|
// Context for inputs provided by the entry point and interacting with the
|
|
// platform's UI loop, to be implemented by platforms.
|
|
class WindowedAppContext {
|
|
public:
|
|
WindowedAppContext(const WindowedAppContext& context) = delete;
|
|
WindowedAppContext& operator=(const WindowedAppContext& context) = delete;
|
|
virtual ~WindowedAppContext();
|
|
|
|
// The thread where the object is created will be assumed to be the UI thread,
|
|
// for the purpose of being able to perform CallInUIThreadSynchronous before
|
|
// running the loop.
|
|
bool IsInUIThread() const {
|
|
return std::this_thread::get_id() == ui_thread_id_;
|
|
}
|
|
|
|
// CallInUIThreadDeferred and CallInUIThread are fire and forget - will be
|
|
// executed at some point the future when the UI thread is running the loop
|
|
// and is not busy doing other things.
|
|
// Therefore, references to objects in the function may outlive the owners of
|
|
// those objects, so use-after-free is very easy to create if not being
|
|
// careful enough.
|
|
// There are two solutions to this issue:
|
|
// - Signaling a fence in the function, awaiting it before destroying objects
|
|
// referenced by the function (works for shutdown from non-UI threads, or
|
|
// for CallInUIThread, but not CallInUIThreadDeferred, in the UI thread).
|
|
// - Calling ExecutePendingFunctionsFromUIThread in the UI thread before
|
|
// destroying objects referenced by the function (works for shutdown from
|
|
// the UI thread, though CallInUIThreadSynchronous doing
|
|
// ExecutePendingFunctionsFromUIThread is also an option for shutdown from
|
|
// any thread).
|
|
// (These are not required if all the called function is doing is triggering a
|
|
// quit with the context pointer captured by value, as the only object
|
|
// involved will be the context itself, with a pointer that is valid until
|
|
// it's destroyed - the most late location of the pending function execution
|
|
// possible.)
|
|
// Returning true if the function has been enqueued (it will be called at some
|
|
// point, at worst, before exiting the loop) or called immediately, false if
|
|
// it was dropped (if calling after exiting the loop).
|
|
// It's okay to enqueue functions from queued functions already being
|
|
// executed. As execution may be happening during the destruction of the
|
|
// context as well, in this case, CallInUIThreadDeferred must make sure it
|
|
// doesn't call anything virtual in the destructor.
|
|
|
|
// Enqueues the function regardless of the current thread. Won't necessarily
|
|
// be executed directly from the platform main loop as
|
|
// ExecutePendingFunctionsFromUIThread may be called anywhere (for instance,
|
|
// to await pending functions before destroying what they are referencing).
|
|
bool CallInUIThreadDeferred(std::function<void()> function);
|
|
// Executes the function immediately if already in the UI thread, enqueues it
|
|
// otherwise.
|
|
bool CallInUIThread(std::function<void()> function);
|
|
bool CallInUIThreadSynchronous(std::function<void()> function);
|
|
// It's okay to call this function from the queued functions themselves (such
|
|
// as in the case of waiting for a pending async CallInUIThreadDeferred
|
|
// described above - the wait may be done inside a CallInUIThreadSynchronous
|
|
// function safely).
|
|
void ExecutePendingFunctionsFromUIThread() {
|
|
ExecutePendingFunctionsFromUIThread(false);
|
|
}
|
|
|
|
// If on the target platform, the program itself is supposed to run the UI
|
|
// loop, this may be checked before doing blocking message waits as an
|
|
// additional safety measure beyond what PlatformQuitFromUIThread guarantees,
|
|
// and if true, the loop should be terminated (pending function will already
|
|
// have been executed). This doesn't imply that pending functions have been
|
|
// executed in all contexts, however - they can be executing from quitting
|
|
// itself (in the worst case, in the destructor where virtual methods can't be
|
|
// called), and in this case, this will be returning true.
|
|
bool HasQuitFromUIThread() const {
|
|
assert_true(IsInUIThread());
|
|
return has_quit_;
|
|
}
|
|
|
|
// Immediately disallows adding new pending UI functions, executes the already
|
|
// queued ones, and makes sure that the UI loop is aware that it was asked to
|
|
// stop running. This must not destroy the context or the app directly - the
|
|
// actual app shutdown will be initiated at some point outside the scope of
|
|
// app's callbacks. May call virtual functions - invoke
|
|
// ExecutePendingFunctionsFromUIThread(true) in the destructor instead, and
|
|
// use the platform-specific destructor to invoke the needed
|
|
// PlatformQuitFromUIThread logic (it should be safe, in case of destruction,
|
|
// to perform platform-specific quit request logic before the common part -
|
|
// NotifyUILoopOfPendingFunctions won't be called after the platform-specific
|
|
// quit request logic in this case anyway as destruction expects that there
|
|
// are no references to the object in other threads). Safe to call from within
|
|
// pending functions - requesting quit from non-UI threads is possible via
|
|
// methods like CallInUIThreadSynchronous. For deferred, rather than
|
|
// immediate, quitting from the UI thread, CallInUIThreadDeferred may be used
|
|
// (but the context pointer should be captured by value not to require
|
|
// explicit completion forcing in case the storage of the pointer is lost
|
|
// before the function is called).
|
|
void QuitFromUIThread();
|
|
// Callable from any thread. This is a special case where a completely
|
|
// fire-and-forget CallInUIThreadDeferred is safe, as the function only
|
|
// references nonvirtual functions of the context itself, and will be called
|
|
// at most from the destructor. No need to return the result - it doesn't
|
|
// matter if has quit already or not, as that's the intention anyway.
|
|
void RequestDeferredQuit() {
|
|
CallInUIThreadDeferred([this] { QuitFromUIThread(); });
|
|
}
|
|
|
|
protected:
|
|
WindowedAppContext() : ui_thread_id_(std::this_thread::get_id()) {}
|
|
|
|
// Can be called from any thread (including the UI thread) to ask the OS to
|
|
// run an iteration of the UI loop (with or without processing internal UI
|
|
// messages, this is platform-dependent) so pending functions will be executed
|
|
// at some point. pending_functions_mutex_ will not be locked when this is
|
|
// called (to make sure that, for example, the caller, waiting for space in
|
|
// the OS's message queue, such as in case of a Linux pipe, won't be blocking
|
|
// the UI thread that has started executing pending messages while pumping
|
|
// that pipe, resulting in a deadlock) - implementations don't directly see
|
|
// anything protected by it anyway, and a spurious notification shouldn't be
|
|
// causing any damage, this is similar to how condition variables can be
|
|
// signaled outside the critical section (signaling inside the critical
|
|
// section may also cause contention if the thread waiting is woken up quickly
|
|
// enough). This, however, means that NotifyUILoopOfPendingFunctions may be
|
|
// called in a non-UI thread after the final pending message processing
|
|
// followed by PlatformQuitFromUIThread in the UI thread - so it can still be
|
|
// called after a quit.
|
|
virtual void NotifyUILoopOfPendingFunctions() = 0;
|
|
|
|
// Called when requesting a quit in the UI thread to tell the platform that
|
|
// the UI loop needs to be terminated. The pending function queue is assumed
|
|
// to be empty before this is called, and no new pending functions can be
|
|
// added (but NotifyUILoopOfPendingFunctions may still be called as it's not
|
|
// mutually exclusive with PlatformQuitFromUIThread - if this matters, the
|
|
// platform implementation itself should be resolving this case).
|
|
virtual void PlatformQuitFromUIThread() = 0;
|
|
|
|
std::thread::id ui_thread_id_;
|
|
|
|
private:
|
|
// May be called with is_final == true from the destructor - must not call
|
|
// anything virtual in this case.
|
|
void ExecutePendingFunctionsFromUIThread(bool is_final);
|
|
|
|
// Accessible by the UI thread.
|
|
bool has_quit_ = false;
|
|
bool is_in_destructor_ = false;
|
|
|
|
// Synchronizes producers with each other and with the consumer, as well as
|
|
// all of them with the pending_functions_accepted_ variable which indicates
|
|
// whether shutdown has not been performed yet, as after the shutdown, no new
|
|
// functions will be executed anymore, therefore no new function should be
|
|
// queued, as it will never be called (thus CallInUIThreadSynchronous for it
|
|
// will never return, for instance).
|
|
std::mutex pending_functions_mutex_;
|
|
std::deque<std::function<void()>> pending_functions_;
|
|
// Protected by pending_functions_mutex_, writable by the UI thread, readable
|
|
// by any thread. Must be set to false before exiting the main platform loop,
|
|
// but before that, all pending functions must be executed no matter what, as
|
|
// they may need to signal fences currently being awaited (like the
|
|
// CallInUIThreadSynchronous fence).
|
|
bool pending_functions_accepted_ = true;
|
|
};
|
|
|
|
} // namespace ui
|
|
} // namespace xe
|
|
|
|
#endif // XENIA_UI_WINDOWED_APP_CONTEXT_H_
|