From 099c74481d1bd9038a1f10c3387aaa505186fbc3 Mon Sep 17 00:00:00 2001 From: Eladash Date: Fri, 29 Sep 2023 02:27:45 +0300 Subject: [PATCH] Savestates: Optimize SPU pausing --- rpcs3/Emu/CPU/CPUThread.cpp | 142 +++++++++++++++++++++++++++--------- rpcs3/Emu/System.cpp | 79 ++++++++++++++++---- rpcs3/Emu/System.h | 10 ++- 3 files changed, 180 insertions(+), 51 deletions(-) diff --git a/rpcs3/Emu/CPU/CPUThread.cpp b/rpcs3/Emu/CPU/CPUThread.cpp index aecf9f2a24..00265ecd37 100644 --- a/rpcs3/Emu/CPU/CPUThread.cpp +++ b/rpcs3/Emu/CPU/CPUThread.cpp @@ -1365,10 +1365,33 @@ u32 CPUDisAsm::DisAsmBranchTarget(s32 /*imm*/) extern bool try_lock_spu_threads_in_a_state_compatible_with_savestates(bool revert_lock) { - const u64 start = get_system_time(); + auto get_spus = [old_counter = u64{umax}, spu_list = std::vector>>()](bool can_collect) mutable + { + const u64 new_counter = cpu_thread::g_threads_created + cpu_thread::g_threads_deleted; + + if (old_counter != new_counter) + { + if (!can_collect) + { + return decltype(&spu_list){}; + } + + // Fetch SPU contexts + spu_list.clear(); + + idm::select>([&](u32 id, spu_thread&) + { + spu_list.emplace_back(ensure(idm::get_unlocked>(id))); + }); + + old_counter = new_counter; + } + + return &spu_list; + }; // Attempt to lock for half a second, if somehow takes longer abort it - do + for (u64 start = 0, passed_count = 0; passed_count < 10; std::this_thread::yield()) { if (revert_lock) { @@ -1376,27 +1399,75 @@ extern bool try_lock_spu_threads_in_a_state_compatible_with_savestates(bool reve break; } - if (cpu_thread::suspend_all(nullptr, {}, []() + if (!start) { - return idm::select>([](u32, spu_thread& spu) - { - if (!spu.unsavable) - { - if (Emu.IsPaused()) - { - // If emulation is paused, we can only hope it's already in a state compatible with savestates - return !!(spu.state & (cpu_flag::dbg_global_pause + cpu_flag::dbg_pause)); - } - else - { - ensure(!spu.state.test_and_set(cpu_flag::dbg_global_pause)); - } + start = get_system_time(); + } + else if (get_system_time() - start >= 100'000) + { + passed_count++; + start = 0; + continue; + } - return false; + // Try to fetch SPUs out of critical section + const auto spu_list = get_spus(true); + + // Avoid using suspend_all when more than one thread is known to be unsavable + u32 unsavable_threads = 0; + + for (auto& spu : *spu_list) + { + if (spu->unsavable && !spu->state.all_of(cpu_flag::wait + cpu_flag::exit)) + { + unsavable_threads++; + + if (unsavable_threads >= 3) + { + break; + } + } + } + + if (unsavable_threads >= 3) + { + continue; + } + + // Flag for optimization + bool paused_anyone = false; + + if (cpu_thread::suspend_all(nullptr, {}, [&]() + { + if (!get_spus(false)) + { + // Avoid locking IDM here because this is a critical section + return true; + } + + for (auto& spu : *spu_list) + { + if (spu->unsavable && !spu->state.all_of(cpu_flag::wait + cpu_flag::exit)) + { + return true; } - return true; - }).ret; + if (Emu.IsPaused()) + { + // If emulation is paused, we can only hope it's already in a state compatible with savestates + if (!(spu->state & (cpu_flag::dbg_global_pause + cpu_flag::dbg_pause))) + { + return true; + } + } + else + { + paused_anyone = true; + ensure(!spu->state.test_and_set(cpu_flag::dbg_global_pause)); + } + } + + return false; })) { if (Emu.IsPaused()) @@ -1404,36 +1475,39 @@ extern bool try_lock_spu_threads_in_a_state_compatible_with_savestates(bool reve return false; } - // It's faster to lock once - reader_lock lock(id_manager::g_mutex); - - idm::select>([](u32, spu_thread& spu) + if (!paused_anyone) { - spu.state -= cpu_flag::dbg_global_pause; - }, idm::unlocked); + // Need not do anything + continue; + } // For faster signalling, first remove state flags then batch notifications - idm::select>([](u32, spu_thread& spu) + for (auto& spu : *spu_list) { - if (spu.state & cpu_flag::wait) + spu->state -= cpu_flag::dbg_global_pause; + } + + for (auto& spu : *spu_list) + { + if (spu->state & cpu_flag::wait) { - spu.state.notify_one(); + spu->state.notify_one(); } - }, idm::unlocked); + } continue; } return true; - } while (std::this_thread::yield(), get_system_time() - start <= 500'000); + } - idm::select>([&](u32, named_thread& spu) + for (auto& spu : *get_spus(true)) { - if (spu.state.test_and_reset(cpu_flag::dbg_global_pause)) + if (spu->state.test_and_reset(cpu_flag::dbg_global_pause)) { - spu.state.notify_one(); + spu->state.notify_one(); } - }); + }; return false; } diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 24a9dcca8c..a90b45f864 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -2628,23 +2628,73 @@ void Emulator::GracefulShutdown(bool allow_autoexit, bool async_op, bool savesta extern bool try_lock_vdec_context_creation(); extern bool try_lock_spu_threads_in_a_state_compatible_with_savestates(bool revert_lock = false); -void Emulator::Kill(bool allow_autoexit, bool savestate) +void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_stage) { - if (!IsStopped() && savestate && !try_lock_spu_threads_in_a_state_compatible_with_savestates()) + static const auto make_ptr = [](auto ptr) { - sys_log.error("Failed to savestate: failed to lock SPU threads execution."); - } + return std::shared_ptr>(ptr); + }; - if (!IsStopped() && savestate && !try_lock_vdec_context_creation()) + if (!IsStopped() && savestate) { - try_lock_spu_threads_in_a_state_compatible_with_savestates(true); + if (!save_stage || !save_stage->prepared) + { + if (m_savestate_pending.exchange(true)) + { + return; + } - sys_log.error("Failed to savestate: HLE VDEC (video decoder) context(s) exist." - "\nLLE libvdec.sprx by selecting it in Advanced tab -> Firmware Libraries." - "\nYou need to close the game for it to take effect." - "\nIf you cannot close the game due to losing important progress, your best chance is to skip the current cutscenes if any are played and retry."); + std::shared_ptr> pause_thread = std::make_shared>(); - return; + *pause_thread = make_ptr(new named_thread("Savestate Prepare Thread"sv, [pause_thread, allow_autoexit, this]() mutable + { + if (!try_lock_spu_threads_in_a_state_compatible_with_savestates()) + { + sys_log.error("Failed to savestate: failed to lock SPU threads execution."); + m_savestate_pending = false; + + CallFromMainThread([pause = std::move(pause_thread)]() + { + // Join thread + }, nullptr, false); + + return; + } + + if (!IsStopped() && !try_lock_vdec_context_creation()) + { + try_lock_spu_threads_in_a_state_compatible_with_savestates(true); + + sys_log.error("Failed to savestate: HLE VDEC (video decoder) context(s) exist." + "\nLLE libvdec.sprx by selecting it in Advanced tab -> Firmware Libraries." + "\nYou need to close the game for it to take effect." + "\nIf you cannot close the game due to losing important progress, your best chance is to skip the current cutscenes if any are played and retry."); + + m_savestate_pending = false; + + CallFromMainThread([pause = std::move(pause_thread)]() + { + // Join thread + }, nullptr, false); + + return; + } + + CallFromMainThread([allow_autoexit, this]() + { + savestate_stage stage{}; + stage.prepared = true; + Kill(allow_autoexit, true, &stage); + }); + + CallFromMainThread([pause = std::move(pause_thread)]() + { + // Join thread + }, nullptr, false); + })); + + return; + } } g_tls_log_prefix = []() @@ -2683,6 +2733,7 @@ void Emulator::Kill(bool allow_autoexit, bool savestate) m_config_mode = cfg_mode::custom; read_used_savestate_versions(); m_savestate_extension_flags1 = {}; + m_savestate_pending = false; // Enable logging rpcs3::utils::configure_logs(true); @@ -2729,11 +2780,6 @@ void Emulator::Kill(bool allow_autoexit, bool savestate) // There is no race condition because it is only accessed by the same thread std::shared_ptr> join_thread = std::make_shared>(); - auto make_ptr = [](auto ptr) - { - return std::shared_ptr>(ptr); - }; - *join_thread = make_ptr(new named_thread("Emulation Join Thread"sv, [join_thread, savestate, allow_autoexit, this]() mutable { named_thread stop_watchdog("Stop Watchdog"sv, [this]() @@ -3081,6 +3127,7 @@ void Emulator::Kill(bool allow_autoexit, bool savestate) m_ar.reset(); read_used_savestate_versions(); m_savestate_extension_flags1 = {}; + m_savestate_pending = false; // Complete the operation m_state = system_state::stopped; diff --git a/rpcs3/Emu/System.h b/rpcs3/Emu/System.h index 6a8c9595ce..cff9c88902 100644 --- a/rpcs3/Emu/System.h +++ b/rpcs3/Emu/System.h @@ -108,6 +108,7 @@ class Emulator final atomic_t m_pause_start_time{0}; // set when paused atomic_t m_pause_amend_time{0}; // increased when resumed atomic_t m_stop_ctr{1}; // Increments when emulation is stopped + atomic_t m_savestate_pending = false; games_config m_games_config; @@ -331,10 +332,17 @@ public: void FixGuestTime(); void FinalizeRunRequest(); +private: + struct savestate_stage + { + bool prepared = false; + }; +public: + bool Pause(bool freeze_emulation = false, bool show_resume_message = true); void Resume(); void GracefulShutdown(bool allow_autoexit = true, bool async_op = false, bool savestate = false); - void Kill(bool allow_autoexit = true, bool savestate = false); + void Kill(bool allow_autoexit = true, bool savestate = false, savestate_stage* stage = nullptr); game_boot_result Restart(bool graceful = true); bool Quit(bool force_quit); static void CleanUp();