Core: Support asynchronously executing functions on the CPU thread

The CPU thread will be interrupted to execute the callback, and then
restored to its old state after execution completes.
This commit is contained in:
Stenzek 2019-06-29 18:18:24 +10:00
parent 29ba53f6c3
commit df45e714a3
4 changed files with 80 additions and 2 deletions

View File

@ -21,6 +21,7 @@
#include "Common/CPUDetect.h" #include "Common/CPUDetect.h"
#include "Common/CommonPaths.h" #include "Common/CommonPaths.h"
#include "Common/CommonTypes.h" #include "Common/CommonTypes.h"
#include "Common/Event.h"
#include "Common/FileUtil.h" #include "Common/FileUtil.h"
#include "Common/Flag.h" #include "Common/Flag.h"
#include "Common/Logging/LogManager.h" #include "Common/Logging/LogManager.h"
@ -110,6 +111,7 @@ struct HostJob
}; };
static std::mutex s_host_jobs_lock; static std::mutex s_host_jobs_lock;
static std::queue<HostJob> s_host_jobs_queue; static std::queue<HostJob> s_host_jobs_queue;
static Common::Event s_cpu_thread_job_finished;
static thread_local bool tls_is_cpu_thread = false; static thread_local bool tls_is_cpu_thread = false;
@ -433,6 +435,7 @@ static void EmuThread(std::unique_ptr<BootParameters> boot, WindowSystemInfo wsi
Common::ScopeGuard movie_guard{Movie::Shutdown}; Common::ScopeGuard movie_guard{Movie::Shutdown};
HW::Init(); HW::Init();
Common::ScopeGuard hw_guard{[] { Common::ScopeGuard hw_guard{[] {
// We must set up this flag before executing HW::Shutdown() // We must set up this flag before executing HW::Shutdown()
s_hardware_initialized = false; s_hardware_initialized = false;
@ -771,6 +774,45 @@ void RunAsCPUThread(std::function<void()> function)
PauseAndLock(false, was_unpaused); PauseAndLock(false, was_unpaused);
} }
void RunOnCPUThread(std::function<void()> function, bool wait_for_completion)
{
// If the CPU thread is not running, assume there is no active CPU thread we can race against.
if (!IsRunning() || IsCPUThread())
{
function();
return;
}
// Pause the CPU (set it to stepping mode).
const bool was_running = PauseAndLock(true, true);
// Queue the job function.
if (wait_for_completion)
{
// Trigger the event after executing the function.
s_cpu_thread_job_finished.Reset();
CPU::AddCPUThreadJob([&function]() {
function();
s_cpu_thread_job_finished.Set();
});
}
else
{
CPU::AddCPUThreadJob(std::move(function));
}
// Release the CPU thread, and let it execute the callback.
PauseAndLock(false, was_running);
// If we're waiting for completion, block until the event fires.
if (wait_for_completion)
{
// Periodically yield to the UI thread, so we don't deadlock.
while (!s_cpu_thread_job_finished.WaitFor(std::chrono::milliseconds(10)))
Host_YieldToUI();
}
}
// Display FPS info // Display FPS info
// This should only be called from VI // This should only be called from VI
void VideoThrottle() void VideoThrottle()

View File

@ -82,6 +82,10 @@ void UpdateTitle();
// This should only be called from the CPU thread or the host thread. // This should only be called from the CPU thread or the host thread.
void RunAsCPUThread(std::function<void()> function); void RunAsCPUThread(std::function<void()> function);
// Run a function on the CPU thread, asynchronously.
// This is only valid to call from the host thread, since it uses PauseAndLock() internally.
void RunOnCPUThread(std::function<void()> function, bool wait_for_completion);
// for calling back into UI code without introducing a dependency on it in core // for calling back into UI code without introducing a dependency on it in core
using StateChangedCallbackFunc = std::function<void(Core::State)>; using StateChangedCallbackFunc = std::function<void(Core::State)>;
void SetOnStateChangedCallback(StateChangedCallbackFunc callback); void SetOnStateChangedCallback(StateChangedCallbackFunc callback);

View File

@ -6,6 +6,7 @@
#include <condition_variable> #include <condition_variable>
#include <mutex> #include <mutex>
#include <queue>
#include "AudioCommon/AudioCommon.h" #include "AudioCommon/AudioCommon.h"
#include "Common/CommonTypes.h" #include "Common/CommonTypes.h"
@ -44,6 +45,7 @@ static bool s_state_paused_and_locked = false;
static bool s_state_system_request_stepping = false; static bool s_state_system_request_stepping = false;
static bool s_state_cpu_step_instruction = false; static bool s_state_cpu_step_instruction = false;
static Common::Event* s_state_cpu_step_instruction_sync = nullptr; static Common::Event* s_state_cpu_step_instruction_sync = nullptr;
static std::queue<std::function<void()>> s_pending_jobs;
void Init(PowerPC::CPUCore cpu_core) void Init(PowerPC::CPUCore cpu_core)
{ {
@ -60,6 +62,9 @@ void Shutdown()
// Requires holding s_state_change_lock // Requires holding s_state_change_lock
static void FlushStepSyncEventLocked() static void FlushStepSyncEventLocked()
{ {
if (!s_state_cpu_step_instruction)
return;
if (s_state_cpu_step_instruction_sync) if (s_state_cpu_step_instruction_sync)
{ {
s_state_cpu_step_instruction_sync->Set(); s_state_cpu_step_instruction_sync->Set();
@ -68,12 +73,25 @@ static void FlushStepSyncEventLocked()
s_state_cpu_step_instruction = false; s_state_cpu_step_instruction = false;
} }
static void ExecutePendingJobs(std::unique_lock<std::mutex>& state_lock)
{
while (!s_pending_jobs.empty())
{
auto callback = s_pending_jobs.front();
s_pending_jobs.pop();
state_lock.unlock();
callback();
state_lock.lock();
}
}
void Run() void Run()
{ {
std::unique_lock<std::mutex> state_lock(s_state_change_lock); std::unique_lock<std::mutex> state_lock(s_state_change_lock);
while (s_state != State::PowerDown) while (s_state != State::PowerDown)
{ {
s_state_cpu_cvar.wait(state_lock, [] { return !s_state_paused_and_locked; }); s_state_cpu_cvar.wait(state_lock, [] { return !s_state_paused_and_locked; });
ExecutePendingJobs(state_lock);
switch (s_state) switch (s_state)
{ {
@ -108,8 +126,10 @@ void Run()
case State::Stepping: case State::Stepping:
// Wait for step command. // Wait for step command.
s_state_cpu_cvar.wait(state_lock, s_state_cpu_cvar.wait(state_lock, [&state_lock] {
[] { return s_state_cpu_step_instruction || !IsStepping(); }); ExecutePendingJobs(state_lock);
return s_state_cpu_step_instruction || !IsStepping();
});
if (!IsStepping()) if (!IsStepping())
{ {
// Signal event if the mode changes. // Signal event if the mode changes.
@ -330,4 +350,11 @@ bool PauseAndLock(bool do_lock, bool unpause_on_unlock, bool control_adjacent)
} }
return was_unpaused; return was_unpaused;
} }
void AddCPUThreadJob(std::function<void()> function)
{
std::unique_lock<std::mutex> state_lock(s_state_change_lock);
s_pending_jobs.push(std::move(function));
}
} // namespace CPU } // namespace CPU

View File

@ -3,6 +3,7 @@
// Refer to the license.txt file included. // Refer to the license.txt file included.
#pragma once #pragma once
#include <functional>
namespace Common namespace Common
{ {
@ -74,4 +75,8 @@ const State* GetStatePtr();
// "control_adjacent" causes PauseAndLock to behave like EnableStepping by modifying the // "control_adjacent" causes PauseAndLock to behave like EnableStepping by modifying the
// state of the Audio and FIFO subsystems as well. // state of the Audio and FIFO subsystems as well.
bool PauseAndLock(bool do_lock, bool unpause_on_unlock = true, bool control_adjacent = false); bool PauseAndLock(bool do_lock, bool unpause_on_unlock = true, bool control_adjacent = false);
// Adds a job to be executed during on the CPU thread. This should be combined with PauseAndLock(),
// as while the CPU is in the run loop, it won't execute the function.
void AddCPUThreadJob(std::function<void()> function);
} // namespace CPU } // namespace CPU