Files
Xenia-Canary/src/xenia/ui/windowed_app_context.cc

181 lines
8.3 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. *
******************************************************************************
*/
#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