[UI] Loop thread to main thread WindowedAppContext
This commit is contained in:
180
src/xenia/ui/windowed_app_context.cc
Normal file
180
src/xenia/ui/windowed_app_context.cc
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* 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. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/ui/windowed_app_context.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "xenia/base/assert.h"
|
||||
#include "xenia/base/threading.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
|
||||
WindowedAppContext::~WindowedAppContext() {
|
||||
// The UI thread is responsible for managing the lifetime of the context.
|
||||
assert_true(IsInUIThread());
|
||||
|
||||
// It's okay to destroy the context from a platform's internal UI loop
|
||||
// callback, primarily on platforms where the loop is run by the OS itself,
|
||||
// and the context can't be created and destroyed in a RAII way, rather, it's
|
||||
// created in an initialization handler and destroyed in a shutdown handler
|
||||
// called by the OS. However, destruction must not be done from within the
|
||||
// queued functions - as in this case, the pending function container, the
|
||||
// mutex, will be accessed after having been destroyed already.
|
||||
|
||||
// Make sure CallInUIThreadDeferred doesn't call
|
||||
// NotifyUILoopOfPendingFunctions, which is virtual.
|
||||
is_in_destructor_ = true;
|
||||
// Make sure the final ExecutePendingFunctionsFromUIThread doesn't call
|
||||
// PlatformQuitFromUIThread, which is virtual.
|
||||
has_quit_ = true;
|
||||
|
||||
// Platform-specific quit is expected to be performed by the subclass (the
|
||||
// order of it vs. the final ExecutePendingFunctionsFromUIThread shouldn't
|
||||
// matter anymore, the implementation may assume that no pending functions
|
||||
// will be requested for execution specifically via the platform-specific
|
||||
// loop, as there should be no more references to the context in other
|
||||
// threads), can't call the virtual PlatformQuitFromUIThread anymore.
|
||||
ExecutePendingFunctionsFromUIThread(true);
|
||||
}
|
||||
|
||||
bool WindowedAppContext::CallInUIThreadDeferred(
|
||||
std::function<void()> function) {
|
||||
{
|
||||
std::unique_lock<std::mutex> pending_functions_lock(
|
||||
pending_functions_mutex_);
|
||||
if (!pending_functions_accepted_) {
|
||||
// Will not be called as the loop will not be executed anymore.
|
||||
return false;
|
||||
}
|
||||
pending_functions_.emplace_back(std::move(function));
|
||||
}
|
||||
// Notify unconditionally, even if currently running pending functions. It's
|
||||
// possible for pending functions themselves to run inner platform message
|
||||
// loops, such as when displaying dialogs - in this case, the notification is
|
||||
// needed to run the new function from such an inner loop. A modal loop can be
|
||||
// started even in leftovers happening during the quit, where there's still
|
||||
// opportunity for enqueueing and executing new pending functions - so only
|
||||
// checking if called in the destructor (it's safe to check this without
|
||||
// locking a mutex as it's assumed that if the object is already being
|
||||
// destroyed, no other threads can have references to it - any access would
|
||||
// result in a race condition anyway) as the subclass has already been
|
||||
// destroyed. Having pending_functions_mutex_ unlocked also means that
|
||||
// NotifyUILoopOfPendingFunctions may be done while the UI thread is calling
|
||||
// or has already called PlatformQuitFromUIThread - but it's better than
|
||||
// keeping pending_functions_mutex_ locked as NotifyUILoopOfPendingFunctions
|
||||
// may be implemented as pushing to a fixed-size pipe, in which case it will
|
||||
// have to wait until free space is available, but if the UI thread tries to
|
||||
// lock the mutex afterwards to execute pending functions (and encouters
|
||||
// contention), nothing will be able to receive from the pipe anymore and thus
|
||||
// free the space, causing a deadlock.
|
||||
if (!is_in_destructor_) {
|
||||
NotifyUILoopOfPendingFunctions();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WindowedAppContext::CallInUIThread(std::function<void()> function) {
|
||||
if (IsInUIThread()) {
|
||||
// The intention is just to make sure the code is executed in the UI thread,
|
||||
// don't defer execution if no need to.
|
||||
function();
|
||||
return true;
|
||||
}
|
||||
return CallInUIThreadDeferred(std::move(function));
|
||||
}
|
||||
|
||||
bool WindowedAppContext::CallInUIThreadSynchronous(
|
||||
std::function<void()> function) {
|
||||
if (IsInUIThread()) {
|
||||
// Prevent deadlock if called from the UI thread.
|
||||
function();
|
||||
return true;
|
||||
}
|
||||
xe::threading::Fence fence;
|
||||
if (!CallInUIThreadDeferred([&function, &fence]() {
|
||||
function();
|
||||
fence.Signal();
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
fence.Wait();
|
||||
return true;
|
||||
}
|
||||
|
||||
void WindowedAppContext::QuitFromUIThread() {
|
||||
assert_true(IsInUIThread());
|
||||
bool has_quit_previously = has_quit_;
|
||||
// Make sure PlatformQuitFromUIThread is called only once, not from nested
|
||||
// pending function execution during the quit - otherwise it will be called
|
||||
// when it's still possible to add new pending functions. This isn't as wrong
|
||||
// as calling PlatformQuitFromUIThread from the destructor, but still a part
|
||||
// of the contract for simplicity.
|
||||
has_quit_ = true;
|
||||
// Executing pending function unconditionally because it's the contract of
|
||||
// this method that functions are executed immediately.
|
||||
ExecutePendingFunctionsFromUIThread(true);
|
||||
if (has_quit_previously) {
|
||||
// Potentially calling QuitFromUIThread from inside a pending function (in
|
||||
// the worst and dangerous case, from a pending function executed in the
|
||||
// destructor - and PlatformQuitFromUIThread is virtual).
|
||||
return;
|
||||
}
|
||||
// Call the platform-specific shutdown while letting it assume that no new
|
||||
// functions will be queued anymore (but NotifyUILoopOfPendingFunctions may
|
||||
// still be called after PlatformQuitFromUIThread as the two are not
|
||||
// interlocked). This is different than the order in the destruction, but
|
||||
// there this assumption is ensured by the expectation that there should be no
|
||||
// more references to the context in other threads that would allow queueing
|
||||
// new functions with calling NotifyUILoopOfPendingFunctions.
|
||||
PlatformQuitFromUIThread();
|
||||
}
|
||||
|
||||
void WindowedAppContext::ExecutePendingFunctionsFromUIThread(bool is_final) {
|
||||
assert_true(IsInUIThread());
|
||||
std::unique_lock<std::mutex> pending_functions_lock(pending_functions_mutex_);
|
||||
while (!pending_functions_.empty()) {
|
||||
// Removing the function from the queue before executing it, as the function
|
||||
// itself may call ExecutePendingFunctionsFromUIThread - if it's kept, the
|
||||
// inner loop will try to execute it again, resulting in potentially endless
|
||||
// recursion, and even if it's terminated, each level will be trying to
|
||||
// remove the same function from the queue - instead, actually removing
|
||||
// other functions, or even beyond the end of the queue.
|
||||
std::function<void()> function = std::move(pending_functions_.front());
|
||||
pending_functions_.pop_front();
|
||||
// Call the function with the lock released as it may take an indefinitely
|
||||
// long time to execute if it opens some dialog (possibly with its own
|
||||
// platform message loop), and in that case, without unlocking, no other
|
||||
// thread would be able to add new pending functions (which would result in
|
||||
// unintended waits for user input). This also allows using std::mutex
|
||||
// instead of std::recursive_mutex.
|
||||
pending_functions_lock.unlock();
|
||||
function();
|
||||
pending_functions_lock.lock();
|
||||
}
|
||||
if (is_final) {
|
||||
// Atomically with completion of the pending functions loop, disallow adding
|
||||
// new functions after executing the existing ones - it was possible to
|
||||
// enqueue new functions from the leftover ones as there still was
|
||||
// opportunity to call them, so it wasn't necessary to disallow adding
|
||||
// before executing, but now new functions will potentially never be
|
||||
// executed. This is done even if this is just an inner pending functions
|
||||
// execution and there's still potential possibility of adding and executing
|
||||
// new functions in the outer loops - for simplicity and consistency (so
|
||||
// QuitFromUIThread's behavior doesn't depend as much on the location of the
|
||||
// call - inside a pending function or from some system callback of the
|
||||
// window), assuming after a PlatformQuitFromUIThread call, it's not
|
||||
// possible to add new pending functions anymore.
|
||||
pending_functions_accepted_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
Reference in New Issue
Block a user