VMManager: Fix deadlock when asking to disable hardcore in fullscreen

This commit is contained in:
chaoticgd 2025-11-09 20:59:53 +00:00 committed by Ty
parent 0cd14c9919
commit 8cc28c25d8
7 changed files with 171 additions and 144 deletions

View File

@ -871,7 +871,7 @@ static void CPUThreadMain(VMBootParameters* params, std::atomic<int>* ret)
VMManager::ApplySettings();
GSDumpReplayer::SetIsDumpRunner(true);
if (VMManager::Initialize(*params))
if (VMManager::Initialize(*params) == VMBootResult::StartupSuccess)
{
// run until end
GSDumpReplayer::SetLoopCount(s_loop_count);

View File

@ -240,8 +240,6 @@ void EmuThread::startVM(std::shared_ptr<VMBootParameters> boot_params)
return;
}
pxAssertRel(!VMManager::HasValidVM(), "VM is shut down");
// Determine whether to start fullscreen or not.
m_is_rendering_to_main = shouldRenderToMain();
if (boot_params->fullscreen.has_value())
@ -249,22 +247,35 @@ void EmuThread::startVM(std::shared_ptr<VMBootParameters> boot_params)
else
m_is_fullscreen = Host::GetBaseBoolSettingValue("UI", "StartFullscreen", false);
if (!VMManager::Initialize(*boot_params))
return;
auto hardcore_disable_callback = [](std::string reason, VMBootRestartCallback restart_callback) {
QtHost::RunOnUIThread([reason = std::move(reason), restart_callback = std::move(restart_callback)]() {
QString title(Achievements::GetHardcoreModeDisableTitle());
QString text(QString::fromStdString(Achievements::GetHardcoreModeDisableText(reason.c_str())));
if (g_main_window->confirmMessage(title, text))
Host::RunOnCPUThread(restart_callback);
});
};
if (!Host::GetBoolSettingValue("UI", "StartPaused", false))
{
// This will come back and call OnVMResumed().
VMManager::SetState(VMState::Running);
}
else
{
// When starting paused, redraw the window, so there's at least something there.
redrawDisplayWindow();
Host::OnVMPaused();
}
auto done_callback = [](VMBootResult result) {
if (result != VMBootResult::StartupSuccess)
return;
m_event_loop->quit();
if (!Host::GetBoolSettingValue("UI", "StartPaused", false))
{
// This will come back and call OnVMResumed().
VMManager::SetState(VMState::Running);
}
else
{
// When starting paused, redraw the window, so there's at least something there.
g_emu_thread->redrawDisplayWindow();
Host::OnVMPaused();
}
g_emu_thread->getEventLoop()->quit();
};
VMManager::InitializeAsync(*boot_params, std::move(hardcore_disable_callback), std::move(done_callback));
}
void EmuThread::resetVM()

View File

@ -1903,62 +1903,19 @@ bool Achievements::ConfirmSystemReset()
return true;
}
bool Achievements::ConfirmHardcoreModeDisable(const char* trigger)
const char* Achievements::GetHardcoreModeDisableTitle()
{
#ifdef ENABLE_RAINTEGRATION
if (IsUsingRAIntegration())
return (RA_WarnDisableHardcore(trigger) != 0);
#endif
// I really hope this doesn't deadlock :/
const bool confirmed = Host::ConfirmMessage(TRANSLATE("Achievements", "Confirm Hardcore Mode"),
fmt::format(TRANSLATE_FS("Achievements", "{0} cannot be performed while hardcore mode is active. Do you "
"want to disable hardcore mode? {0} will be cancelled if you select No."),
trigger));
if (!confirmed)
return false;
DisableHardcoreMode();
return true;
return TRANSLATE("Achievements", "Confirm Hardcore Mode");
}
void Achievements::ConfirmHardcoreModeDisableAsync(const char* trigger, std::function<void(bool)> callback)
std::string Achievements::GetHardcoreModeDisableText(const char* reason)
{
#ifdef ENABLE_RAINTEGRATION
if (IsUsingRAIntegration())
{
const bool result = (RA_WarnDisableHardcore(trigger) != 0);
callback(result);
return;
}
#endif
MTGS::RunOnGSThread([trigger = TinyString(trigger), callback = std::move(callback)]() {
if (!FullscreenUI::Initialize())
{
Host::AddOSDMessage(fmt::format(TRANSLATE_FS("Cannot {} while hardcore mode is active.", trigger)),
Host::OSD_WARNING_DURATION);
callback(false);
return;
}
auto real_callback = [callback = std::move(callback)](bool res) mutable {
// don't run the callback in the middle of rendering the UI
Host::RunOnCPUThread([callback = std::move(callback), res]() {
if (res)
DisableHardcoreMode();
callback(res);
});
};
ImGuiFullscreen::OpenConfirmMessageDialog(
TRANSLATE_STR("Achievements", "Confirm Hardcore Mode"),
fmt::format(TRANSLATE_FS("Achievements", "{0} cannot be performed while hardcore mode is active. Do you "
"want to disable hardcore mode? {0} will be cancelled if you select No."),
trigger),
std::move(real_callback), true, fmt::format(ICON_FA_CHECK " {}", TRANSLATE_SV("Achievements", "Yes")),
fmt::format(ICON_FA_XMARK " {}", TRANSLATE_SV("Achievements", "No")));
});
return fmt::format(
TRANSLATE_FS("Achievements",
"{0} cannot be performed while hardcore mode is active. "
"Do you want to disable hardcore mode? "
"{0} will be cancelled if you select No."),
reason);
}
void Achievements::ClearUIState()

View File

@ -73,9 +73,13 @@ namespace Achievements
/// Forces hardcore mode off until next reset.
void DisableHardcoreMode();
/// Prompts the user to disable hardcore mode, if they agree, returns true.
bool ConfirmHardcoreModeDisable(const char* trigger);
void ConfirmHardcoreModeDisableAsync(const char* trigger, std::function<void(bool)> callback);
/// Returns the translated title to display for the message box asking the
// user to disable hardcore mode.
const char* GetHardcoreModeDisableTitle();
/// Returns the translated text to display in the message box asking the
/// user to disable hardcore mode.
std::string GetHardcoreModeDisableText(const char* reason);
/// Returns true if hardcore mode is active, and functionality should be restricted.
bool IsHardcoreModeActive();

View File

@ -489,6 +489,7 @@ namespace FullscreenUI
static ImGuiFullscreen::FileSelectorFilters GetDiscImageFilters();
static ImGuiFullscreen::FileSelectorFilters GetAudioFileFilters();
static ImGuiFullscreen::FileSelectorFilters GetImageFileFilters();
static void DoVMInitialize(const VMBootParameters& boot_params, bool switch_to_landing_on_failure);
static void DoStartPath(
const std::string& path, std::optional<s32> state_index = std::nullopt, std::optional<bool> fast_boot = std::nullopt);
static void DoStartFile();
@ -1436,6 +1437,40 @@ ImGuiFullscreen::FileSelectorFilters FullscreenUI::GetImageFileFilters()
return {"*.png", "*.jpg", "*.jpeg", "*.bmp"};
}
void FullscreenUI::DoVMInitialize(const VMBootParameters& boot_params, bool switch_to_landing_on_failure)
{
auto hardcore_disable_callback = [switch_to_landing_on_failure](
std::string reason, VMBootRestartCallback restart_callback) {
MTGS::RunOnGSThread([reason = std::move(reason),
restart_callback = std::move(restart_callback),
switch_to_landing_on_failure]() {
const auto callback = [restart_callback = std::move(restart_callback),
switch_to_landing_on_failure](bool confirmed) {
if (confirmed)
Host::RunOnCPUThread(restart_callback);
else if (switch_to_landing_on_failure)
SwitchToLanding();
};
ImGuiFullscreen::OpenConfirmMessageDialog(
Achievements::GetHardcoreModeDisableTitle(),
Achievements::GetHardcoreModeDisableText(reason.c_str()),
std::move(callback), true,
fmt::format(ICON_FA_CHECK " {}", TRANSLATE_SV("Achievements", "Yes")),
fmt::format(ICON_FA_XMARK " {}", TRANSLATE_SV("Achievements", "No")));
});
};
auto done_callback = [switch_to_landing_on_failure](VMBootResult result) {
if (result == VMBootResult::StartupSuccess)
VMManager::SetState(VMState::Running);
else if (switch_to_landing_on_failure)
MTGS::RunOnGSThread(SwitchToLanding);
};
VMManager::InitializeAsync(boot_params, std::move(hardcore_disable_callback), std::move(done_callback));
}
void FullscreenUI::DoStartPath(const std::string& path, std::optional<s32> state_index, std::optional<bool> fast_boot)
{
VMBootParameters params;
@ -1445,11 +1480,7 @@ void FullscreenUI::DoStartPath(const std::string& path, std::optional<s32> state
// switch to nothing, we'll get brought back if init fails
Host::RunOnCPUThread([params = std::move(params)]() {
if (VMManager::HasValidVM())
return;
if (VMManager::Initialize(params))
VMManager::SetState(VMState::Running);
DoVMInitialize(std::move(params), false);
});
}
@ -1472,10 +1503,7 @@ void FullscreenUI::DoStartBIOS()
return;
VMBootParameters params;
if (VMManager::Initialize(params))
VMManager::SetState(VMState::Running);
else
SwitchToLanding();
DoVMInitialize(std::move(params), true);
});
// switch to nothing, we'll get brought back if init fails
@ -1491,10 +1519,7 @@ void FullscreenUI::DoStartDisc(const std::string& drive)
VMBootParameters params;
params.filename = std::move(drive);
params.source_type = CDVD_SourceType::Disc;
if (VMManager::Initialize(params))
VMManager::SetState(VMState::Running);
else
SwitchToLanding();
DoVMInitialize(std::move(params), true);
});
}
@ -7628,8 +7653,7 @@ void FullscreenUI::DoLoadState(std::string path)
VMBootParameters params;
params.filename = std::move(boot_path);
params.save_state = std::move(path);
if (VMManager::Initialize(params))
VMManager::SetState(VMState::Running);
DoVMInitialize(params, false);
}
});
}

View File

@ -1267,10 +1267,42 @@ void VMManager::PrecacheCDVDFile()
}
}
bool VMManager::Initialize(const VMBootParameters& boot_params)
void VMManager::InitializeAsync(
const VMBootParameters& boot_params,
VMBootHardcoreDisableCallback hardcore_disable_callback,
VMBootDoneCallback done_callback)
{
VMBootResult result = VMManager::Initialize(boot_params);
if (result == VMBootResult::PromptDisableHardcoreMode)
{
std::string reason;
if (DebugInterface::getPauseOnEntry())
reason = TRANSLATE_STR("VMManager", "Boot and Debug");
else
reason = TRANSLATE_STR("VMManager", "Resuming state");
hardcore_disable_callback(reason,
[boot_params, done_callback = std::move(done_callback)]() {
VMBootParameters new_boot_params = std::move(boot_params);
new_boot_params.disable_achievements_hardcore_mode = true;
done_callback(VMManager::Initialize(new_boot_params));
});
return;
}
done_callback(result);
}
VMBootResult VMManager::Initialize(const VMBootParameters& boot_params)
{
const Common::Timer init_timer;
pxAssertRel(s_state.load(std::memory_order_acquire) == VMState::Shutdown, "VM is shutdown");
if (s_state.load(std::memory_order_acquire) != VMState::Shutdown)
{
Host::ReportErrorAsync("Error", "The virtual machine is already running.");
return VMBootResult::StartupFailure;
}
// cancel any game list scanning, we need to use CDVD!
// TODO: we can get rid of this once, we make CDVD not use globals...
@ -1313,14 +1345,14 @@ bool VMManager::Initialize(const VMBootParameters& boot_params)
if (boot_params.filename.empty())
{
Host::ReportErrorAsync("Error", "Cannot load an indexed save state without a boot filename.");
return false;
return VMBootResult::StartupFailure;
}
state_to_load = GetSaveStateFileName(boot_params.filename.c_str(), boot_params.state_index.value());
if (state_to_load.empty())
{
Host::ReportErrorAsync("Error", "Could not resolve path indexed save state load.");
return false;
return VMBootResult::StartupFailure;
}
}
@ -1328,7 +1360,7 @@ bool VMManager::Initialize(const VMBootParameters& boot_params)
if (!cdvdLock(&cdvd_lock_error))
{
Host::ReportErrorAsync("Startup Error", cdvd_lock_error.GetDescription());
return false;
return VMBootResult::StartupFailure;
}
ScopedGuard unlock_cdvd = &cdvdUnlock;
@ -1341,7 +1373,7 @@ bool VMManager::Initialize(const VMBootParameters& boot_params)
{
Host::ReportErrorAsync(
"Error", fmt::format("Requested filename '{}' does not exist.", boot_params.filename));
return false;
return VMBootResult::StartupFailure;
}
// Use specified source type.
@ -1352,7 +1384,7 @@ bool VMManager::Initialize(const VMBootParameters& boot_params)
{
// Automatic type detection of boot parameter based on filename.
if (!AutoDetectSource(boot_params.filename))
return false;
return VMBootResult::StartupFailure;
}
ScopedGuard close_cdvd_files(&CDVDsys_ClearFiles);
@ -1372,7 +1404,7 @@ bool VMManager::Initialize(const VMBootParameters& boot_params)
"PCSX2 will be able to run once you've placed your BIOS image inside the folder named \"bios\" within the data directory "
"(Tools Menu -> Open Data Directory)."),
PCSX2_DOCUMENTATION_BIOS_URL_SHORTENED));
return false;
return VMBootResult::StartupFailure;
}
// Must happen after BIOS load, depends on BIOS version.
@ -1386,7 +1418,7 @@ bool VMManager::Initialize(const VMBootParameters& boot_params)
Host::ReportErrorAsync("Startup Error", fmt::format("Failed to open CDVD '{}': {}.",
Path::GetFileName(CDVDsys_GetFile(CDVDsys_GetSourceType())),
error.GetDescription()));
return false;
return VMBootResult::StartupFailure;
}
ScopedGuard close_cdvd(&DoCDVDclose);
@ -1407,7 +1439,7 @@ bool VMManager::Initialize(const VMBootParameters& boot_params)
if (!FileSystem::FileExists(s_elf_override.c_str()))
{
Host::ReportErrorAsync("Error", fmt::format("Requested boot ELF '{}' does not exist.", s_elf_override));
return false;
return VMBootResult::StartupFailure;
}
Hle_SetHostRoot(s_elf_override.c_str());
@ -1429,42 +1461,9 @@ bool VMManager::Initialize(const VMBootParameters& boot_params)
Achievements::DisableHardcoreMode();
else
Achievements::ResetHardcoreMode(true);
if (Achievements::IsHardcoreModeActive())
{
auto confirm_hc_mode_disable = [&boot_params](const char* trigger) mutable {
if (FullscreenUI::IsInitialized())
{
s_elf_override = {};
VMBootParameters new_boot_params = boot_params;
new_boot_params.disable_achievements_hardcore_mode = true;
Achievements::ConfirmHardcoreModeDisableAsync(trigger,
[new_boot_params = std::move(new_boot_params)](bool approved) mutable {
if (approved && Initialize(new_boot_params))
SetState(VMState::Running);
});
return false;
}
else if (!Achievements::ConfirmHardcoreModeDisable(trigger))
{
return false;
}
return true;
};
if (!state_to_load.empty())
{
if (!confirm_hc_mode_disable(TRANSLATE("VMManager", "Resuming state")))
return false;
}
if (DebugInterface::getPauseOnEntry())
{
if (!confirm_hc_mode_disable(TRANSLATE("VMManager", "Boot and Debug")))
return false;
}
}
if (Achievements::IsHardcoreModeActive() && (!state_to_load.empty() || DebugInterface::getPauseOnEntry()))
return VMBootResult::PromptDisableHardcoreMode;
s_limiter_mode = LimiterModeType::Nominal;
s_target_speed = GetTargetSpeedForLimiterMode(s_limiter_mode);
@ -1486,7 +1485,7 @@ bool VMManager::Initialize(const VMBootParameters& boot_params)
{
// we assume GS is going to report its own error
Console.WriteLn("Failed to open GS.");
return false;
return VMBootResult::StartupFailure;
}
ScopedGuard close_gs = []() {
@ -1498,7 +1497,7 @@ bool VMManager::Initialize(const VMBootParameters& boot_params)
if (!SPU2::Open())
{
Host::ReportErrorAsync("Startup Error", "Failed to initialize SPU2.");
return false;
return VMBootResult::StartupFailure;
}
ScopedGuard close_spu2(&SPU2::Close);
@ -1507,7 +1506,7 @@ bool VMManager::Initialize(const VMBootParameters& boot_params)
if (!Pad::Initialize())
{
Host::ReportErrorAsync("Startup Error", "Failed to initialize PAD");
return false;
return VMBootResult::StartupFailure;
}
ScopedGuard close_pad = &Pad::Shutdown;
@ -1515,7 +1514,7 @@ bool VMManager::Initialize(const VMBootParameters& boot_params)
if (!g_Sio2.Initialize())
{
Host::ReportErrorAsync("Startup Error", "Failed to initialize SIO2");
return false;
return VMBootResult::StartupFailure;
}
ScopedGuard close_sio2 = []() {
g_Sio2.Shutdown();
@ -1525,7 +1524,7 @@ bool VMManager::Initialize(const VMBootParameters& boot_params)
if (!g_Sio0.Initialize())
{
Host::ReportErrorAsync("Startup Error", "Failed to initialize SIO0");
return false;
return VMBootResult::StartupFailure;
}
ScopedGuard close_sio0 = []() {
g_Sio0.Shutdown();
@ -1535,7 +1534,7 @@ bool VMManager::Initialize(const VMBootParameters& boot_params)
if (DEV9init() != 0 || DEV9open() != 0)
{
Host::ReportErrorAsync("Startup Error", "Failed to initialize DEV9.");
return false;
return VMBootResult::StartupFailure;
}
ScopedGuard close_dev9 = []() {
DEV9close();
@ -1546,7 +1545,7 @@ bool VMManager::Initialize(const VMBootParameters& boot_params)
if (!USBopen())
{
Host::ReportErrorAsync("Startup Error", "Failed to initialize USB.");
return false;
return VMBootResult::StartupFailure;
}
ScopedGuard close_usb = []() { USBclose(); };
@ -1554,7 +1553,7 @@ bool VMManager::Initialize(const VMBootParameters& boot_params)
if (FWopen() != 0)
{
Host::ReportErrorAsync("Startup Error", "Failed to initialize FW.");
return false;
return VMBootResult::StartupFailure;
}
ScopedGuard close_fw = []() { FWclose(); };
@ -1594,12 +1593,12 @@ bool VMManager::Initialize(const VMBootParameters& boot_params)
{
Host::ReportErrorAsync(TRANSLATE_SV("VMManager", "Failed to load save state."), state_error.GetDescription());
Shutdown(false);
return false;
return VMBootResult::StartupFailure;
}
}
PerformanceMetrics::Clear();
return true;
return VMBootResult::StartupSuccess;
}
void VMManager::Shutdown(bool save_resume_state)

View File

@ -39,6 +39,30 @@ struct VMBootParameters
bool disable_achievements_hardcore_mode = false;
};
enum class VMBootResult
{
/// The boot succeeded.
StartupSuccess,
/// The boot failed and an error should be displayed in the UI.
StartupFailure,
/// The boot failed because the user needs to be prompted to disable
/// hardcore mode. If the user agrees, VMManager::Initialize should be
/// called again with disable_achievements_hardcore_mode set to true.
PromptDisableHardcoreMode
};
/// Callback used to restart the VM boot process after the user has consented
/// to hardcore mode being disabled.
using VMBootRestartCallback = std::function<void()>;
/// Callback used when the VM boot process has been interrupted because the user
/// needs to be prompted to disable hardcore mode.
using VMBootHardcoreDisableCallback = std::function<void(std::string reason, VMBootRestartCallback restart_callback)>;
/// Callback used when the VM boot process has finished. This may be called
/// asynchronously after the user has been prompted to disable hardcore mode.
using VMBootDoneCallback = std::function<void(VMBootResult result)>;
namespace VMManager
{
/// The number of usable save state slots.
@ -83,8 +107,16 @@ namespace VMManager
/// Returns the path to the ELF which is currently running. Only safe to read on the EE thread.
const std::string& GetCurrentELF();
/// Initializes all system components.
bool Initialize(const VMBootParameters& boot_params);
/// Initializes all system components. May restart itself asynchronously
/// using the provided hardcore_disable_callback function. Will call the
/// done_callback function on either success or failure.
void InitializeAsync(
const VMBootParameters& boot_params,
VMBootHardcoreDisableCallback hardcore_disable_callback,
VMBootDoneCallback done_callback);
/// Initializes all system components. Will not attempt to restart itself.
VMBootResult Initialize(const VMBootParameters& boot_params);
/// Destroys all system components.
void Shutdown(bool save_resume_state);