diff --git a/rpcs3/Emu/Cell/lv2/sys_process.cpp b/rpcs3/Emu/Cell/lv2/sys_process.cpp index 9a6369cd81..3a0a3323ed 100644 --- a/rpcs3/Emu/Cell/lv2/sys_process.cpp +++ b/rpcs3/Emu/Cell/lv2/sys_process.cpp @@ -425,7 +425,6 @@ void lv2_exitspawn(ppu_thread& ppu, std::vector& argv, std::vector< std::string path = vfs::get(argv[0]); std::string hdd1 = vfs::get("/dev_hdd1/"); - std::string old_config = Emu.GetUsedConfig(); const u128 klic = g_fxo->get().last_key(); @@ -457,27 +456,32 @@ void lv2_exitspawn(ppu_thread& ppu, std::vector& argv, std::vector< g_fxo->init(std::min(old_size - total_size, sdk_suggested_mem) + total_size); }; + Emu.after_kill_callback = [func = std::move(func), argv = std::move(argv), envp = std::move(envp), data = std::move(data), + disc = std::move(disc), path = std::move(path), hdd1 = std::move(hdd1), old_config = Emu.GetUsedConfig(), klic]() mutable + { + Emu.argv = std::move(argv); + Emu.envp = std::move(envp); + Emu.data = std::move(data); + Emu.disc = std::move(disc); + Emu.hdd1 = std::move(hdd1); + Emu.init_mem_containers = std::move(func); + + if (klic) + { + Emu.klic.emplace_back(klic); + } + + Emu.SetForceBoot(true); + + auto res = Emu.BootGame(path, "", true, cfg_mode::continuous, old_config); + + if (res != game_boot_result::no_errors) + { + sys_process.fatal("Failed to boot from exitspawn! (path=\"%s\", error=%s)", path, res); + } + }; + Emu.Kill(false); - Emu.argv = std::move(argv); - Emu.envp = std::move(envp); - Emu.data = std::move(data); - Emu.disc = std::move(disc); - Emu.hdd1 = std::move(hdd1); - Emu.init_mem_containers = std::move(func); - - if (klic) - { - Emu.klic.emplace_back(klic); - } - - Emu.SetForceBoot(true); - - auto res = Emu.BootGame(path, "", true, cfg_mode::continuous, old_config); - - if (res != game_boot_result::no_errors) - { - sys_process.fatal("Failed to boot from exitspawn! (path=\"%s\", error=%s)", path, res); - } }); // Wait for GUI thread diff --git a/rpcs3/Emu/RSX/Overlays/HomeMenu/overlay_home_menu_main_menu.cpp b/rpcs3/Emu/RSX/Overlays/HomeMenu/overlay_home_menu_main_menu.cpp index 4eddf98e9f..204e6efdbc 100644 --- a/rpcs3/Emu/RSX/Overlays/HomeMenu/overlay_home_menu_main_menu.cpp +++ b/rpcs3/Emu/RSX/Overlays/HomeMenu/overlay_home_menu_main_menu.cpp @@ -52,12 +52,15 @@ namespace rsx Emu.CallFromMainThread([suspend_mode]() { - Emu.Kill(false, true); - if (!suspend_mode) { - Emu.Restart(); + Emu.after_kill_callback = []() + { + Emu.Restart(); + }; } + + Emu.Kill(false, true); }); return page_navigation::exit; diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 190f5f9de8..34e8b9348a 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -177,10 +177,10 @@ void Emulator::BlockingCallFromMainThread(std::function&& func) const CallFromMainThread(std::move(func), &wake_up); - while (!wake_up && !IsStopped()) + while (!wake_up) { ensure(thread_ctrl::get_current()); - thread_ctrl::wait_on(wake_up, false); + wake_up.wait(false); } } @@ -635,6 +635,11 @@ std::string Emulator::GetBackgroundPicturePath() const bool Emulator::BootRsxCapture(const std::string& path) { + if (m_state != system_state::stopped) + { + return false; + } + fs::file in_file(path); if (!in_file) @@ -781,6 +786,11 @@ void Emulator::SetForceBoot(bool force_boot) game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, usz recursion_count) { + if (m_state != system_state::stopped) + { + return game_boot_result::still_running; + } + m_ar.reset(); { @@ -817,11 +827,6 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, } } - if (!IsStopped()) - { - Kill(); - } - if (!title_id.empty()) { m_title_id = title_id; @@ -2422,8 +2427,14 @@ void Emulator::GracefulShutdown(bool allow_autoexit, bool async_op, bool savesta { const auto old_state = m_state.load(); - if (old_state == system_state::stopped) + if (old_state == system_state::stopped || old_state == system_state::stopping) { + while (!async_op && m_state != system_state::stopped) + { + process_qt_events(); + std::this_thread::sleep_for(16ms); + } + return; } @@ -2438,6 +2449,13 @@ void Emulator::GracefulShutdown(bool allow_autoexit, bool async_op, bool savesta { // The callback has been rudely ignored, we have no other option but to force termination Kill(allow_autoexit && !savestate, savestate); + + while (!async_op && m_state != system_state::stopped) + { + process_qt_events(); + std::this_thread::sleep_for(16ms); + } + return; } @@ -2483,22 +2501,26 @@ void Emulator::GracefulShutdown(bool allow_autoexit, bool async_op, bool savesta else { perform_kill(); + + while (m_state != system_state::stopped) + { + process_qt_events(); + std::this_thread::sleep_for(16ms); + } } } extern bool try_lock_vdec_context_creation(); extern bool try_lock_spu_threads_in_a_state_compatible_with_savestates(bool revert_lock = false); -std::shared_ptr Emulator::Kill(bool allow_autoexit, bool savestate) +void Emulator::Kill(bool allow_autoexit, bool savestate) { - std::shared_ptr to_ar; - - if (savestate && !try_lock_spu_threads_in_a_state_compatible_with_savestates()) + if (!IsStopped() && savestate && !try_lock_spu_threads_in_a_state_compatible_with_savestates()) { sys_log.error("Failed to savestate: failed to lock SPU threads execution."); } - if (savestate && !try_lock_vdec_context_creation()) + if (!IsStopped() && savestate && !try_lock_vdec_context_creation()) { try_lock_spu_threads_in_a_state_compatible_with_savestates(true); @@ -2507,7 +2529,7 @@ std::shared_ptr Emulator::Kill(bool allow_autoexit, bool savestat "\nYou need to close the game for 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."); - return to_ar; + return; } g_tls_log_prefix = []() @@ -2515,8 +2537,23 @@ std::shared_ptr Emulator::Kill(bool allow_autoexit, bool savestat return std::string(); }; - if (m_state.exchange(system_state::stopped) == system_state::stopped) + if (system_state old_state = m_state.fetch_op([](system_state& state) { + if (state == system_state::stopping || state == system_state::stopped) + { + return false; + } + + state = system_state::stopping; + return true; + }).first; old_state <= system_state::stopping) + { + if (old_state == system_state::stopping) + { + // Termination is in progress + return; + } + // Ensure clean state m_ar.reset(); argv.clear(); @@ -2526,11 +2563,12 @@ std::shared_ptr Emulator::Kill(bool allow_autoexit, bool savestat klic.clear(); hdd1.clear(); init_mem_containers = nullptr; + after_kill_callback = nullptr; m_config_path.clear(); m_config_mode = cfg_mode::custom; read_used_savestate_versions(); m_savestate_extension_flags1 = {}; - return to_ar; + return; } sys_log.notice("Stopping emulator..."); @@ -2546,28 +2584,6 @@ std::shared_ptr Emulator::Kill(bool allow_autoexit, bool savestat } } - named_thread stop_watchdog("Stop Watchdog", [&]() - { - for (int i = 0; thread_ctrl::state() != thread_state::aborting;) - { - // We don't need accurate timekeeping, using clocks may interfere with debugging - if (i >= (savestate ? 2000 : 1000)) - { - // Total amount of waiting: about 5s - report_fatal_error("Stopping emulator took too long." - "\nSome thread has probably deadlocked. Aborting."); - } - - thread_ctrl::wait_for(5'000); - - if (!g_watchdog_hold_ctr) - { - // Don't count if there are still uninterruptable threads like PPU LLVM workers - i++; - } - } - }); - // Signal threads if (auto rsx = g_fxo->try_get()) @@ -2588,257 +2604,313 @@ std::shared_ptr Emulator::Kill(bool allow_autoexit, bool savestat // Wait fot newly created cpu_thread to see that emulation has been stopped id_manager::g_mutex.lock_unlock(); - GetCallbacks().on_stop(); + // Type-less smart pointer container for thread (cannot know its type with this approach) + // There is no race condition because it is only accessed by the same thread + std::shared_ptr> join_thread = std::make_shared>(); - // Join threads - for (const auto& [type, data] : *g_fxo) + auto make_ptr = [](auto ptr) { - if (type.stop) - { - type.stop(data, thread_state::finished); - } - } + return std::shared_ptr>(ptr); + }; - // Save it first for maximum timing accuracy - const u64 timestamp = get_timebased_time(); - - stop_watchdog = thread_state::aborting; - - sys_log.notice("All threads have been stopped."); - - if (savestate) + *join_thread = make_ptr(new named_thread("Emulation Join Thread"sv, [join_thread, savestate, allow_autoexit, this]() mutable { - to_ar = std::make_unique(); - - // Savestate thread - named_thread emu_state_cap_thread("Emu State Capture Thread", [&]() + named_thread stop_watchdog("Stop Watchdog"sv, [this]() { - g_tls_log_prefix = []() + const auto closed_sucessfully = std::make_shared>(false); + + for (int i = 0; thread_ctrl::state() != thread_state::aborting;) { - return fmt::format("Emu State Capture Thread: '%s'", g_tls_serialize_name); - }; - - auto& ar = *to_ar; - - read_used_savestate_versions(); // Reset version data - USING_SERIALIZATION_VERSION(global_version); - - // Avoid duplicating TAR object memory because it can be very large - auto save_tar = [&](const std::string& path) - { - ar(usz{}); // Reserve memory to be patched later with correct size - const usz old_size = ar.data.size(); - ar.data = tar_object::save_directory(path, std::move(ar.data)); - ar.seek_end(); - const usz tar_size = ar.data.size() - old_size; - std::memcpy(ar.data.data() + old_size - sizeof(usz), &tar_size, sizeof(usz)); - sys_log.success("Saved the contents of directory '%s' (size=0x%x)", path, tar_size); - }; - - auto save_hdd1 = [&]() - { - const std::string _path = vfs::get("/dev_hdd1"); - std::string_view path = _path; - - path = path.substr(0, path.find_last_not_of(fs::delim) + 1); - - ar(std::string(path.substr(path.find_last_of(fs::delim) + 1))); - - if (!_path.empty()) + // We don't need accurate timekeeping, using clocks may interfere with debugging + if (i >= 2000) { - save_tar(_path); - } - }; + // Total amount of waiting: about 10s + GetCallbacks().on_emulation_stop_no_response(closed_sucessfully, 10); - auto save_hdd0 = [&]() - { - if (g_cfg.savestate.save_disc_game_data) - { - const std::string path = vfs::get("/dev_hdd0/game/"); - - for (auto& entry : fs::dir(path)) + while (thread_ctrl::state() != thread_state::aborting) { - if (entry.is_directory && entry.name != "." && entry.name != "..") + thread_ctrl::wait_for(5'000); + } + + break; + } + + thread_ctrl::wait_for(5'000); + + if (!g_watchdog_hold_ctr) + { + // Don't count if there are still uninterruptable threads like PPU LLVM workers + i++; + } + } + + *closed_sucessfully = true; + }); + + // Join threads + for (const auto& [type, data] : *g_fxo) + { + if (type.stop) + { + type.stop(data, thread_state::finished); + } + } + + // Save it first for maximum timing accuracy + const u64 timestamp = get_timebased_time(); + + sys_log.notice("All threads have been stopped."); + + std::unique_ptr to_ar; + + if (savestate) + { + to_ar = std::make_unique(); + + // Savestate thread + named_thread emu_state_cap_thread("Emu State Capture Thread", [&]() + { + g_tls_log_prefix = []() + { + return fmt::format("Emu State Capture Thread: '%s'", g_tls_serialize_name); + }; + + auto& ar = *to_ar; + + read_used_savestate_versions(); // Reset version data + USING_SERIALIZATION_VERSION(global_version); + + // Avoid duplicating TAR object memory because it can be very large + auto save_tar = [&](const std::string& path) + { + ar(usz{}); // Reserve memory to be patched later with correct size + const usz old_size = ar.data.size(); + ar.data = tar_object::save_directory(path, std::move(ar.data)); + ar.seek_end(); + const usz tar_size = ar.data.size() - old_size; + std::memcpy(ar.data.data() + old_size - sizeof(usz), &tar_size, sizeof(usz)); + sys_log.success("Saved the contents of directory '%s' (size=0x%x)", path, tar_size); + }; + + auto save_hdd1 = [&]() + { + const std::string _path = vfs::get("/dev_hdd1"); + std::string_view path = _path; + + path = path.substr(0, path.find_last_not_of(fs::delim) + 1); + + ar(std::string(path.substr(path.find_last_of(fs::delim) + 1))); + + if (!_path.empty()) + { + save_tar(_path); + } + }; + + auto save_hdd0 = [&]() + { + if (g_cfg.savestate.save_disc_game_data) + { + const std::string path = vfs::get("/dev_hdd0/game/"); + + for (auto& entry : fs::dir(path)) { - if (auto res = psf::load(path + entry.name + "/PARAM.SFO"); res && /*!m_title_id.empty() &&*/ psf::get_string(res.sfo, "TITLE_ID") == m_title_id && psf::get_string(res.sfo, "CATEGORY") == "GD") + if (entry.is_directory && entry.name != "." && entry.name != "..") { - ar(entry.name); - save_tar(path + entry.name); + if (auto res = psf::load(path + entry.name + "/PARAM.SFO"); res && /*!m_title_id.empty() &&*/ psf::get_string(res.sfo, "TITLE_ID") == m_title_id && psf::get_string(res.sfo, "CATEGORY") == "GD") + { + ar(entry.name); + save_tar(path + entry.name); + } } } } + + ar(std::string{}); + }; + + ar("RPCS3SAV"_u64); + ar(std::endian::native == std::endian::little); + ar(g_cfg.savestate.state_inspection_mode.get()); + ar(std::array{}); // Reserved for future use + ar(usz{0}); // Offset of versioning data, to be overwritten at the end of saving + + if (auto dir = vfs::get("/dev_bdvd/PS3_GAME"); fs::is_dir(dir) && !fs::is_file(fs::get_parent_dir(dir) + "/PS3_DISC.SFB")) + { + // Fake /dev_bdvd/PS3_GAME detected, use HDD0 for m_path restoration + ensure(vfs::unmount("/dev_bdvd/PS3_GAME")); + ar(vfs::retrieve(m_path)); + ar(vfs::retrieve(disc)); + ensure(vfs::mount("/dev_bdvd/PS3_GAME", dir)); + } + else + { + ar(vfs::retrieve(m_path)); + ar(!m_title_id.empty() && !vfs::get("/dev_bdvd").empty() ? m_title_id : vfs::retrieve(disc)); } - ar(std::string{}); - }; + ar(klic.empty() ? std::array{} : std::bit_cast>(klic[0])); + ar(m_game_dir); + save_hdd1(); + save_hdd0(); + ar(std::array{}); // Reserved for future use + vm::save(ar); + g_fxo->save(ar); - ar("RPCS3SAV"_u64); - ar(std::endian::native == std::endian::little); - ar(g_cfg.savestate.state_inspection_mode.get()); - ar(std::array{}); // Reserved for future use - ar(usz{0}); // Offset of versioning data, to be overwritten at the end of saving + bs_t extension_flags{SaveStateExtentionFlags1::SupportsMenuOpenResume}; - if (auto dir = vfs::get("/dev_bdvd/PS3_GAME"); fs::is_dir(dir) && !fs::is_file(fs::get_parent_dir(dir) + "/PS3_DISC.SFB")) + if (g_fxo->get().active) + { + extension_flags += SaveStateExtentionFlags1::ShouldCloseMenu; + } + + ar(extension_flags); + + ar(std::array{}); // Reserved for future use + ar(timestamp); + }); + + // Join it + emu_state_cap_thread(); + + if (emu_state_cap_thread == thread_state::errored) { - // Fake /dev_bdvd/PS3_GAME detected, use HDD0 for m_path restoration - ensure(vfs::unmount("/dev_bdvd/PS3_GAME")); - ar(vfs::retrieve(m_path)); - ar(vfs::retrieve(disc)); - ensure(vfs::mount("/dev_bdvd/PS3_GAME", dir)); + sys_log.error("Saving savestate failed due to fatal error!"); + to_ar.reset(); + savestate = false; + } + } + + stop_watchdog = thread_state::aborting; + + if (savestate) + { + const std::string path = get_savestate_path(m_title_id, m_path); + + fs::pending_file file(path); + + // Identifer -> version + std::vector> used_serial = read_used_savestate_versions(); + + auto& ar = *to_ar; + const usz pos = ar.seek_end(); + std::memcpy(&ar.data[10], &pos, 8);// Set offset + ar(used_serial); + + if (!file.file || (file.file.write(ar.data), !file.commit())) + { + sys_log.error("Failed to write savestate to file! (path='%s', %s)", path, fs::g_tls_error); + savestate = false; } else { - ar(vfs::retrieve(m_path)); - ar(!m_title_id.empty() && !vfs::get("/dev_bdvd").empty() ? m_title_id : vfs::retrieve(disc)); + std::string old_path = path.substr(0, path.find_last_not_of(fs::delim)); + std::string old_path2 = old_path; + + old_path2.insert(old_path.find_last_of(fs::delim) + 1, "old-"sv); + old_path.insert(old_path.find_last_of(fs::delim) + 1, "used_"sv); + + if (fs::remove_file(old_path)) + { + sys_log.success("Old savestate has been removed: path='%s'", old_path); + } + + // For backwards compatibility - avoid having loose files + if (fs::remove_file(old_path2)) + { + sys_log.success("Old savestate has been removed: path='%s'", old_path2); + } + + sys_log.success("Saved savestate! path='%s'", path); + + if (!g_cfg.savestate.suspend_emu) + { + // Allow to reboot from GUI + m_path = path; + } } - ar(klic.empty() ? std::array{} : std::bit_cast>(klic[0])); - ar(m_game_dir); - save_hdd1(); - save_hdd0(); - ar(std::array{}); // Reserved for future use - vm::save(ar); - g_fxo->save(ar); + ar.set_reading_state(); + } - bs_t extension_flags{SaveStateExtentionFlags1::SupportsMenuOpenResume}; + // Final termination from main thread (move the last ownership of join thread in order to destroy it) + CallFromMainThread([join_thread = std::move(join_thread), allow_autoexit, this]() mutable + { + cpu_thread::cleanup(); - if (g_fxo->get().active) + initialize_timebased_time(0, true); + + lv2_obj::cleanup(); + + g_fxo->reset(); + + sys_log.notice("Objects cleared..."); + + vm::close(); + + jit_runtime::finalize(); + + perf_stat_base::report(); + + static u64 aw_refs = 0; + static u64 aw_colm = 0; + static u64 aw_colc = 0; + static u64 aw_used = 0; + + aw_refs = 0; + aw_colm = 0; + aw_colc = 0; + aw_used = 0; + + atomic_wait::parse_hashtable([](u64 /*id*/, u32 refs, u64 ptr, u32 maxc) -> bool { - extension_flags += SaveStateExtentionFlags1::ShouldCloseMenu; + aw_refs += refs != 0; + aw_used += ptr != 0; + + aw_colm = std::max(aw_colm, maxc); + aw_colc += maxc != 0; + + return false; + }); + + sys_log.notice("Atomic wait hashtable stats: [in_use=%u, used=%u, max_collision_weight=%u, total_collisions=%u]", aw_refs, aw_used, aw_colm, aw_colc); + + m_stop_ctr++; + m_stop_ctr.notify_all(); + + // Boot arg cleanup (preserved in the case restarting) + argv.clear(); + envp.clear(); + data.clear(); + disc.clear(); + klic.clear(); + hdd1.clear(); + init_mem_containers = nullptr; + m_config_path.clear(); + m_config_mode = cfg_mode::custom; + m_ar.reset(); + read_used_savestate_versions(); + m_savestate_extension_flags1 = {}; + + // Complete the operation + m_state = system_state::stopped; + GetCallbacks().on_stop(); + + // Always Enable display sleep, not only if it was prevented. + enable_display_sleep(); + + if (allow_autoexit) + { + Quit(g_cfg.misc.autoexit.get()); } - ar(extension_flags); - - ar(std::array{}); // Reserved for future use - ar(timestamp); + if (after_kill_callback) + { + after_kill_callback(); + after_kill_callback = nullptr; + } }); - - // Join it - emu_state_cap_thread(); - - if (emu_state_cap_thread == thread_state::errored) - { - sys_log.error("Saving savestate failed due to fatal error!"); - to_ar.reset(); - savestate = false; - } - } - - cpu_thread::cleanup(); - - initialize_timebased_time(0, true); - - lv2_obj::cleanup(); - - g_fxo->reset(); - - sys_log.notice("Objects cleared..."); - - vm::close(); - - jit_runtime::finalize(); - - perf_stat_base::report(); - - static u64 aw_refs = 0; - static u64 aw_colm = 0; - static u64 aw_colc = 0; - static u64 aw_used = 0; - - aw_refs = 0; - aw_colm = 0; - aw_colc = 0; - aw_used = 0; - - atomic_wait::parse_hashtable([](u64 /*id*/, u32 refs, u64 ptr, u32 maxc) -> bool - { - aw_refs += refs != 0; - aw_used += ptr != 0; - - aw_colm = std::max(aw_colm, maxc); - aw_colc += maxc != 0; - - return false; - }); - - sys_log.notice("Atomic wait hashtable stats: [in_use=%u, used=%u, max_collision_weight=%u, total_collisions=%u]", aw_refs, aw_used, aw_colm, aw_colc); - - m_stop_ctr++; - m_stop_ctr.notify_all(); - - if (savestate) - { - const std::string path = get_savestate_path(m_title_id, m_path); - - fs::pending_file file(path); - - // Identifer -> version - std::vector> used_serial = read_used_savestate_versions(); - - auto& ar = *to_ar; - const usz pos = ar.seek_end(); - std::memcpy(&ar.data[10], &pos, 8);// Set offset - ar(used_serial); - - if (!file.file || (file.file.write(ar.data), !file.commit())) - { - sys_log.error("Failed to write savestate to file! (path='%s', %s)", path, fs::g_tls_error); - savestate = false; - } - else - { - std::string old_path = path.substr(0, path.find_last_not_of(fs::delim)); - std::string old_path2 = old_path; - - old_path2.insert(old_path.find_last_of(fs::delim) + 1, "old-"sv); - old_path.insert(old_path.find_last_of(fs::delim) + 1, "used_"sv); - - if (fs::remove_file(old_path)) - { - sys_log.success("Old savestate has been removed: path='%s'", old_path); - } - - // For backwards compatibility - avoid having loose files - if (fs::remove_file(old_path2)) - { - sys_log.success("Old savestate has been removed: path='%s'", old_path2); - } - - sys_log.success("Saved savestate! path='%s'", path); - - if (!g_cfg.savestate.suspend_emu) - { - // Allow to reboot from GUI - m_path = path; - } - } - - ar.set_reading_state(); - } - - // Boot arg cleanup (preserved in the case restarting) - argv.clear(); - envp.clear(); - data.clear(); - disc.clear(); - klic.clear(); - hdd1.clear(); - init_mem_containers = nullptr; - m_config_path.clear(); - m_config_mode = cfg_mode::custom; - m_ar.reset(); - read_used_savestate_versions(); - m_savestate_extension_flags1 = {}; - - // Always Enable display sleep, not only if it was prevented. - enable_display_sleep(); - - if (allow_autoexit) - { - Quit(g_cfg.misc.autoexit.get()); - } - - return to_ar; + })); } game_boot_result Emulator::Restart(bool graceful) diff --git a/rpcs3/Emu/System.h b/rpcs3/Emu/System.h index a06ed67653..00ccaf717f 100644 --- a/rpcs3/Emu/System.h +++ b/rpcs3/Emu/System.h @@ -21,6 +21,7 @@ enum class video_renderer; enum class system_state : u32 { stopped, + stopping, running, paused, frozen, // paused but cannot resume @@ -43,6 +44,7 @@ enum class game_boot_result : u32 unsupported_disc_type, savestate_corrupted, savestate_version_unsupported, + still_running, }; constexpr bool is_error(game_boot_result res) @@ -59,6 +61,7 @@ struct EmuCallbacks std::function on_stop; std::function on_ready; std::function on_missing_fw; + std::function>, int)> on_emulation_stop_no_response; std::function enable_disc_eject; std::function enable_disc_insert; std::function)> try_to_quit; // (force_quit, on_exit) Try to close RPCS3 @@ -215,6 +218,7 @@ public: std::string disc; std::string hdd1; std::function init_mem_containers; + std::function after_kill_callback; u32 m_boot_source_type = 0; // CELL_GAME_GAMETYPE_SYS @@ -322,17 +326,17 @@ 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); - std::shared_ptr Kill(bool allow_autoexit = true, bool savestate = false); + void Kill(bool allow_autoexit = true, bool savestate = false); game_boot_result Restart(bool graceful = true); bool Quit(bool force_quit); static void CleanUp(); bool IsRunning() const { return m_state == system_state::running; } bool IsPaused() const { return m_state >= system_state::paused; } // ready/starting are also considered paused by this function - bool IsStopped() const { return m_state == system_state::stopped; } + bool IsStopped() const { return m_state <= system_state::stopping; } bool IsReady() const { return m_state == system_state::ready; } bool IsStarting() const { return m_state == system_state::starting; } - auto GetStatus(bool fixup = true) const { system_state state = m_state; return fixup && state == system_state::frozen ? system_state::paused : state; } + auto GetStatus(bool fixup = true) const { system_state state = m_state; return fixup && state == system_state::frozen ? system_state::paused : fixup && state == system_state::stopping ? system_state::stopped : state; } bool HasGui() const { return m_has_gui; } void SetHasGui(bool has_gui) { m_has_gui = has_gui; } diff --git a/rpcs3/headless_application.cpp b/rpcs3/headless_application.cpp index c1c2f50e6b..a7bbdee44b 100644 --- a/rpcs3/headless_application.cpp +++ b/rpcs3/headless_application.cpp @@ -10,6 +10,8 @@ #include +[[noreturn]] void report_fatal_error(std::string_view text, bool is_html = false, bool include_help_text = true); + // For now, a trivial constructor/destructor. May add command line usage later. headless_application::headless_application(int& argc, char** argv) : QCoreApplication(argc, argv) { @@ -137,6 +139,14 @@ void headless_application::InitializeCallbacks() callbacks.on_resume = []() {}; callbacks.on_stop = []() {}; callbacks.on_ready = []() {}; + callbacks.on_emulation_stop_no_response = [](std::shared_ptr> closed_successfully, int /*seconds_waiting_already*/) + { + if (!closed_successfully || !*closed_successfully) + { + report_fatal_error(tr("Stopping emulator took too long." + "\nSome thread has probably deadlocked. Aborting.").toStdString()); + } + }; callbacks.enable_disc_eject = [](bool) {}; callbacks.enable_disc_insert = [](bool) {}; diff --git a/rpcs3/rpcs3qt/gs_frame.cpp b/rpcs3/rpcs3qt/gs_frame.cpp index 22ea2dbabd..44b927738a 100644 --- a/rpcs3/rpcs3qt/gs_frame.cpp +++ b/rpcs3/rpcs3qt/gs_frame.cpp @@ -605,10 +605,13 @@ void gs_frame::close() if (!Emu.IsStopped()) { // Blocking shutdown request. Obsolete, but I'm keeping it here as last resort. - Emu.GracefulShutdown(true, false); + Emu.after_kill_callback = [this](){ deleteLater(); }; + Emu.GracefulShutdown(true); + } + else + { + deleteLater(); } - - deleteLater(); }); } diff --git a/rpcs3/rpcs3qt/gui_application.cpp b/rpcs3/rpcs3qt/gui_application.cpp index fccca602de..685ed4d7fa 100644 --- a/rpcs3/rpcs3qt/gui_application.cpp +++ b/rpcs3/rpcs3qt/gui_application.cpp @@ -49,6 +49,8 @@ LOG_CHANNEL(gui_log, "GUI"); +[[noreturn]] void report_fatal_error(std::string_view text, bool is_html = false, bool include_help_text = true); + gui_application::gui_application(int& argc, char** argv) : QApplication(argc, argv) { } @@ -569,6 +571,61 @@ void gui_application::InitializeCallbacks() }; } + callbacks.on_emulation_stop_no_response = [this](std::shared_ptr> closed_successfully, int seconds_waiting_already) + { + const std::string terminate_message = tr("Stopping emulator took too long." + "\nSome thread has probably deadlocked. Aborting.").toStdString(); + + if (!closed_successfully) + { + report_fatal_error(terminate_message); + } + + Emu.CallFromMainThread([this, closed_successfully, seconds_waiting_already, terminate_message] + { + const auto seconds = std::make_shared(seconds_waiting_already); + + QMessageBox* mb = new QMessageBox(); + mb->setWindowTitle(tr("PS3 Game/Application Is Unresponsive")); + mb->setIcon(QMessageBox::Critical); + mb->setStandardButtons(QMessageBox::Yes | QMessageBox::No); + mb->setDefaultButton(QMessageBox::No); + mb->button(QMessageBox::Yes)->setText(tr("Terminate RPCS3")); + mb->button(QMessageBox::No)->setText(tr("Keep Waiting")); + + QString text_base = tr("Waiting for %0 second(s) already to stop emulation without success." + "\nKeep waiting or terminate RPCS3 unsafely at your own risk?"); + + mb->setText(text_base.arg(10)); + mb->layout()->setSizeConstraint(QLayout::SetFixedSize); + mb->setAttribute(Qt::WA_DeleteOnClose); + + QTimer* update_timer = new QTimer(mb); + + connect(update_timer, &QTimer::timeout, [mb, seconds, text_base, closed_successfully]() + { + *seconds += 1; + mb->setText(text_base.arg(*seconds)); + + if (*closed_successfully) + { + mb->reject(); + } + }); + + connect(mb, &QDialog::accepted, mb, [closed_successfully, terminate_message] + { + if (!*closed_successfully) + { + report_fatal_error(terminate_message); + } + }); + + mb->open(); + update_timer->start(1000); + }); + }; + Emu.SetCallbacks(std::move(callbacks)); } diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 76f43b28da..4b46a770fa 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -439,6 +439,9 @@ void main_window::show_boot_error(game_boot_result status) case game_boot_result::savestate_version_unsupported: message = tr("Savestate versioning data differes from your RPCS3 build."); break; + case game_boot_result::still_running: + message = tr("A game or PS3 application is still running or has yet to be fully stopped."); + break; case game_boot_result::firmware_missing: // Handled elsewhere case game_boot_result::no_errors: return; @@ -1854,8 +1857,6 @@ void main_window::OnEmuStop() const QString title = GetCurrentTitle(); const QString play_tooltip = tr("Play %0").arg(title); - m_debugger_frame->UpdateUI(); - ui->sysPauseAct->setText(tr("&Play")); ui->sysPauseAct->setIcon(m_icon_play); #ifdef _WIN32