// Copyright 2015 Dolphin Emulator Project // Licensed under GPLv2+ // Refer to the license.txt file included. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(__unix__) || defined(__unix) || defined(__APPLE__) #include #include "QtUtils/SignalDaemon.h" #endif #include "Common/Version.h" #include "Core/Boot/Boot.h" #include "Core/BootManager.h" #include "Core/CommonTitles.h" #include "Core/Config/MainSettings.h" #include "Core/Config/NetplaySettings.h" #include "Core/ConfigManager.h" #include "Core/Core.h" #include "Core/HW/DVD/DVDInterface.h" #include "Core/HW/GCKeyboard.h" #include "Core/HW/GCPad.h" #include "Core/HW/ProcessorInterface.h" #include "Core/HW/Wiimote.h" #include "Core/HW/WiimoteEmu/WiimoteEmu.h" #include "Core/HotkeyManager.h" #include "Core/IOS/USB/Bluetooth/BTEmu.h" #include "Core/IOS/USB/Bluetooth/WiimoteDevice.h" #include "Core/Movie.h" #include "Core/NetPlayClient.h" #include "Core/NetPlayProto.h" #include "Core/NetPlayServer.h" #include "Core/State.h" #include "DiscIO/NANDImporter.h" #include "DolphinQt/AboutDialog.h" #include "DolphinQt/CheatsManager.h" #include "DolphinQt/Config/ControllersWindow.h" #include "DolphinQt/Config/Graphics/GraphicsWindow.h" #include "DolphinQt/Config/LogConfigWidget.h" #include "DolphinQt/Config/LogWidget.h" #include "DolphinQt/Config/Mapping/MappingWindow.h" #include "DolphinQt/Config/SettingsWindow.h" #include "DolphinQt/Debugger/BreakpointWidget.h" #include "DolphinQt/Debugger/CodeViewWidget.h" #include "DolphinQt/Debugger/CodeWidget.h" #include "DolphinQt/Debugger/JITWidget.h" #include "DolphinQt/Debugger/MemoryWidget.h" #include "DolphinQt/Debugger/RegisterWidget.h" #include "DolphinQt/Debugger/WatchWidget.h" #include "DolphinQt/FIFO/FIFOPlayerWindow.h" #include "DolphinQt/GCMemcardManager.h" #include "DolphinQt/GameList/GameList.h" #include "DolphinQt/Host.h" #include "DolphinQt/HotkeyScheduler.h" #include "DolphinQt/MainWindow.h" #include "DolphinQt/MenuBar.h" #include "DolphinQt/NetPlay/NetPlayDialog.h" #include "DolphinQt/NetPlay/NetPlaySetupDialog.h" #include "DolphinQt/QtUtils/QueueOnObject.h" #include "DolphinQt/QtUtils/RunOnObject.h" #include "DolphinQt/QtUtils/WindowActivationEventFilter.h" #include "DolphinQt/RenderWidget.h" #include "DolphinQt/Resources.h" #include "DolphinQt/SearchBar.h" #include "DolphinQt/Settings.h" #include "DolphinQt/TAS/GCTASInputWindow.h" #include "DolphinQt/TAS/WiiTASInputWindow.h" #include "DolphinQt/ToolBar.h" #include "DolphinQt/WiiUpdate.h" #include "InputCommon/ControllerInterface/ControllerInterface.h" #include "UICommon/DiscordPresence.h" #include "UICommon/GameFile.h" #include "UICommon/UICommon.h" #include "VideoCommon/VideoConfig.h" #if defined(HAVE_XRANDR) && HAVE_XRANDR #include #include "UICommon/X11Utils.h" #endif #if defined(__unix__) || defined(__unix) || defined(__APPLE__) void MainWindow::OnSignal() { close(); } static void InstallSignalHandler() { struct sigaction sa; sa.sa_handler = &SignalDaemon::HandleInterrupt; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESETHAND; sigaction(SIGINT, &sa, nullptr); sigaction(SIGTERM, &sa, nullptr); } #endif MainWindow::MainWindow(std::unique_ptr boot_parameters) : QMainWindow(nullptr) { setWindowTitle(QString::fromStdString(Common::scm_rev_str)); setWindowIcon(Resources::GetAppIcon()); setUnifiedTitleAndToolBarOnMac(true); setAcceptDrops(true); InitControllers(); CreateComponents(); ConnectGameList(); ConnectHost(); ConnectToolBar(); ConnectRenderWidget(); ConnectStack(); ConnectMenuBar(); ConnectHotkeys(); InitCoreCallbacks(); NetPlayInit(); #if defined(__unix__) || defined(__unix) || defined(__APPLE__) auto* daemon = new SignalDaemon(this); connect(daemon, &SignalDaemon::InterruptReceived, this, &MainWindow::OnSignal); InstallSignalHandler(); #endif if (boot_parameters) m_pending_boot = std::move(boot_parameters); QSettings& settings = Settings::GetQSettings(); restoreState(settings.value(QStringLiteral("mainwindow/state")).toByteArray()); restoreGeometry(settings.value(QStringLiteral("mainwindow/geometry")).toByteArray()); m_render_widget_geometry = settings.value(QStringLiteral("renderwidget/geometry")).toByteArray(); } MainWindow::~MainWindow() { m_render_widget->deleteLater(); m_netplay_dialog->deleteLater(); ShutdownControllers(); QSettings& settings = Settings::GetQSettings(); settings.setValue(QStringLiteral("mainwindow/state"), saveState()); settings.setValue(QStringLiteral("mainwindow/geometry"), saveGeometry()); settings.setValue(QStringLiteral("renderwidget/geometry"), m_render_widget_geometry); Config::Save(); } void MainWindow::InitControllers() { if (g_controller_interface.IsInit()) return; g_controller_interface.Initialize(reinterpret_cast(winId())); Pad::Initialize(); Keyboard::Initialize(); Wiimote::Initialize(Wiimote::InitializeMode::DO_NOT_WAIT_FOR_WIIMOTES); m_hotkey_scheduler = new HotkeyScheduler(); m_hotkey_scheduler->Start(); // Defaults won't work reliabily without loading and saving the config first Wiimote::LoadConfig(); Wiimote::GetConfig()->SaveConfig(); Pad::LoadConfig(); Pad::GetConfig()->SaveConfig(); Keyboard::LoadConfig(); Keyboard::GetConfig()->SaveConfig(); } void MainWindow::ShutdownControllers() { m_hotkey_scheduler->Stop(); g_controller_interface.Shutdown(); Pad::Shutdown(); Keyboard::Shutdown(); Wiimote::Shutdown(); HotkeyManagerEmu::Shutdown(); m_hotkey_scheduler->deleteLater(); } void MainWindow::InitCoreCallbacks() { connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, [=](Core::State state) { if (state == Core::State::Uninitialized) OnStopComplete(); if (state == Core::State::Running && m_fullscreen_requested) { FullScreen(); m_fullscreen_requested = false; } }); installEventFilter(this); m_render_widget->installEventFilter(this); } static void InstallHotkeyFilter(QWidget* dialog) { auto* filter = new WindowActivationEventFilter(); dialog->installEventFilter(filter); filter->connect(filter, &WindowActivationEventFilter::windowDeactivated, [] { HotkeyManagerEmu::Enable(true); }); filter->connect(filter, &WindowActivationEventFilter::windowActivated, [] { HotkeyManagerEmu::Enable(false); }); } void MainWindow::CreateComponents() { m_menu_bar = new MenuBar(this); m_tool_bar = new ToolBar(this); m_search_bar = new SearchBar(this); m_game_list = new GameList(this); m_render_widget = new RenderWidget; m_stack = new QStackedWidget(this); m_controllers_window = new ControllersWindow(this); m_settings_window = new SettingsWindow(this); for (int i = 0; i < 4; i++) { m_gc_tas_input_windows[i] = new GCTASInputWindow(this, i); m_wii_tas_input_windows[i] = new WiiTASInputWindow(this, i); } Movie::SetGCInputManip([this](GCPadStatus* pad_status, int controller_id) { m_gc_tas_input_windows[controller_id]->GetValues(pad_status); }); Movie::SetWiiInputManip([this](u8* input_data, WiimoteEmu::ReportFeatures rptf, int controller_id, int ext, wiimote_key key) { m_wii_tas_input_windows[controller_id]->GetValues(input_data, rptf, ext, key); }); m_jit_widget = new JITWidget(this); m_log_widget = new LogWidget(this); m_log_config_widget = new LogConfigWidget(this); m_fifo_window = new FIFOPlayerWindow(this); m_memory_widget = new MemoryWidget(this); connect(m_fifo_window, &FIFOPlayerWindow::LoadFIFORequested, this, [this](const QString& path) { StartGame(path); }); m_register_widget = new RegisterWidget(this); m_watch_widget = new WatchWidget(this); m_breakpoint_widget = new BreakpointWidget(this); m_code_widget = new CodeWidget(this); m_cheats_manager = new CheatsManager(this); connect(m_watch_widget, &WatchWidget::RequestMemoryBreakpoint, [this](u32 addr) { m_breakpoint_widget->AddAddressMBP(addr); }); connect(m_register_widget, &RegisterWidget::RequestMemoryBreakpoint, [this](u32 addr) { m_breakpoint_widget->AddAddressMBP(addr); }); connect(m_code_widget, &CodeWidget::BreakpointsChanged, m_breakpoint_widget, &BreakpointWidget::Update); connect(m_code_widget, &CodeWidget::RequestPPCComparison, m_jit_widget, &JITWidget::Compare); connect(m_memory_widget, &MemoryWidget::BreakpointsChanged, m_breakpoint_widget, &BreakpointWidget::Update); connect(m_breakpoint_widget, &BreakpointWidget::BreakpointsChanged, m_code_widget, &CodeWidget::Update); connect(m_breakpoint_widget, &BreakpointWidget::BreakpointsChanged, m_memory_widget, &MemoryWidget::Update); connect(m_breakpoint_widget, &BreakpointWidget::SelectedBreakpoint, [this](u32 address) { if (Core::GetState() == Core::State::Paused) m_code_widget->SetAddress(address, CodeViewWidget::SetAddressUpdate::WithUpdate); }); #if defined(HAVE_XRANDR) && HAVE_XRANDR m_xrr_config = std::make_unique( static_cast(QGuiApplication::platformNativeInterface()->nativeResourceForWindow( "display", windowHandle())), winId()); m_graphics_window = new GraphicsWindow(m_xrr_config.get(), this); #else m_graphics_window = new GraphicsWindow(nullptr, this); #endif InstallHotkeyFilter(m_controllers_window); InstallHotkeyFilter(m_settings_window); InstallHotkeyFilter(m_graphics_window); } void MainWindow::ConnectMenuBar() { setMenuBar(m_menu_bar); // File connect(m_menu_bar, &MenuBar::Open, this, &MainWindow::Open); connect(m_menu_bar, &MenuBar::Exit, this, &MainWindow::close); connect(m_menu_bar, &MenuBar::EjectDisc, this, &MainWindow::EjectDisc); connect(m_menu_bar, &MenuBar::ChangeDisc, this, &MainWindow::ChangeDisc); connect(m_menu_bar, &MenuBar::BootDVDBackup, this, [this](const QString& drive) { StartGame(drive); }); // Emulation connect(m_menu_bar, &MenuBar::Pause, this, &MainWindow::Pause); connect(m_menu_bar, &MenuBar::Play, this, [this]() { Play(); }); connect(m_menu_bar, &MenuBar::Stop, this, &MainWindow::RequestStop); connect(m_menu_bar, &MenuBar::Reset, this, &MainWindow::Reset); connect(m_menu_bar, &MenuBar::Fullscreen, this, &MainWindow::FullScreen); connect(m_menu_bar, &MenuBar::FrameAdvance, this, &MainWindow::FrameAdvance); connect(m_menu_bar, &MenuBar::Screenshot, this, &MainWindow::ScreenShot); connect(m_menu_bar, &MenuBar::StateLoad, this, &MainWindow::StateLoad); connect(m_menu_bar, &MenuBar::StateSave, this, &MainWindow::StateSave); connect(m_menu_bar, &MenuBar::StateLoadSlot, this, &MainWindow::StateLoadSlot); connect(m_menu_bar, &MenuBar::StateSaveSlot, this, &MainWindow::StateSaveSlot); connect(m_menu_bar, &MenuBar::StateLoadSlotAt, this, &MainWindow::StateLoadSlotAt); connect(m_menu_bar, &MenuBar::StateSaveSlotAt, this, &MainWindow::StateSaveSlotAt); connect(m_menu_bar, &MenuBar::StateLoadUndo, this, &MainWindow::StateLoadUndo); connect(m_menu_bar, &MenuBar::StateSaveUndo, this, &MainWindow::StateSaveUndo); connect(m_menu_bar, &MenuBar::StateSaveOldest, this, &MainWindow::StateSaveOldest); connect(m_menu_bar, &MenuBar::SetStateSlot, this, &MainWindow::SetStateSlot); // Options connect(m_menu_bar, &MenuBar::Configure, this, &MainWindow::ShowSettingsWindow); connect(m_menu_bar, &MenuBar::ConfigureGraphics, this, &MainWindow::ShowGraphicsWindow); connect(m_menu_bar, &MenuBar::ConfigureAudio, this, &MainWindow::ShowAudioWindow); connect(m_menu_bar, &MenuBar::ConfigureControllers, this, &MainWindow::ShowControllersWindow); connect(m_menu_bar, &MenuBar::ConfigureHotkeys, this, &MainWindow::ShowHotkeyDialog); // Tools connect(m_menu_bar, &MenuBar::ShowMemcardManager, this, &MainWindow::ShowMemcardManager); connect(m_menu_bar, &MenuBar::ShowCheatsManager, this, &MainWindow::ShowCheatsManager); connect(m_menu_bar, &MenuBar::BootGameCubeIPL, this, &MainWindow::OnBootGameCubeIPL); connect(m_menu_bar, &MenuBar::ImportNANDBackup, this, &MainWindow::OnImportNANDBackup); connect(m_menu_bar, &MenuBar::PerformOnlineUpdate, this, &MainWindow::PerformOnlineUpdate); connect(m_menu_bar, &MenuBar::BootWiiSystemMenu, this, &MainWindow::BootWiiSystemMenu); connect(m_menu_bar, &MenuBar::StartNetPlay, this, &MainWindow::ShowNetPlaySetupDialog); connect(m_menu_bar, &MenuBar::ShowFIFOPlayer, this, &MainWindow::ShowFIFOPlayer); connect(m_menu_bar, &MenuBar::ConnectWiiRemote, this, &MainWindow::OnConnectWiiRemote); // Movie connect(m_menu_bar, &MenuBar::PlayRecording, this, &MainWindow::OnPlayRecording); connect(m_menu_bar, &MenuBar::StartRecording, this, &MainWindow::OnStartRecording); connect(m_menu_bar, &MenuBar::StopRecording, this, &MainWindow::OnStopRecording); connect(m_menu_bar, &MenuBar::ExportRecording, this, &MainWindow::OnExportRecording); connect(m_menu_bar, &MenuBar::ShowTASInput, this, &MainWindow::ShowTASInput); // View connect(m_menu_bar, &MenuBar::ShowList, m_game_list, &GameList::SetListView); connect(m_menu_bar, &MenuBar::ShowGrid, m_game_list, &GameList::SetGridView); connect(m_menu_bar, &MenuBar::ToggleSearch, m_search_bar, &SearchBar::Toggle); connect(m_menu_bar, &MenuBar::ColumnVisibilityToggled, m_game_list, &GameList::OnColumnVisibilityToggled); connect(m_menu_bar, &MenuBar::GameListPlatformVisibilityToggled, m_game_list, &GameList::OnGameListVisibilityChanged); connect(m_menu_bar, &MenuBar::GameListRegionVisibilityToggled, m_game_list, &GameList::OnGameListVisibilityChanged); connect(m_menu_bar, &MenuBar::ShowAboutDialog, this, &MainWindow::ShowAboutDialog); connect(m_game_list, &GameList::SelectionChanged, m_menu_bar, &MenuBar::SelectionChanged); connect(this, &MainWindow::ReadOnlyModeChanged, m_menu_bar, &MenuBar::ReadOnlyModeChanged); connect(this, &MainWindow::RecordingStatusChanged, m_menu_bar, &MenuBar::RecordingStatusChanged); // Symbols connect(m_menu_bar, &MenuBar::NotifySymbolsUpdated, [this] { m_code_widget->UpdateSymbols(); m_code_widget->Update(); }); } void MainWindow::ConnectHotkeys() { connect(m_hotkey_scheduler, &HotkeyScheduler::Open, this, &MainWindow::Open); connect(m_hotkey_scheduler, &HotkeyScheduler::ChangeDisc, this, &MainWindow::ChangeDisc); connect(m_hotkey_scheduler, &HotkeyScheduler::EjectDisc, this, &MainWindow::EjectDisc); connect(m_hotkey_scheduler, &HotkeyScheduler::ExitHotkey, this, &MainWindow::close); connect(m_hotkey_scheduler, &HotkeyScheduler::TogglePauseHotkey, this, &MainWindow::TogglePause); connect(m_hotkey_scheduler, &HotkeyScheduler::RefreshGameListHotkey, this, &MainWindow::RefreshGameList); connect(m_hotkey_scheduler, &HotkeyScheduler::StopHotkey, this, &MainWindow::RequestStop); connect(m_hotkey_scheduler, &HotkeyScheduler::ResetHotkey, this, &MainWindow::Reset); connect(m_hotkey_scheduler, &HotkeyScheduler::ScreenShotHotkey, this, &MainWindow::ScreenShot); connect(m_hotkey_scheduler, &HotkeyScheduler::FullScreenHotkey, this, &MainWindow::FullScreen); connect(m_hotkey_scheduler, &HotkeyScheduler::StateLoadSlot, this, &MainWindow::StateLoadSlotAt); connect(m_hotkey_scheduler, &HotkeyScheduler::StateSaveSlot, this, &MainWindow::StateSaveSlotAt); connect(m_hotkey_scheduler, &HotkeyScheduler::StateLoadLastSaved, this, &MainWindow::StateLoadLastSavedAt); connect(m_hotkey_scheduler, &HotkeyScheduler::StateLoadUndo, this, &MainWindow::StateLoadUndo); connect(m_hotkey_scheduler, &HotkeyScheduler::StateSaveUndo, this, &MainWindow::StateSaveUndo); connect(m_hotkey_scheduler, &HotkeyScheduler::StateSaveOldest, this, &MainWindow::StateSaveOldest); connect(m_hotkey_scheduler, &HotkeyScheduler::StateSaveFile, this, &MainWindow::StateSave); connect(m_hotkey_scheduler, &HotkeyScheduler::StateLoadFile, this, &MainWindow::StateLoad); connect(m_hotkey_scheduler, &HotkeyScheduler::StateLoadSlotHotkey, this, &MainWindow::StateLoadSlot); connect(m_hotkey_scheduler, &HotkeyScheduler::StateSaveSlotHotkey, this, &MainWindow::StateSaveSlot); connect(m_hotkey_scheduler, &HotkeyScheduler::SetStateSlotHotkey, this, &MainWindow::SetStateSlot); connect(m_hotkey_scheduler, &HotkeyScheduler::StartRecording, this, &MainWindow::OnStartRecording); connect(m_hotkey_scheduler, &HotkeyScheduler::ExportRecording, this, &MainWindow::OnExportRecording); connect(m_hotkey_scheduler, &HotkeyScheduler::ConnectWiiRemote, this, &MainWindow::OnConnectWiiRemote); connect(m_hotkey_scheduler, &HotkeyScheduler::ToggleReadOnlyMode, [this] { bool read_only = !Movie::IsReadOnly(); Movie::SetReadOnly(read_only); emit ReadOnlyModeChanged(read_only); }); connect(m_hotkey_scheduler, &HotkeyScheduler::Step, m_code_widget, &CodeWidget::Step); connect(m_hotkey_scheduler, &HotkeyScheduler::StepOver, m_code_widget, &CodeWidget::StepOver); connect(m_hotkey_scheduler, &HotkeyScheduler::StepOut, m_code_widget, &CodeWidget::StepOut); connect(m_hotkey_scheduler, &HotkeyScheduler::Skip, m_code_widget, &CodeWidget::Skip); connect(m_hotkey_scheduler, &HotkeyScheduler::ShowPC, m_code_widget, &CodeWidget::ShowPC); connect(m_hotkey_scheduler, &HotkeyScheduler::SetPC, m_code_widget, &CodeWidget::SetPC); connect(m_hotkey_scheduler, &HotkeyScheduler::ToggleBreakpoint, m_code_widget, &CodeWidget::ToggleBreakpoint); connect(m_hotkey_scheduler, &HotkeyScheduler::AddBreakpoint, m_code_widget, &CodeWidget::AddBreakpoint); } void MainWindow::ConnectToolBar() { addToolBar(m_tool_bar); connect(m_tool_bar, &ToolBar::OpenPressed, this, &MainWindow::Open); connect(m_tool_bar, &ToolBar::RefreshPressed, this, &MainWindow::RefreshGameList); connect(m_tool_bar, &ToolBar::PlayPressed, this, [this]() { Play(); }); connect(m_tool_bar, &ToolBar::PausePressed, this, &MainWindow::Pause); connect(m_tool_bar, &ToolBar::StopPressed, this, &MainWindow::RequestStop); connect(m_tool_bar, &ToolBar::FullScreenPressed, this, &MainWindow::FullScreen); connect(m_tool_bar, &ToolBar::ScreenShotPressed, this, &MainWindow::ScreenShot); connect(m_tool_bar, &ToolBar::SettingsPressed, this, &MainWindow::ShowSettingsWindow); connect(m_tool_bar, &ToolBar::ControllersPressed, this, &MainWindow::ShowControllersWindow); connect(m_tool_bar, &ToolBar::GraphicsPressed, this, &MainWindow::ShowGraphicsWindow); connect(m_tool_bar, &ToolBar::StepPressed, m_code_widget, &CodeWidget::Step); connect(m_tool_bar, &ToolBar::StepOverPressed, m_code_widget, &CodeWidget::StepOver); connect(m_tool_bar, &ToolBar::StepOutPressed, m_code_widget, &CodeWidget::StepOut); connect(m_tool_bar, &ToolBar::SkipPressed, m_code_widget, &CodeWidget::Skip); connect(m_tool_bar, &ToolBar::ShowPCPressed, m_code_widget, &CodeWidget::ShowPC); connect(m_tool_bar, &ToolBar::SetPCPressed, m_code_widget, &CodeWidget::SetPC); } void MainWindow::ConnectGameList() { connect(m_game_list, &GameList::GameSelected, this, [this]() { Play(); }); connect(m_game_list, &GameList::NetPlayHost, this, &MainWindow::NetPlayHost); connect(m_game_list, &GameList::OpenGeneralSettings, this, &MainWindow::ShowGeneralWindow); } void MainWindow::ConnectRenderWidget() { m_rendering_to_main = false; m_render_widget->hide(); connect(m_render_widget, &RenderWidget::Closed, this, &MainWindow::ForceStop); connect(m_render_widget, &RenderWidget::FocusChanged, this, [this](bool focus) { if (m_render_widget->isFullScreen()) SetFullScreenResolution(focus); }); } void MainWindow::ConnectHost() { connect(Host::GetInstance(), &Host::UpdateProgressDialog, this, &MainWindow::OnUpdateProgressDialog); } void MainWindow::ConnectStack() { auto* widget = new QWidget; auto* layout = new QVBoxLayout; widget->setLayout(layout); layout->addWidget(m_game_list); layout->addWidget(m_search_bar); layout->setMargin(0); connect(m_search_bar, &SearchBar::Search, m_game_list, &GameList::SetSearchTerm); m_stack->addWidget(widget); setCentralWidget(m_stack); setTabPosition(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea, QTabWidget::North); addDockWidget(Qt::LeftDockWidgetArea, m_log_widget); addDockWidget(Qt::LeftDockWidgetArea, m_log_config_widget); addDockWidget(Qt::LeftDockWidgetArea, m_code_widget); addDockWidget(Qt::LeftDockWidgetArea, m_register_widget); addDockWidget(Qt::LeftDockWidgetArea, m_watch_widget); addDockWidget(Qt::LeftDockWidgetArea, m_breakpoint_widget); addDockWidget(Qt::LeftDockWidgetArea, m_memory_widget); addDockWidget(Qt::LeftDockWidgetArea, m_jit_widget); tabifyDockWidget(m_log_widget, m_log_config_widget); tabifyDockWidget(m_log_widget, m_code_widget); tabifyDockWidget(m_log_widget, m_register_widget); tabifyDockWidget(m_log_widget, m_watch_widget); tabifyDockWidget(m_log_widget, m_breakpoint_widget); tabifyDockWidget(m_log_widget, m_memory_widget); tabifyDockWidget(m_log_widget, m_jit_widget); } void MainWindow::RefreshGameList() { Settings::Instance().ReloadTitleDB(); Settings::Instance().RefreshGameList(); } QString MainWindow::PromptFileName() { auto& settings = Settings::Instance().GetQSettings(); QString path = QFileDialog::getOpenFileName( this, tr("Select a File"), settings.value(QStringLiteral("mainwindow/lastdir"), QStringLiteral("")).toString(), tr("All GC/Wii files (*.elf *.dol *.gcm *.iso *.tgc *.wbfs *.ciso *.gcz *.wad);;" "All Files (*)")); if (!path.isEmpty()) { settings.setValue(QStringLiteral("mainwindow/lastdir"), QFileInfo(path).absoluteDir().absolutePath()); } return path; } void MainWindow::ChangeDisc() { QString file = PromptFileName(); if (!file.isEmpty()) Core::RunAsCPUThread([&file] { DVDInterface::ChangeDisc(file.toStdString()); }); } void MainWindow::EjectDisc() { Core::RunAsCPUThread(DVDInterface::EjectDisc); } void MainWindow::Open() { QString file = PromptFileName(); if (!file.isEmpty()) StartGame(file); } void MainWindow::Play(const std::optional& savestate_path) { // If we're in a paused game, start it up again. // Otherwise, play the selected game, if there is one. // Otherwise, play the default game. // Otherwise, play the last played game, if there is one. // Otherwise, prompt for a new game. if (Core::GetState() == Core::State::Paused) { Core::SetState(Core::State::Running); } else { std::shared_ptr selection = m_game_list->GetSelectedGame(); if (selection) { StartGame(selection->GetFilePath(), savestate_path); EnableScreenSaver(false); } else { const QString default_path = QString::fromStdString(Config::Get(Config::MAIN_DEFAULT_ISO)); if (!default_path.isEmpty() && QFile::exists(default_path)) { StartGame(default_path, savestate_path); EnableScreenSaver(false); } else { Open(); } } } } void MainWindow::Pause() { Core::SetState(Core::State::Paused); } void MainWindow::TogglePause() { if (Core::GetState() == Core::State::Paused) { Play(); } else { Pause(); } } void MainWindow::OnStopComplete() { m_stop_requested = false; HideRenderWidget(); EnableScreenSaver(true); #ifdef USE_DISCORD_PRESENCE Discord::UpdateDiscordPresence(); #endif SetFullScreenResolution(false); if (m_exit_requested || Settings::Instance().IsBatchModeEnabled()) QGuiApplication::instance()->quit(); // If the current emulation prevented the booting of another, do that now if (m_pending_boot != nullptr) { StartGame(std::move(m_pending_boot)); m_pending_boot.reset(); } } bool MainWindow::RequestStop() { if (!Core::IsRunning()) { Core::QueueHostJob([this] { OnStopComplete(); }, true); return true; } if (!m_render_widget->isFullScreen()) m_render_widget_geometry = m_render_widget->saveGeometry(); else FullScreen(); if (SConfig::GetInstance().bConfirmStop) { const Core::State state = Core::GetState(); // Only pause the game, if NetPlay is not running bool pause = !Settings::Instance().GetNetPlayClient(); if (pause) Core::SetState(Core::State::Paused); QMessageBox::StandardButton confirm; confirm = QMessageBox::question(this, tr("Confirm"), m_stop_requested ? tr("A shutdown is already in progress. Unsaved data " "may be lost if you stop the current emulation " "before it completes. Force stop?") : tr("Do you want to stop the current emulation?")); if (pause) Core::SetState(state); if (confirm != QMessageBox::Yes) return false; } // TODO: Add Movie shutdown // TODO: Add Debugger shutdown if (!m_stop_requested && UICommon::TriggerSTMPowerEvent()) { m_stop_requested = true; // Unpause because gracefully shutting down needs the game to actually request a shutdown. // TODO: Do not unpause in debug mode to allow debugging until the complete shutdown. if (Core::GetState() == Core::State::Paused) Core::SetState(Core::State::Running); return true; } ForceStop(); #ifdef Q_OS_WIN // Allow windows to idle or turn off display again SetThreadExecutionState(ES_CONTINUOUS); #endif return true; } void MainWindow::ForceStop() { Core::Stop(); } void MainWindow::Reset() { if (Movie::IsRecordingInput()) Movie::SetReset(true); ProcessorInterface::ResetButton_Tap(); } void MainWindow::FrameAdvance() { Core::DoFrameStep(); } void MainWindow::FullScreen() { // If the render widget is fullscreen we want to reset it to whatever is in // settings. If it's set to be fullscreen then it just remakes the window, // which probably isn't ideal. bool was_fullscreen = m_render_widget->isFullScreen(); if (!was_fullscreen) m_render_widget_geometry = m_render_widget->saveGeometry(); HideRenderWidget(false); SetFullScreenResolution(!was_fullscreen); if (was_fullscreen) { ShowRenderWidget(); } else { m_render_widget->showFullScreen(); } } void MainWindow::ScreenShot() { Core::SaveScreenShot(); } void MainWindow::StartGame(const QString& path, const std::optional& savestate_path) { StartGame(path.toStdString(), savestate_path); } void MainWindow::StartGame(const std::string& path, const std::optional& savestate_path) { StartGame(BootParameters::GenerateFromFile(path, savestate_path)); } void MainWindow::StartGame(std::unique_ptr&& parameters) { // If we're running, only start a new game once we've stopped the last. if (Core::GetState() != Core::State::Uninitialized) { if (!RequestStop()) return; // As long as the shutdown isn't complete, we can't boot, so let's boot later m_pending_boot = std::move(parameters); return; } // Boot up, show an error if it fails to load the game. if (!BootManager::BootCore(std::move(parameters))) { QMessageBox::critical(this, tr("Error"), tr("Failed to init core"), QMessageBox::Ok); return; } ShowRenderWidget(); #ifdef USE_DISCORD_PRESENCE Discord::UpdateDiscordPresence(); #endif if (SConfig::GetInstance().bFullscreen) m_fullscreen_requested = true; #ifdef Q_OS_WIN // Prevents Windows from sleeping, turning off the display, or idling EXECUTION_STATE shouldScreenSave = SConfig::GetInstance().bDisableScreenSaver ? ES_DISPLAY_REQUIRED : 0; SetThreadExecutionState(ES_CONTINUOUS | shouldScreenSave | ES_SYSTEM_REQUIRED); #endif } void MainWindow::SetFullScreenResolution(bool fullscreen) { if (SConfig::GetInstance().strFullscreenResolution == "Auto") return; #ifdef _WIN32 if (!fullscreen) { ChangeDisplaySettings(nullptr, CDS_FULLSCREEN); return; } DEVMODE screen_settings; memset(&screen_settings, 0, sizeof(screen_settings)); screen_settings.dmSize = sizeof(screen_settings); sscanf(SConfig::GetInstance().strFullscreenResolution.c_str(), "%dx%d", &screen_settings.dmPelsWidth, &screen_settings.dmPelsHeight); screen_settings.dmBitsPerPel = 32; screen_settings.dmFields = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT; // Try To Set Selected Mode And Get Results. NOTE: CDS_FULLSCREEN Gets Rid Of Start Bar. ChangeDisplaySettings(&screen_settings, CDS_FULLSCREEN); #elif defined(HAVE_XRANDR) && HAVE_XRANDR m_xrr_config->ToggleDisplayMode(fullscreen); #endif } void MainWindow::ShowRenderWidget() { SetFullScreenResolution(false); Host::GetInstance()->SetRenderFullscreen(false); if (SConfig::GetInstance().bRenderToMain) { // If we're rendering to main, add it to the stack and update our title when necessary. m_rendering_to_main = true; m_stack->setCurrentIndex(m_stack->addWidget(m_render_widget)); connect(Host::GetInstance(), &Host::RequestTitle, this, &MainWindow::setWindowTitle); m_stack->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); m_stack->repaint(); Host::GetInstance()->SetRenderFocus(isActiveWindow()); } else { // Otherwise, just show it. m_rendering_to_main = false; m_render_widget->showNormal(); m_render_widget->restoreGeometry(m_render_widget_geometry); } } void MainWindow::HideRenderWidget(bool reinit) { if (m_rendering_to_main) { // Remove the widget from the stack and reparent it to nullptr, so that it can draw // itself in a new window if it wants. Disconnect the title updates. m_stack->removeWidget(m_render_widget); m_render_widget->setParent(nullptr); m_rendering_to_main = false; m_stack->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); disconnect(Host::GetInstance(), &Host::RequestTitle, this, &MainWindow::setWindowTitle); setWindowTitle(QString::fromStdString(Common::scm_rev_str)); } // The following code works around a driver bug that would lead to Dolphin crashing when changing // graphics backends (e.g. OpenGL to Vulkan). To avoid this the render widget is (safely) // recreated if (reinit) { m_render_widget->hide(); disconnect(m_render_widget, &RenderWidget::Closed, this, &MainWindow::ForceStop); m_render_widget->removeEventFilter(this); m_render_widget->deleteLater(); m_render_widget = new RenderWidget; m_render_widget->installEventFilter(this); connect(m_render_widget, &RenderWidget::Closed, this, &MainWindow::ForceStop); connect(m_render_widget, &RenderWidget::FocusChanged, this, [this](bool focus) { if (m_render_widget->isFullScreen()) SetFullScreenResolution(focus); }); } } void MainWindow::ShowControllersWindow() { m_controllers_window->show(); m_controllers_window->raise(); m_controllers_window->activateWindow(); } void MainWindow::ShowSettingsWindow() { m_settings_window->show(); m_settings_window->raise(); m_settings_window->activateWindow(); } void MainWindow::ShowAudioWindow() { m_settings_window->SelectAudioPane(); ShowSettingsWindow(); } void MainWindow::ShowGeneralWindow() { m_settings_window->SelectGeneralPane(); ShowSettingsWindow(); } void MainWindow::ShowAboutDialog() { AboutDialog about{this}; about.exec(); } void MainWindow::ShowHotkeyDialog() { auto* hotkey_window = new MappingWindow(this, MappingWindow::Type::MAPPING_HOTKEYS, 0); InstallHotkeyFilter(hotkey_window); hotkey_window->show(); hotkey_window->raise(); hotkey_window->activateWindow(); } void MainWindow::ShowGraphicsWindow() { m_graphics_window->Initialize(); m_graphics_window->show(); m_graphics_window->raise(); m_graphics_window->activateWindow(); } void MainWindow::ShowNetPlaySetupDialog() { m_netplay_setup_dialog->show(); m_netplay_setup_dialog->raise(); m_netplay_setup_dialog->activateWindow(); } void MainWindow::ShowFIFOPlayer() { m_fifo_window->show(); m_fifo_window->raise(); m_fifo_window->activateWindow(); } void MainWindow::StateLoad() { QString path = QFileDialog::getOpenFileName(this, tr("Select a File"), QDir::currentPath(), tr("All Save States (*.sav *.s##);; All Files (*)")); State::LoadAs(path.toStdString()); } void MainWindow::StateSave() { QString path = QFileDialog::getSaveFileName(this, tr("Select a File"), QDir::currentPath(), tr("All Save States (*.sav *.s##);; All Files (*)")); State::SaveAs(path.toStdString()); } void MainWindow::StateLoadSlot() { State::Load(m_state_slot); } void MainWindow::StateSaveSlot() { State::Save(m_state_slot, true); m_menu_bar->UpdateStateSlotMenu(); } void MainWindow::StateLoadSlotAt(int slot) { State::Load(slot); } void MainWindow::StateLoadLastSavedAt(int slot) { State::LoadLastSaved(slot); } void MainWindow::StateSaveSlotAt(int slot) { State::Save(slot, true); m_menu_bar->UpdateStateSlotMenu(); } void MainWindow::StateLoadUndo() { State::UndoLoadState(); } void MainWindow::StateSaveUndo() { State::UndoSaveState(); } void MainWindow::StateSaveOldest() { State::SaveFirstSaved(); } void MainWindow::SetStateSlot(int slot) { Settings::Instance().SetStateSlot(slot); m_state_slot = slot; Core::DisplayMessage(StringFromFormat("Selected slot %d - %s", m_state_slot, State::GetInfoStringOfSlot(m_state_slot, false).c_str()), 2500); } void MainWindow::PerformOnlineUpdate(const std::string& region) { WiiUpdate::PerformOnlineUpdate(region, this); // Since the update may have installed a newer system menu, refresh the tools menu. m_menu_bar->UpdateToolsMenu(false); } void MainWindow::BootWiiSystemMenu() { StartGame(std::make_unique(BootParameters::NANDTitle{Titles::SYSTEM_MENU})); } void MainWindow::NetPlayInit() { m_netplay_setup_dialog = new NetPlaySetupDialog(this); m_netplay_dialog = new NetPlayDialog; connect(m_netplay_dialog, &NetPlayDialog::Boot, this, [this](const QString& path) { StartGame(path); }); connect(m_netplay_dialog, &NetPlayDialog::Stop, this, &MainWindow::ForceStop); connect(m_netplay_dialog, &NetPlayDialog::rejected, this, &MainWindow::NetPlayQuit); connect(m_netplay_setup_dialog, &NetPlaySetupDialog::Join, this, &MainWindow::NetPlayJoin); connect(m_netplay_setup_dialog, &NetPlaySetupDialog::Host, this, &MainWindow::NetPlayHost); } bool MainWindow::NetPlayJoin() { if (Core::IsRunning()) { QMessageBox::critical( nullptr, QObject::tr("Error"), QObject::tr("Can't start a NetPlay Session while a game is still running!")); return false; } if (m_netplay_dialog->isVisible()) { QMessageBox::critical(nullptr, QObject::tr("Error"), QObject::tr("A NetPlay Session is already in progress!")); return false; } // Settings const std::string traversal_choice = Config::Get(Config::NETPLAY_TRAVERSAL_CHOICE); const bool is_traversal = traversal_choice == "traversal"; std::string host_ip; u16 host_port; if (Settings::Instance().GetNetPlayServer()) { host_ip = "127.0.0.1"; host_port = Settings::Instance().GetNetPlayServer()->GetPort(); } else { host_ip = is_traversal ? Config::Get(Config::NETPLAY_HOST_CODE) : Config::Get(Config::NETPLAY_ADDRESS); host_port = Config::Get(Config::NETPLAY_CONNECT_PORT); } const std::string traversal_host = Config::Get(Config::NETPLAY_TRAVERSAL_SERVER); const u16 traversal_port = Config::Get(Config::NETPLAY_TRAVERSAL_PORT); const std::string nickname = Config::Get(Config::NETPLAY_NICKNAME); // Create Client const bool is_hosting_netplay = Settings::Instance().GetNetPlayServer() != nullptr; Settings::Instance().ResetNetPlayClient(new NetPlay::NetPlayClient( host_ip, host_port, m_netplay_dialog, nickname, NetPlay::NetTraversalConfig{is_hosting_netplay ? false : is_traversal, traversal_host, traversal_port})); if (!Settings::Instance().GetNetPlayClient()->IsConnected()) { NetPlayQuit(); return false; } if (Settings::Instance().GetNetPlayServer() != nullptr) Settings::Instance().GetNetPlayServer()->SetNetPlayUI(m_netplay_dialog); m_netplay_setup_dialog->close(); m_netplay_dialog->show(nickname, is_traversal); return true; } bool MainWindow::NetPlayHost(const QString& game_id) { if (Core::IsRunning()) { QMessageBox::critical( nullptr, QObject::tr("Error"), QObject::tr("Can't start a NetPlay Session while a game is still running!")); return false; } if (m_netplay_dialog->isVisible()) { QMessageBox::critical(nullptr, QObject::tr("Error"), QObject::tr("A NetPlay Session is already in progress!")); return false; } // Settings u16 host_port = Config::Get(Config::NETPLAY_HOST_PORT); const std::string traversal_choice = Config::Get(Config::NETPLAY_TRAVERSAL_CHOICE); const bool is_traversal = traversal_choice == "traversal"; const bool use_upnp = Config::Get(Config::NETPLAY_USE_UPNP); const std::string traversal_host = Config::Get(Config::NETPLAY_TRAVERSAL_SERVER); const u16 traversal_port = Config::Get(Config::NETPLAY_TRAVERSAL_PORT); if (is_traversal) host_port = Config::Get(Config::NETPLAY_LISTEN_PORT); // Create Server Settings::Instance().ResetNetPlayServer(new NetPlay::NetPlayServer( host_port, use_upnp, NetPlay::NetTraversalConfig{is_traversal, traversal_host, traversal_port})); if (!Settings::Instance().GetNetPlayServer()->is_connected) { QMessageBox::critical( nullptr, QObject::tr("Failed to open server"), QObject::tr( "Failed to listen on port %1. Is another instance of the NetPlay server running?") .arg(host_port)); NetPlayQuit(); return false; } Settings::Instance().GetNetPlayServer()->ChangeGame(game_id.toStdString()); // Join our local server return NetPlayJoin(); } void MainWindow::NetPlayQuit() { Settings::Instance().ResetNetPlayClient(); Settings::Instance().ResetNetPlayServer(); } void MainWindow::EnableScreenSaver(bool enable) { #if defined(HAVE_XRANDR) && HAVE_XRANDR UICommon::EnableScreenSaver(winId(), enable); #else UICommon::EnableScreenSaver(enable); #endif } bool MainWindow::eventFilter(QObject* object, QEvent* event) { if (event->type() == QEvent::Close) { if (RequestStop() && object == this) m_exit_requested = true; static_cast(event)->ignore(); return true; } return false; } void MainWindow::dragEnterEvent(QDragEnterEvent* event) { if (event->mimeData()->hasUrls() && event->mimeData()->urls().size() == 1) event->acceptProposedAction(); } void MainWindow::dropEvent(QDropEvent* event) { const auto& urls = event->mimeData()->urls(); if (urls.empty()) return; const auto& url = urls[0]; QFileInfo file_info(url.toLocalFile()); auto path = file_info.filePath(); if (!file_info.exists() || !file_info.isReadable()) { QMessageBox::critical(this, tr("Error"), tr("Failed to open '%1'").arg(path)); return; } if (file_info.isFile()) { StartGame(path); } else { auto& settings = Settings::Instance(); if (settings.GetPaths().size() != 0) { if (QMessageBox::question( this, tr("Confirm"), tr("Do you want to add \"%1\" to the list of Game Paths?").arg(path)) != QMessageBox::Yes) return; } settings.AddPath(path); } } QSize MainWindow::sizeHint() const { return QSize(800, 600); } void MainWindow::OnBootGameCubeIPL(DiscIO::Region region) { StartGame(std::make_unique(BootParameters::IPL{region})); } void MainWindow::OnImportNANDBackup() { auto response = QMessageBox::question( this, tr("Question"), tr("Merging a new NAND over your currently selected NAND will overwrite any channels " "and savegames that already exist. This process is not reversible, so it is " "recommended that you keep backups of both NANDs. Are you sure you want to " "continue?")); if (response == QMessageBox::No) return; QString file = QFileDialog::getOpenFileName(this, tr("Select the save file"), QDir::currentPath(), tr("BootMii NAND backup file (*.bin);;" "All Files (*)")); if (file.isEmpty()) return; QProgressDialog* dialog = new QProgressDialog(this); dialog->setMinimum(0); dialog->setMaximum(0); dialog->setLabelText(tr("Importing NAND backup")); dialog->setCancelButton(nullptr); auto beginning = QDateTime::currentDateTime().toMSecsSinceEpoch(); auto result = std::async(std::launch::async, [&] { DiscIO::NANDImporter().ImportNANDBin( file.toStdString(), [&dialog, beginning] { QueueOnObject(dialog, [&dialog, beginning] { dialog->setLabelText( tr("Importing NAND backup\n Time elapsed: %1s") .arg((QDateTime::currentDateTime().toMSecsSinceEpoch() - beginning) / 1000)); }); }, [this] { std::optional keys_file = RunOnObject(this, [this] { return QFileDialog::getOpenFileName(this, tr("Select the keys file (OTP/SEEPROM dump)"), QDir::currentPath(), tr("BootMii keys file (*.bin);;" "All Files (*)")) .toStdString(); }); if (keys_file) return *keys_file; return std::string(""); }); QueueOnObject(dialog, &QProgressDialog::close); }); dialog->exec(); result.wait(); m_menu_bar->UpdateToolsMenu(Core::IsRunning()); } void MainWindow::OnPlayRecording() { QString dtm_file = QFileDialog::getOpenFileName(this, tr("Select the Recording File"), QString(), tr("Dolphin TAS Movies (*.dtm)")); if (dtm_file.isEmpty()) return; if (!Movie::IsReadOnly()) { // let's make the read-only flag consistent at the start of a movie. Movie::SetReadOnly(true); emit ReadOnlyModeChanged(true); } std::optional savestate_path; if (Movie::PlayInput(dtm_file.toStdString(), &savestate_path)) { emit RecordingStatusChanged(true); Play(savestate_path); } } void MainWindow::OnStartRecording() { if ((!Core::IsRunningAndStarted() && Core::IsRunning()) || Movie::IsRecordingInput() || Movie::IsPlayingInput()) return; if (Movie::IsReadOnly()) { // The user just chose to record a movie, so that should take precedence Movie::SetReadOnly(false); emit ReadOnlyModeChanged(true); } int controllers = 0; for (int i = 0; i < 4; i++) { if (SerialInterface::SIDevice_IsGCController(SConfig::GetInstance().m_SIDevice[i])) controllers |= (1 << i); if (g_wiimote_sources[i] != WIIMOTE_SRC_NONE) controllers |= (1 << (i + 4)); } if (Movie::BeginRecordingInput(controllers)) { emit RecordingStatusChanged(true); if (!Core::IsRunning()) Play(); } } void MainWindow::OnStopRecording() { if (Movie::IsRecordingInput()) OnExportRecording(); Movie::EndPlayInput(false); emit RecordingStatusChanged(true); } void MainWindow::OnExportRecording() { bool was_paused = Core::GetState() == Core::State::Paused; if (was_paused) Core::SetState(Core::State::Paused); QString dtm_file = QFileDialog::getSaveFileName(this, tr("Select the Recording File"), QString(), tr("Dolphin TAS Movies (*.dtm)")); if (was_paused) Core::SetState(Core::State::Running); if (dtm_file.isEmpty()) return; Core::SetState(Core::State::Running); Movie::SaveRecording(dtm_file.toStdString()); } void MainWindow::ShowTASInput() { for (int i = 0; i < num_gc_controllers; i++) { if (SConfig::GetInstance().m_SIDevice[i] != SerialInterface::SIDEVICE_NONE && SConfig::GetInstance().m_SIDevice[i] != SerialInterface::SIDEVICE_GC_GBA) { m_gc_tas_input_windows[i]->show(); m_gc_tas_input_windows[i]->raise(); m_gc_tas_input_windows[i]->activateWindow(); } } for (int i = 0; i < num_wii_controllers; i++) { if (g_wiimote_sources[i] == WIIMOTE_SRC_EMU && (!Core::IsRunning() || SConfig::GetInstance().bWii)) { m_wii_tas_input_windows[i]->show(); m_wii_tas_input_windows[i]->raise(); m_wii_tas_input_windows[i]->activateWindow(); } } } void MainWindow::OnConnectWiiRemote(int id) { const auto ios = IOS::HLE::GetIOS(); if (!ios || SConfig::GetInstance().m_bt_passthrough_enabled) return; Core::RunAsCPUThread([&] { const auto bt = std::static_pointer_cast( ios->GetDeviceByName("/dev/usb/oh1/57e/305")); const bool is_connected = bt && bt->AccessWiimoteByIndex(id)->IsConnected(); Wiimote::Connect(id, !is_connected); }); } void MainWindow::ShowMemcardManager() { GCMemcardManager manager(this); manager.exec(); } void MainWindow::ShowCheatsManager() { m_cheats_manager->show(); } void MainWindow::OnUpdateProgressDialog(QString title, int progress, int total) { if (!m_progress_dialog) { m_progress_dialog = new QProgressDialog(m_render_widget); m_progress_dialog->show(); } m_progress_dialog->setValue(progress); m_progress_dialog->setLabelText(title); m_progress_dialog->setWindowTitle(title); m_progress_dialog->setMaximum(total); if (total < 0 || progress >= total) { m_progress_dialog->hide(); m_progress_dialog->deleteLater(); m_progress_dialog = nullptr; } } void MainWindow::Show() { QWidget::show(); // If the booting of a game was requested on start up, do that now if (m_pending_boot != nullptr) { StartGame(std::move(m_pending_boot)); m_pending_boot.reset(); } }