diff options
Diffstat (limited to 'src/yuzu')
-rw-r--r-- | src/yuzu/bootmanager.cpp | 117 | ||||
-rw-r--r-- | src/yuzu/bootmanager.h | 61 | ||||
-rw-r--r-- | src/yuzu/configuration/config.cpp | 4 | ||||
-rw-r--r-- | src/yuzu/configuration/configure_input_player.cpp | 12 | ||||
-rw-r--r-- | src/yuzu/configuration/configure_input_player.h | 3 | ||||
-rw-r--r-- | src/yuzu/configuration/configure_input_player.ui | 19 | ||||
-rw-r--r-- | src/yuzu/configuration/configure_system.cpp | 4 | ||||
-rw-r--r-- | src/yuzu/configuration/configure_system.ui | 14 | ||||
-rw-r--r-- | src/yuzu/game_list.cpp | 14 | ||||
-rw-r--r-- | src/yuzu/game_list.h | 7 | ||||
-rw-r--r-- | src/yuzu/main.cpp | 365 | ||||
-rw-r--r-- | src/yuzu/main.h | 16 | ||||
-rw-r--r-- | src/yuzu/startup_checks.cpp | 2 | ||||
-rw-r--r-- | src/yuzu/uisettings.h | 1 | ||||
-rw-r--r-- | src/yuzu/util/overlay_dialog.cpp | 21 | ||||
-rw-r--r-- | src/yuzu/util/overlay_dialog.h | 1 |
16 files changed, 447 insertions, 214 deletions
diff --git a/src/yuzu/bootmanager.cpp b/src/yuzu/bootmanager.cpp index 5b5b6fed8..3d560f303 100644 --- a/src/yuzu/bootmanager.cpp +++ b/src/yuzu/bootmanager.cpp @@ -44,32 +44,30 @@ #include "yuzu/bootmanager.h" #include "yuzu/main.h" -EmuThread::EmuThread(Core::System& system_) : system{system_} {} +static Core::Frontend::WindowSystemType GetWindowSystemType(); + +EmuThread::EmuThread(Core::System& system) : m_system{system} {} EmuThread::~EmuThread() = default; void EmuThread::run() { - std::string name = "EmuControlThread"; - MicroProfileOnThreadCreate(name.c_str()); - Common::SetCurrentThreadName(name.c_str()); + const char* name = "EmuControlThread"; + MicroProfileOnThreadCreate(name); + Common::SetCurrentThreadName(name); - auto& gpu = system.GPU(); - auto stop_token = stop_source.get_token(); - bool debugger_should_start = system.DebuggerEnabled(); + auto& gpu = m_system.GPU(); + auto stop_token = m_stop_source.get_token(); - system.RegisterHostThread(); + m_system.RegisterHostThread(); // Main process has been loaded. Make the context current to this thread and begin GPU and CPU // execution. - gpu.Start(); - gpu.ObtainContext(); emit LoadProgress(VideoCore::LoadCallbackStage::Prepare, 0, 0); - if (Settings::values.use_disk_shader_cache.GetValue()) { - system.Renderer().ReadRasterizer()->LoadDiskResources( - system.GetCurrentProcessProgramID(), stop_token, + m_system.Renderer().ReadRasterizer()->LoadDiskResources( + m_system.GetCurrentProcessProgramID(), stop_token, [this](VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total) { emit LoadProgress(stage, value, total); }); @@ -77,53 +75,36 @@ void EmuThread::run() { emit LoadProgress(VideoCore::LoadCallbackStage::Complete, 0, 0); gpu.ReleaseContext(); + gpu.Start(); - system.GetCpuManager().OnGpuReady(); - - // Holds whether the cpu was running during the last iteration, - // so that the DebugModeLeft signal can be emitted before the - // next execution step - bool was_active = false; - while (!stop_token.stop_requested()) { - if (running) { - if (was_active) { - emit DebugModeLeft(); - } - - running_guard = true; - Core::SystemResultStatus result = system.Run(); - if (result != Core::SystemResultStatus::Success) { - running_guard = false; - this->SetRunning(false); - emit ErrorThrown(result, system.GetStatusDetails()); - } + m_system.GetCpuManager().OnGpuReady(); - if (debugger_should_start) { - system.InitializeDebugger(); - debugger_should_start = false; - } + if (m_system.DebuggerEnabled()) { + m_system.InitializeDebugger(); + } - running_wait.Wait(); - result = system.Pause(); - if (result != Core::SystemResultStatus::Success) { - running_guard = false; - this->SetRunning(false); - emit ErrorThrown(result, system.GetStatusDetails()); - } - running_guard = false; + while (!stop_token.stop_requested()) { + std::unique_lock lk{m_should_run_mutex}; + if (m_should_run) { + m_system.Run(); + m_is_running.store(true); + m_is_running.notify_all(); - if (!stop_token.stop_requested()) { - was_active = true; - emit DebugModeEntered(); - } + Common::CondvarWait(m_should_run_cv, lk, stop_token, [&] { return !m_should_run; }); } else { - std::unique_lock lock{running_mutex}; - Common::CondvarWait(running_cv, lock, stop_token, [&] { return IsRunning(); }); + m_system.Pause(); + m_is_running.store(false); + m_is_running.notify_all(); + + emit DebugModeEntered(); + Common::CondvarWait(m_should_run_cv, lk, stop_token, [&] { return m_should_run; }); + emit DebugModeLeft(); } } // Shutdown the main emulated process - system.ShutdownMainProcess(); + m_system.DetachDebugger(); + m_system.ShutdownMainProcess(); #if MICROPROFILE_ENABLED MicroProfileOnThreadExit(); @@ -225,6 +206,9 @@ public: explicit RenderWidget(GRenderWindow* parent) : QWidget(parent), render_window(parent) { setAttribute(Qt::WA_NativeWindow); setAttribute(Qt::WA_PaintOnScreen); + if (GetWindowSystemType() == Core::Frontend::WindowSystemType::Wayland) { + setAttribute(Qt::WA_DontCreateNativeAncestors); + } } virtual ~RenderWidget() = default; @@ -269,12 +253,14 @@ static Core::Frontend::WindowSystemType GetWindowSystemType() { return Core::Frontend::WindowSystemType::X11; else if (platform_name == QStringLiteral("wayland")) return Core::Frontend::WindowSystemType::Wayland; + else if (platform_name == QStringLiteral("wayland-egl")) + return Core::Frontend::WindowSystemType::Wayland; else if (platform_name == QStringLiteral("cocoa")) return Core::Frontend::WindowSystemType::Cocoa; else if (platform_name == QStringLiteral("android")) return Core::Frontend::WindowSystemType::Android; - LOG_CRITICAL(Frontend, "Unknown Qt platform!"); + LOG_CRITICAL(Frontend, "Unknown Qt platform {}!", platform_name.toStdString()); return Core::Frontend::WindowSystemType::Windows; } @@ -314,6 +300,9 @@ GRenderWindow::GRenderWindow(GMainWindow* parent, EmuThread* emu_thread_, input_subsystem->Initialize(); this->setMouseTracking(true); + strict_context_required = QGuiApplication::platformName() == QStringLiteral("wayland") || + QGuiApplication::platformName() == QStringLiteral("wayland-egl"); + connect(this, &GRenderWindow::FirstFrameDisplayed, parent, &GMainWindow::OnLoadComplete); connect(this, &GRenderWindow::ExecuteProgramSignal, parent, &GMainWindow::OnExecuteProgram, Qt::QueuedConnection); @@ -750,6 +739,9 @@ void GRenderWindow::InitializeCamera() { return; } + const auto camera_width = input_subsystem->GetCamera()->getImageWidth(); + const auto camera_height = input_subsystem->GetCamera()->getImageHeight(); + camera_data.resize(camera_width * camera_height); camera_capture->setCaptureDestination(QCameraImageCapture::CaptureDestination::CaptureToBuffer); connect(camera_capture.get(), &QCameraImageCapture::imageCaptured, this, &GRenderWindow::OnCameraCapture); @@ -805,17 +797,22 @@ void GRenderWindow::RequestCameraCapture() { } void GRenderWindow::OnCameraCapture(int requestId, const QImage& img) { - constexpr std::size_t camera_width = 320; - constexpr std::size_t camera_height = 240; +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) && YUZU_USE_QT_MULTIMEDIA + // TODO: Capture directly in the format and resolution needed + const auto camera_width = input_subsystem->GetCamera()->getImageWidth(); + const auto camera_height = input_subsystem->GetCamera()->getImageHeight(); const auto converted = - img.scaled(camera_width, camera_height, Qt::AspectRatioMode::IgnoreAspectRatio, + img.scaled(static_cast<int>(camera_width), static_cast<int>(camera_height), + Qt::AspectRatioMode::IgnoreAspectRatio, Qt::TransformationMode::SmoothTransformation) .mirrored(false, true); - std::vector<u32> camera_data{}; - camera_data.resize(camera_width * camera_height); + if (camera_data.size() != camera_width * camera_height) { + camera_data.resize(camera_width * camera_height); + } std::memcpy(camera_data.data(), converted.bits(), camera_width * camera_height * sizeof(u32)); input_subsystem->GetCamera()->SetCameraData(camera_width, camera_height, camera_data); pending_camera_snapshots = 0; +#endif } bool GRenderWindow::event(QEvent* event) { @@ -952,6 +949,12 @@ void GRenderWindow::OnMinimalClientAreaChangeRequest(std::pair<u32, u32> minimal bool GRenderWindow::InitializeOpenGL() { #ifdef HAS_OPENGL + if (!QOpenGLContext::supportsThreadedOpenGL()) { + QMessageBox::warning(this, tr("OpenGL not available!"), + tr("OpenGL shared contexts are not supported.")); + return false; + } + // TODO: One of these flags might be interesting: WA_OpaquePaintEvent, WA_NoBackground, // WA_DontShowOnScreen, WA_DeleteOnClose auto child = new OpenGLRenderWidget(this); diff --git a/src/yuzu/bootmanager.h b/src/yuzu/bootmanager.h index f4deae4ee..eca16b313 100644 --- a/src/yuzu/bootmanager.h +++ b/src/yuzu/bootmanager.h @@ -47,7 +47,7 @@ class EmuThread final : public QThread { Q_OBJECT public: - explicit EmuThread(Core::System& system_); + explicit EmuThread(Core::System& system); ~EmuThread() override; /** @@ -57,48 +57,48 @@ public: void run() override; /** - * Sets whether the emulation thread is running or not - * @param running_ Boolean value, set the emulation thread to running if true - * @note This function is thread-safe + * Sets whether the emulation thread should run or not + * @param should_run Boolean value, set the emulation thread to running if true */ - void SetRunning(bool running_) { - std::unique_lock lock{running_mutex}; - running = running_; - lock.unlock(); - running_cv.notify_all(); - if (!running) { - running_wait.Set(); - /// Wait until effectively paused - while (running_guard) - ; + void SetRunning(bool should_run) { + // TODO: Prevent other threads from modifying the state until we finish. + { + // Notify the running thread to change state. + std::unique_lock run_lk{m_should_run_mutex}; + m_should_run = should_run; + m_should_run_cv.notify_one(); + } + + // Wait until paused, if pausing. + if (!should_run) { + m_is_running.wait(true); } } /** * Check if the emulation thread is running or not * @return True if the emulation thread is running, otherwise false - * @note This function is thread-safe */ bool IsRunning() const { - return running; + return m_is_running.load() || m_should_run; } /** - * Requests for the emulation thread to stop running + * Requests for the emulation thread to immediately stop running */ - void RequestStop() { - stop_source.request_stop(); - SetRunning(false); + void ForceStop() { + LOG_WARNING(Frontend, "Force stopping EmuThread"); + m_stop_source.request_stop(); } private: - bool running = false; - std::stop_source stop_source; - std::mutex running_mutex; - std::condition_variable_any running_cv; - Common::Event running_wait{}; - std::atomic_bool running_guard{false}; - Core::System& system; + Core::System& m_system; + + std::stop_source m_stop_source; + std::mutex m_should_run_mutex; + std::condition_variable_any m_should_run_cv; + std::atomic<bool> m_is_running{false}; + bool m_should_run{true}; signals: /** @@ -119,8 +119,6 @@ signals: */ void DebugModeLeft(); - void ErrorThrown(Core::SystemResultStatus, std::string); - void LoadProgress(VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total); }; @@ -241,13 +239,14 @@ private: bool first_frame = false; InputCommon::TasInput::TasState last_tas_state; +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) && YUZU_USE_QT_MULTIMEDIA bool is_virtual_camera; int pending_camera_snapshots; -#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) && YUZU_USE_QT_MULTIMEDIA + std::vector<u32> camera_data; std::unique_ptr<QCamera> camera; std::unique_ptr<QCameraImageCapture> camera_capture; -#endif std::unique_ptr<QTimer> camera_timer; +#endif Core::System& system; diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index 722fc708e..2ea4f367b 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -697,7 +697,6 @@ void Config::ReadRendererValues() { ReadGlobalSetting(Settings::values.fsr_sharpening_slider); ReadGlobalSetting(Settings::values.anti_aliasing); ReadGlobalSetting(Settings::values.max_anisotropy); - ReadGlobalSetting(Settings::values.use_speed_limit); ReadGlobalSetting(Settings::values.speed_limit); ReadGlobalSetting(Settings::values.use_disk_shader_cache); ReadGlobalSetting(Settings::values.gpu_accuracy); @@ -796,6 +795,7 @@ void Config::ReadSystemValues() { } else { Settings::values.custom_rtc = std::nullopt; } + ReadBasicSetting(Settings::values.device_name); } ReadGlobalSetting(Settings::values.sound_index); @@ -1328,7 +1328,6 @@ void Config::SaveRendererValues() { static_cast<u32>(Settings::values.anti_aliasing.GetDefault()), Settings::values.anti_aliasing.UsingGlobal()); WriteGlobalSetting(Settings::values.max_anisotropy); - WriteGlobalSetting(Settings::values.use_speed_limit); WriteGlobalSetting(Settings::values.speed_limit); WriteGlobalSetting(Settings::values.use_disk_shader_cache); WriteSetting(QString::fromStdString(Settings::values.gpu_accuracy.GetLabel()), @@ -1415,6 +1414,7 @@ void Config::SaveSystemValues() { false); WriteSetting(QStringLiteral("custom_rtc"), QVariant::fromValue<long long>(Settings::values.custom_rtc.value_or(0)), 0); + WriteBasicSetting(Settings::values.device_name); } WriteGlobalSetting(Settings::values.sound_index); diff --git a/src/yuzu/configuration/configure_input_player.cpp b/src/yuzu/configuration/configure_input_player.cpp index b1575b0d3..183cbe562 100644 --- a/src/yuzu/configuration/configure_input_player.cpp +++ b/src/yuzu/configuration/configure_input_player.cpp @@ -738,13 +738,10 @@ ConfigureInputPlayer::ConfigureInputPlayer(QWidget* parent, std::size_t player_i connect(ui->comboDevices, qOverload<int>(&QComboBox::activated), this, &ConfigureInputPlayer::UpdateMappingWithDefaults); + ui->comboDevices->installEventFilter(this); ui->comboDevices->setCurrentIndex(-1); - ui->buttonRefreshDevices->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); - connect(ui->buttonRefreshDevices, &QPushButton::clicked, - [this] { emit RefreshInputDevices(); }); - timeout_timer->setSingleShot(true); connect(timeout_timer.get(), &QTimer::timeout, [this] { SetPollingResult({}, true); }); @@ -1479,6 +1476,13 @@ void ConfigureInputPlayer::keyPressEvent(QKeyEvent* event) { } } +bool ConfigureInputPlayer::eventFilter(QObject* object, QEvent* event) { + if (object == ui->comboDevices && event->type() == QEvent::MouseButtonPress) { + RefreshInputDevices(); + } + return object->eventFilter(object, event); +} + void ConfigureInputPlayer::CreateProfile() { const auto profile_name = LimitableInputDialog::GetText(this, tr("New Profile"), tr("Enter a profile name:"), 1, 30, diff --git a/src/yuzu/configuration/configure_input_player.h b/src/yuzu/configuration/configure_input_player.h index 26f60d121..6d1876f2b 100644 --- a/src/yuzu/configuration/configure_input_player.h +++ b/src/yuzu/configuration/configure_input_player.h @@ -119,6 +119,9 @@ private: /// Handle key press events. void keyPressEvent(QKeyEvent* event) override; + /// Handle combobox list refresh + bool eventFilter(QObject* object, QEvent* event) override; + /// Update UI to reflect current configuration. void UpdateUI(); diff --git a/src/yuzu/configuration/configure_input_player.ui b/src/yuzu/configuration/configure_input_player.ui index a62b57501..a9567c6ee 100644 --- a/src/yuzu/configuration/configure_input_player.ui +++ b/src/yuzu/configuration/configure_input_player.ui @@ -122,25 +122,6 @@ </property> </widget> </item> - <item> - <widget class="QPushButton" name="buttonRefreshDevices"> - <property name="minimumSize"> - <size> - <width>21</width> - <height>21</height> - </size> - </property> - <property name="maximumSize"> - <size> - <width>21</width> - <height>21</height> - </size> - </property> - <property name="styleSheet"> - <string notr="true"/> - </property> - </widget> - </item> </layout> </widget> </item> diff --git a/src/yuzu/configuration/configure_system.cpp b/src/yuzu/configuration/configure_system.cpp index bc9d9d77a..9b14e5903 100644 --- a/src/yuzu/configuration/configure_system.cpp +++ b/src/yuzu/configuration/configure_system.cpp @@ -72,6 +72,8 @@ void ConfigureSystem::SetConfiguration() { ui->custom_rtc_checkbox->setChecked(Settings::values.custom_rtc.has_value()); ui->custom_rtc_edit->setEnabled(Settings::values.custom_rtc.has_value()); ui->custom_rtc_edit->setDateTime(QDateTime::fromSecsSinceEpoch(rtc_time)); + ui->device_name_edit->setText( + QString::fromUtf8(Settings::values.device_name.GetValue().c_str())); if (Settings::IsConfiguringGlobal()) { ui->combo_language->setCurrentIndex(Settings::values.language_index.GetValue()); @@ -115,6 +117,8 @@ void ConfigureSystem::ApplyConfiguration() { } } + Settings::values.device_name = ui->device_name_edit->text().toStdString(); + if (!enabled) { return; } diff --git a/src/yuzu/configuration/configure_system.ui b/src/yuzu/configuration/configure_system.ui index b234ea87b..46892f5c1 100644 --- a/src/yuzu/configuration/configure_system.ui +++ b/src/yuzu/configuration/configure_system.ui @@ -432,6 +432,13 @@ </property> </widget> </item> + <item row="7" column="0"> + <widget class="QLabel" name="device_name_label"> + <property name="text"> + <string>Device Name</string> + </property> + </widget> + </item> <item row="3" column="1"> <widget class="QComboBox" name="combo_sound"> <item> @@ -476,6 +483,13 @@ </property> </widget> </item> + <item row="7" column="1"> + <widget class="QLineEdit" name="device_name_edit"> + <property name="maxLength"> + <number>128</number> + </property> + </widget> + </item> <item row="6" column="1"> <widget class="QLineEdit" name="rng_seed_edit"> <property name="sizePolicy"> diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index 5c33c1b0f..22aa19c56 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -554,6 +554,12 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri QAction* dump_romfs_sdmc = dump_romfs_menu->addAction(tr("Dump RomFS to SDMC")); QAction* copy_tid = context_menu.addAction(tr("Copy Title ID to Clipboard")); QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); +#ifndef WIN32 + QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut")); + QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop")); + QAction* create_applications_menu_shortcut = + shortcut_menu->addAction(tr("Add to Applications Menu")); +#endif context_menu.addSeparator(); QAction* properties = context_menu.addAction(tr("Properties")); @@ -619,6 +625,14 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); }); +#ifndef WIN32 + connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path]() { + emit CreateShortcut(program_id, path, GameListShortcutTarget::Desktop); + }); + connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path]() { + emit CreateShortcut(program_id, path, GameListShortcutTarget::Applications); + }); +#endif connect(properties, &QAction::triggered, [this, path]() { emit OpenPerGameGeneralRequested(path); }); }; diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h index cdf085019..f7ff93ed9 100644 --- a/src/yuzu/game_list.h +++ b/src/yuzu/game_list.h @@ -52,6 +52,11 @@ enum class DumpRomFSTarget { SDMC, }; +enum class GameListShortcutTarget { + Desktop, + Applications, +}; + enum class InstalledEntryType { Game, Update, @@ -108,6 +113,8 @@ signals: const std::string& game_path); void DumpRomFSRequested(u64 program_id, const std::string& game_path, DumpRomFSTarget target); void CopyTIDRequested(u64 program_id); + void CreateShortcut(u64 program_id, const std::string& game_path, + GameListShortcutTarget target); void NavigateToGamedbEntryRequested(u64 program_id, const CompatibilityList& compatibility_list); void OpenPerGameGeneralRequested(const std::string& file); diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index b11b26f7b..524650144 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -4,6 +4,8 @@ #include <cinttypes> #include <clocale> #include <cmath> +#include <fstream> +#include <iostream> #include <memory> #include <thread> #ifdef __APPLE__ @@ -1249,6 +1251,7 @@ void GMainWindow::ConnectWidgetEvents() { connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID); connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, &GMainWindow::OnGameListNavigateToGamedbEntry); + connect(game_list, &GameList::CreateShortcut, this, &GMainWindow::OnGameListCreateShortcut); connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); @@ -1495,7 +1498,7 @@ void GMainWindow::SetupSigInterrupts() { void GMainWindow::HandleSigInterrupt(int sig) { if (sig == SIGINT) { - exit(1); + _exit(1); } // Calling into Qt directly from a signal handler is not safe, @@ -1547,8 +1550,9 @@ void GMainWindow::AllowOSSleep() { bool GMainWindow::LoadROM(const QString& filename, u64 program_id, std::size_t program_index) { // Shutdown previous session if the emu thread is still active... - if (emu_thread != nullptr) + if (emu_thread != nullptr) { ShutdownGame(); + } if (!render_window->InitRenderTarget()) { return false; @@ -1707,8 +1711,10 @@ void GMainWindow::BootGame(const QString& filename, u64 program_id, std::size_t system->RegisterExecuteProgramCallback( [this](std::size_t program_index_) { render_window->ExecuteProgram(program_index_); }); - // Register an Exit callback such that Core can exit the currently running application. - system->RegisterExitCallback([this]() { render_window->Exit(); }); + system->RegisterExitCallback([this] { + emu_thread->ForceStop(); + render_window->Exit(); + }); connect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); connect(render_window, &GRenderWindow::MouseActivity, this, &GMainWindow::OnMouseActivity); @@ -1779,9 +1785,9 @@ void GMainWindow::BootGame(const QString& filename, u64 program_id, std::size_t OnStartGame(); } -void GMainWindow::ShutdownGame() { +bool GMainWindow::OnShutdownBegin() { if (!emulation_running) { - return; + return false; } if (ui->action_Fullscreen->isChecked()) { @@ -1790,17 +1796,58 @@ void GMainWindow::ShutdownGame() { AllowOSSleep(); + // Disable unlimited frame rate + Settings::values.use_speed_limit.SetValue(true); + + if (system->IsShuttingDown()) { + return false; + } + system->SetShuttingDown(true); - system->DetachDebugger(); discord_rpc->Pause(); - emu_thread->RequestStop(); + + RequestGameExit(); + emu_thread->disconnect(); + emu_thread->SetRunning(true); emit EmulationStopping(); - // Wait for emulation thread to complete and delete it + shutdown_timer.setSingleShot(true); + shutdown_timer.start(system->DebuggerEnabled() ? 0 : 5000); + connect(&shutdown_timer, &QTimer::timeout, this, &GMainWindow::OnEmulationStopTimeExpired); + connect(emu_thread.get(), &QThread::finished, this, &GMainWindow::OnEmulationStopped); + + // Disable everything to prevent anything from being triggered here + ui->action_Pause->setEnabled(false); + ui->action_Restart->setEnabled(false); + ui->action_Stop->setEnabled(false); + + return true; +} + +void GMainWindow::OnShutdownBeginDialog() { + shutdown_dialog = new OverlayDialog(this, *system, QString{}, tr("Closing software..."), + QString{}, QString{}, Qt::AlignHCenter | Qt::AlignVCenter); + shutdown_dialog->open(); +} + +void GMainWindow::OnEmulationStopTimeExpired() { + if (emu_thread) { + emu_thread->ForceStop(); + } +} + +void GMainWindow::OnEmulationStopped() { + shutdown_timer.stop(); + emu_thread->disconnect(); emu_thread->wait(); emu_thread = nullptr; + if (shutdown_dialog) { + shutdown_dialog->deleteLater(); + shutdown_dialog = nullptr; + } + emulation_running = false; discord_rpc->Update(); @@ -1846,6 +1893,20 @@ void GMainWindow::ShutdownGame() { // When closing the game, destroy the GLWindow to clear the context after the game is closed render_window->ReleaseRenderTarget(); + + Settings::RestoreGlobalState(system->IsPoweredOn()); + system->HIDCore().ReloadInputDevices(); + UpdateStatusButtons(); +} + +void GMainWindow::ShutdownGame() { + if (!emulation_running) { + return; + } + + OnShutdownBegin(); + OnEmulationStopTimeExpired(); + OnEmulationStopped(); } void GMainWindow::StoreRecentFile(const QString& filename) { @@ -2375,6 +2436,152 @@ void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id, QDesktopServices::openUrl(QUrl(QStringLiteral("https://yuzu-emu.org/game/") + directory)); } +void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& game_path, + GameListShortcutTarget target) { + // Get path to yuzu executable + const QStringList args = QApplication::arguments(); + std::filesystem::path yuzu_command = args[0].toStdString(); + +#if defined(__linux__) || defined(__FreeBSD__) + // If relative path, make it an absolute path + if (yuzu_command.c_str()[0] == '.') { + yuzu_command = Common::FS::GetCurrentDir() / yuzu_command; + } + +#if defined(__linux__) + // Warn once if we are making a shortcut to a volatile AppImage + const std::string appimage_ending = + std::string(Common::g_scm_rev).substr(0, 9).append(".AppImage"); + if (yuzu_command.string().ends_with(appimage_ending) && + !UISettings::values.shortcut_already_warned) { + if (QMessageBox::warning(this, tr("Create Shortcut"), + tr("This will create a shortcut to the current AppImage. This may " + "not work well if you update. Continue?"), + QMessageBox::StandardButton::Ok | + QMessageBox::StandardButton::Cancel) == + QMessageBox::StandardButton::Cancel) { + return; + } + UISettings::values.shortcut_already_warned = true; + } +#endif // __linux__ +#endif // __linux__ || __FreeBSD__ + + std::filesystem::path target_directory{}; + // Determine target directory for shortcut +#if defined(__linux__) || defined(__FreeBSD__) + const char* home = std::getenv("HOME"); + const std::filesystem::path home_path = (home == nullptr ? "~" : home); + const char* xdg_data_home = std::getenv("XDG_DATA_HOME"); + + if (target == GameListShortcutTarget::Desktop) { + target_directory = home_path / "Desktop"; + if (!Common::FS::IsDir(target_directory)) { + QMessageBox::critical( + this, tr("Create Shortcut"), + tr("Cannot create shortcut on desktop. Path \"%1\" does not exist.") + .arg(QString::fromStdString(target_directory)), + QMessageBox::StandardButton::Ok); + return; + } + } else if (target == GameListShortcutTarget::Applications) { + target_directory = (xdg_data_home == nullptr ? home_path / ".local/share" : xdg_data_home) / + "applications"; + if (!Common::FS::CreateDirs(target_directory)) { + QMessageBox::critical(this, tr("Create Shortcut"), + tr("Cannot create shortcut in applications menu. Path \"%1\" " + "does not exist and cannot be created.") + .arg(QString::fromStdString(target_directory)), + QMessageBox::StandardButton::Ok); + return; + } + } +#endif + + const std::string game_file_name = std::filesystem::path(game_path).filename().string(); + // Determine full paths for icon and shortcut +#if defined(__linux__) || defined(__FreeBSD__) + std::filesystem::path system_icons_path = + (xdg_data_home == nullptr ? home_path / ".local/share/" : xdg_data_home) / + "icons/hicolor/256x256"; + if (!Common::FS::CreateDirs(system_icons_path)) { + QMessageBox::critical( + this, tr("Create Icon"), + tr("Cannot create icon file. Path \"%1\" does not exist and cannot be created.") + .arg(QString::fromStdString(system_icons_path)), + QMessageBox::StandardButton::Ok); + return; + } + std::filesystem::path icon_path = + system_icons_path / (program_id == 0 ? fmt::format("yuzu-{}.png", game_file_name) + : fmt::format("yuzu-{:016X}.png", program_id)); + const std::filesystem::path shortcut_path = + target_directory / (program_id == 0 ? fmt::format("yuzu-{}.desktop", game_file_name) + : fmt::format("yuzu-{:016X}.desktop", program_id)); +#else + const std::filesystem::path icon_path{}; + const std::filesystem::path shortcut_path{}; +#endif + + // Get title from game file + const FileSys::PatchManager pm{program_id, system->GetFileSystemController(), + system->GetContentProvider()}; + const auto control = pm.GetControlMetadata(); + const auto loader = Loader::GetLoader(*system, vfs->OpenFile(game_path, FileSys::Mode::Read)); + + std::string title{fmt::format("{:016X}", program_id)}; + + if (control.first != nullptr) { + title = control.first->GetApplicationName(); + } else { + loader->ReadTitle(title); + } + + // Get icon from game file + std::vector<u8> icon_image_file{}; + if (control.second != nullptr) { + icon_image_file = control.second->ReadAllBytes(); + } else if (loader->ReadIcon(icon_image_file) != Loader::ResultStatus::Success) { + LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path); + } + + QImage icon_jpeg = + QImage::fromData(icon_image_file.data(), static_cast<int>(icon_image_file.size())); +#if defined(__linux__) || defined(__FreeBSD__) + // Convert and write the icon as a PNG + if (!icon_jpeg.save(QString::fromStdString(icon_path.string()))) { + LOG_ERROR(Frontend, "Could not write icon as PNG to file"); + } else { + LOG_INFO(Frontend, "Wrote an icon to {}", icon_path.string()); + } +#endif // __linux__ + +#if defined(__linux__) || defined(__FreeBSD__) + const std::string comment = + tr("Start %1 with the yuzu Emulator").arg(QString::fromStdString(title)).toStdString(); + const std::string arguments = fmt::format("-g \"{:s}\"", game_path); + const std::string categories = "Game;Emulator;Qt;"; + const std::string keywords = "Switch;Nintendo;"; +#else + const std::string comment{}; + const std::string arguments{}; + const std::string categories{}; + const std::string keywords{}; +#endif + if (!CreateShortcut(shortcut_path.string(), title, comment, icon_path.string(), + yuzu_command.string(), arguments, categories, keywords)) { + QMessageBox::critical(this, tr("Create Shortcut"), + tr("Failed to create a shortcut at %1") + .arg(QString::fromStdString(shortcut_path.string()))); + return; + } + + LOG_INFO(Frontend, "Wrote a shortcut to {}", shortcut_path.string()); + QMessageBox::information( + this, tr("Create Shortcut"), + tr("Successfully created a shortcut to %1").arg(QString::fromStdString(title))); +} + void GMainWindow::OnGameListOpenDirectory(const QString& directory) { std::filesystem::path fs_path; if (directory == QStringLiteral("SDMC")) { @@ -2508,6 +2715,9 @@ void GMainWindow::OnMenuInstallToNAND() { return; } + // Save folder location of the first selected file + UISettings::values.roms_path = QFileInfo(filenames[0]).path(); + int remaining = filenames.size(); // This would only overflow above 2^43 bytes (8.796 TB) @@ -2763,8 +2973,6 @@ void GMainWindow::OnStartGame() { emu_thread->SetRunning(true); - connect(emu_thread.get(), &EmuThread::ErrorThrown, this, &GMainWindow::OnCoreError); - UpdateMenuState(); OnTasStateChanged(); @@ -2801,11 +3009,9 @@ void GMainWindow::OnStopGame() { return; } - ShutdownGame(); - - Settings::RestoreGlobalState(system->IsPoweredOn()); - system->HIDCore().ReloadInputDevices(); - UpdateStatusButtons(); + if (OnShutdownBegin()) { + OnShutdownBeginDialog(); + } } void GMainWindow::OnLoadComplete() { @@ -2912,9 +3118,15 @@ static QScreen* GuessCurrentScreen(QWidget* window) { }); } +bool GMainWindow::UsingExclusiveFullscreen() { + return Settings::values.fullscreen_mode.GetValue() == Settings::FullscreenMode::Exclusive || + QGuiApplication::platformName() == QStringLiteral("wayland") || + QGuiApplication::platformName() == QStringLiteral("wayland-egl"); +} + void GMainWindow::ShowFullscreen() { - const auto show_fullscreen = [](QWidget* window) { - if (Settings::values.fullscreen_mode.GetValue() == Settings::FullscreenMode::Exclusive) { + const auto show_fullscreen = [this](QWidget* window) { + if (UsingExclusiveFullscreen()) { window->showFullScreen(); return; } @@ -2942,7 +3154,7 @@ void GMainWindow::ShowFullscreen() { void GMainWindow::HideFullscreen() { if (ui->action_Single_Window_Mode->isChecked()) { - if (Settings::values.fullscreen_mode.GetValue() == Settings::FullscreenMode::Exclusive) { + if (UsingExclusiveFullscreen()) { showNormal(); restoreGeometry(UISettings::values.geometry); } else { @@ -2956,7 +3168,7 @@ void GMainWindow::HideFullscreen() { statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); ui->menubar->show(); } else { - if (Settings::values.fullscreen_mode.GetValue() == Settings::FullscreenMode::Exclusive) { + if (UsingExclusiveFullscreen()) { render_window->showNormal(); render_window->restoreGeometry(UISettings::values.renderwindow_geometry); } else { @@ -3293,6 +3505,38 @@ void GMainWindow::OpenPerGameConfiguration(u64 title_id, const std::string& file } } +bool GMainWindow::CreateShortcut(const std::string& shortcut_path, const std::string& title, + const std::string& comment, const std::string& icon_path, + const std::string& command, const std::string& arguments, + const std::string& categories, const std::string& keywords) { +#if defined(__linux__) || defined(__FreeBSD__) + // This desktop file template was writting referencing + // https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.0.html + std::string shortcut_contents{}; + shortcut_contents.append("[Desktop Entry]\n"); + shortcut_contents.append("Type=Application\n"); + shortcut_contents.append("Version=1.0\n"); + shortcut_contents.append(fmt::format("Name={:s}\n", title)); + shortcut_contents.append(fmt::format("Comment={:s}\n", comment)); + shortcut_contents.append(fmt::format("Icon={:s}\n", icon_path)); + shortcut_contents.append(fmt::format("TryExec={:s}\n", command)); + shortcut_contents.append(fmt::format("Exec={:s} {:s}\n", command, arguments)); + shortcut_contents.append(fmt::format("Categories={:s}\n", categories)); + shortcut_contents.append(fmt::format("Keywords={:s}\n", keywords)); + + std::ofstream shortcut_stream(shortcut_path); + if (!shortcut_stream.is_open()) { + LOG_WARNING(Common, "Failed to create file {:s}", shortcut_path); + return false; + } + shortcut_stream << shortcut_contents; + shortcut_stream.close(); + + return true; +#endif + return false; +} + void GMainWindow::OnLoadAmiibo() { if (emu_thread == nullptr || !emu_thread->IsRunning()) { return; @@ -3710,79 +3954,6 @@ void GMainWindow::OnMouseActivity() { mouse_center_timer.stop(); } -void GMainWindow::OnCoreError(Core::SystemResultStatus result, std::string details) { - QMessageBox::StandardButton answer; - QString status_message; - const QString common_message = - tr("The game you are trying to load requires additional files from your Switch to be " - "dumped " - "before playing.<br/><br/>For more information on dumping these files, please see the " - "following wiki page: <a " - "href='https://yuzu-emu.org/wiki/" - "dumping-system-archives-and-the-shared-fonts-from-a-switch-console/'>Dumping System " - "Archives and the Shared Fonts from a Switch Console</a>.<br/><br/>Would you like to " - "quit " - "back to the game list? Continuing emulation may result in crashes, corrupted save " - "data, or other bugs."); - switch (result) { - case Core::SystemResultStatus::ErrorSystemFiles: { - QString message; - if (details.empty()) { - message = - tr("yuzu was unable to locate a Switch system archive. %1").arg(common_message); - } else { - message = tr("yuzu was unable to locate a Switch system archive: %1. %2") - .arg(QString::fromStdString(details), common_message); - } - - answer = QMessageBox::question(this, tr("System Archive Not Found"), message, - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - status_message = tr("System Archive Missing"); - break; - } - - case Core::SystemResultStatus::ErrorSharedFont: { - const QString message = - tr("yuzu was unable to locate the Switch shared fonts. %1").arg(common_message); - answer = QMessageBox::question(this, tr("Shared Fonts Not Found"), message, - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - status_message = tr("Shared Font Missing"); - break; - } - - default: - answer = QMessageBox::question( - this, tr("Fatal Error"), - tr("yuzu has encountered a fatal error, please see the log for more details. " - "For more information on accessing the log, please see the following page: " - "<a href='https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>How " - "to " - "Upload the Log File</a>.<br/><br/>Would you like to quit back to the game " - "list? " - "Continuing emulation may result in crashes, corrupted save data, or other " - "bugs."), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - status_message = tr("Fatal Error encountered"); - break; - } - - if (answer == QMessageBox::Yes) { - if (emu_thread) { - ShutdownGame(); - - Settings::RestoreGlobalState(system->IsPoweredOn()); - system->HIDCore().ReloadInputDevices(); - UpdateStatusButtons(); - } - } else { - // Only show the message if the game is still running. - if (emu_thread) { - emu_thread->SetRunning(true); - message_label->setText(status_message); - } - } -} - void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) { if (behavior == ReinitializeKeyBehavior::Warning) { const auto res = QMessageBox::information( @@ -3927,10 +4098,6 @@ void GMainWindow::closeEvent(QCloseEvent* event) { // Shutdown session if the emu thread is active... if (emu_thread != nullptr) { ShutdownGame(); - - Settings::RestoreGlobalState(system->IsPoweredOn()); - system->HIDCore().ReloadInputDevices(); - UpdateStatusButtons(); } render_window->close(); @@ -4023,6 +4190,10 @@ bool GMainWindow::ConfirmForceLockedExit() { } void GMainWindow::RequestGameExit() { + if (!system->IsPoweredOn()) { + return; + } + auto& sm{system->ServiceManager()}; auto applet_oe = sm.GetService<Service::AM::AppletOE>("appletOE"); auto applet_ae = sm.GetService<Service::AM::AppletAE>("appletAE"); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 62d629973..db318485d 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -29,6 +29,7 @@ class GImageInfo; class GRenderWindow; class LoadingScreen; class MicroProfileDialog; +class OverlayDialog; class ProfilerWidget; class ControllerDialog; class QLabel; @@ -38,6 +39,7 @@ class QProgressDialog; class WaitTreeWidget; enum class GameListOpenTarget; enum class GameListRemoveTarget; +enum class GameListShortcutTarget; enum class DumpRomFSTarget; enum class InstalledEntryType; class GameListPlaceholder; @@ -293,6 +295,8 @@ private slots: void OnGameListCopyTID(u64 program_id); void OnGameListNavigateToGamedbEntry(u64 program_id, const CompatibilityList& compatibility_list); + void OnGameListCreateShortcut(u64 program_id, const std::string& game_path, + GameListShortcutTarget target); void OnGameListOpenDirectory(const QString& directory); void OnGameListAddDirectory(); void OnGameListShowList(bool show); @@ -320,6 +324,7 @@ private slots: void OnDisplayTitleBars(bool); void InitializeHotkeys(); void ToggleFullscreen(); + bool UsingExclusiveFullscreen(); void ShowFullscreen(); void HideFullscreen(); void ToggleWindowMode(); @@ -328,10 +333,13 @@ private slots: void ResetWindowSize900(); void ResetWindowSize1080(); void OnCaptureScreenshot(); - void OnCoreError(Core::SystemResultStatus, std::string); void OnReinitializeKeys(ReinitializeKeyBehavior behavior); void OnLanguageChanged(const QString& locale); void OnMouseActivity(); + bool OnShutdownBegin(); + void OnShutdownBeginDialog(); + void OnEmulationStopped(); + void OnEmulationStopTimeExpired(); private: QString GetGameListErrorRemoving(InstalledEntryType type) const; @@ -365,6 +373,10 @@ private: bool CheckDarkMode(); QString GetTasStateDescription() const; + bool CreateShortcut(const std::string& shortcut_path, const std::string& title, + const std::string& comment, const std::string& icon_path, + const std::string& command, const std::string& arguments, + const std::string& categories, const std::string& keywords); std::unique_ptr<Ui::MainWindow> ui; @@ -377,6 +389,8 @@ private: GRenderWindow* render_window; GameList* game_list; LoadingScreen* loading_screen; + QTimer shutdown_timer; + OverlayDialog* shutdown_dialog{}; GameListPlaceholder* game_list_placeholder; diff --git a/src/yuzu/startup_checks.cpp b/src/yuzu/startup_checks.cpp index 563818362..9f702fe95 100644 --- a/src/yuzu/startup_checks.cpp +++ b/src/yuzu/startup_checks.cpp @@ -186,7 +186,7 @@ pid_t SpawnChild(const char* arg0) { return pid; } else if (pid == 0) { // child - execl(arg0, arg0, nullptr); + execlp(arg0, arg0, nullptr); const int err = errno; fmt::print(stderr, "execl failed with error {}\n", err); _exit(0); diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h index 452038cd9..2006b883e 100644 --- a/src/yuzu/uisettings.h +++ b/src/yuzu/uisettings.h @@ -138,6 +138,7 @@ struct Values { bool configuration_applied; bool reset_to_defaults; + bool shortcut_already_warned{false}; Settings::Setting<bool> disable_web_applet{true, "disable_web_applet"}; }; diff --git a/src/yuzu/util/overlay_dialog.cpp b/src/yuzu/util/overlay_dialog.cpp index b27954512..796f5bf41 100644 --- a/src/yuzu/util/overlay_dialog.cpp +++ b/src/yuzu/util/overlay_dialog.cpp @@ -3,6 +3,7 @@ #include <QKeyEvent> #include <QScreen> +#include <QWindow> #include "core/core.h" #include "core/hid/hid_types.h" @@ -42,7 +43,7 @@ OverlayDialog::OverlayDialog(QWidget* parent, Core::System& system, const QStrin MoveAndResizeWindow(); // TODO (Morph): Remove this when InputInterpreter no longer relies on the HID backend - if (system.IsPoweredOn()) { + if (system.IsPoweredOn() && !ui->buttonsDialog->isHidden()) { input_interpreter = std::make_unique<InputInterpreter>(system); StartInputThread(); @@ -83,6 +84,11 @@ void OverlayDialog::InitializeRegularTextDialog(const QString& title_text, const ui->button_ok_label->setEnabled(false); } + if (ui->button_cancel->isHidden() && ui->button_ok_label->isHidden()) { + ui->buttonsDialog->hide(); + return; + } + connect( ui->button_cancel, &QPushButton::clicked, this, [this](bool) { @@ -130,6 +136,11 @@ void OverlayDialog::InitializeRichTextDialog(const QString& title_text, const QS ui->button_ok_rich->setEnabled(false); } + if (ui->button_cancel_rich->isHidden() && ui->button_ok_rich->isHidden()) { + ui->buttonsRichDialog->hide(); + return; + } + connect( ui->button_cancel_rich, &QPushButton::clicked, this, [this](bool) { @@ -152,7 +163,7 @@ void OverlayDialog::MoveAndResizeWindow() { const auto height = static_cast<float>(parentWidget()->height()); // High DPI - const float dpi_scale = qApp->screenAt(pos)->logicalDotsPerInch() / 96.0f; + const float dpi_scale = parentWidget()->windowHandle()->screen()->logicalDotsPerInch() / 96.0f; const auto title_text_font_size = BASE_TITLE_FONT_SIZE * (height / BASE_HEIGHT) / dpi_scale; const auto body_text_font_size = @@ -249,3 +260,9 @@ void OverlayDialog::InputThread() { std::this_thread::sleep_for(std::chrono::milliseconds(50)); } } + +void OverlayDialog::keyPressEvent(QKeyEvent* e) { + if (!ui->buttonsDialog->isHidden() || e->key() != Qt::Key_Escape) { + QDialog::keyPressEvent(e); + } +} diff --git a/src/yuzu/util/overlay_dialog.h b/src/yuzu/util/overlay_dialog.h index 39c44393c..872283d61 100644 --- a/src/yuzu/util/overlay_dialog.h +++ b/src/yuzu/util/overlay_dialog.h @@ -94,6 +94,7 @@ private: /// The thread where input is being polled and processed. void InputThread(); + void keyPressEvent(QKeyEvent* e) override; std::unique_ptr<Ui::OverlayDialog> ui; |