aboutsummaryrefslogtreecommitdiff
path: root/src/yuzu/main.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/yuzu/main.cpp')
-rw-r--r--src/yuzu/main.cpp1297
1 files changed, 939 insertions, 358 deletions
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index 1717e06f97..e704cc6561 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -11,15 +11,19 @@
#endif
// VFS includes must be before glad as they will conflict with Windows file api, which uses defines.
+#include "applets/controller.h"
#include "applets/error.h"
#include "applets/profile_select.h"
#include "applets/software_keyboard.h"
#include "applets/web_browser.h"
#include "configuration/configure_input.h"
-#include "configuration/configure_per_general.h"
+#include "configuration/configure_per_game.h"
+#include "configuration/configure_vibration.h"
#include "core/file_sys/vfs.h"
#include "core/file_sys/vfs_real.h"
+#include "core/frontend/applets/controller.h"
#include "core/frontend/applets/general_frontend.h"
+#include "core/frontend/applets/software_keyboard.h"
#include "core/hle/service/acc/profile_manager.h"
#include "core/hle/service/am/applet_ae.h"
#include "core/hle/service/am/applet_oe.h"
@@ -47,15 +51,18 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include <QDesktopServices>
#include <QDesktopWidget>
#include <QDialogButtonBox>
+#include <QDir>
#include <QFile>
#include <QFileDialog>
#include <QInputDialog>
#include <QMessageBox>
#include <QProgressBar>
#include <QProgressDialog>
+#include <QPushButton>
#include <QShortcut>
#include <QStatusBar>
#include <QSysInfo>
+#include <QUrl>
#include <QtConcurrent/QtConcurrent>
#include <fmt/format.h>
@@ -65,6 +72,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include "common/logging/backend.h"
#include "common/logging/filter.h"
#include "common/logging/log.h"
+#include "common/memory_detect.h"
#include "common/microprofile.h"
#include "common/scm_rev.h"
#include "common/scope_exit.h"
@@ -82,7 +90,6 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include "core/file_sys/romfs.h"
#include "core/file_sys/savedata_factory.h"
#include "core/file_sys/submission_package.h"
-#include "core/frontend/applets/software_keyboard.h"
#include "core/hle/kernel/process.h"
#include "core/hle/service/am/am.h"
#include "core/hle/service/filesystem/filesystem.h"
@@ -92,6 +99,9 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include "core/perf_stats.h"
#include "core/settings.h"
#include "core/telemetry_session.h"
+#include "input_common/main.h"
+#include "video_core/gpu.h"
+#include "video_core/shader_notify.h"
#include "yuzu/about_dialog.h"
#include "yuzu/bootmanager.h"
#include "yuzu/compatdb.h"
@@ -105,6 +115,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include "yuzu/game_list.h"
#include "yuzu/game_list_p.h"
#include "yuzu/hotkeys.h"
+#include "yuzu/install_dialog.h"
#include "yuzu/loading_screen.h"
#include "yuzu/main.h"
#include "yuzu/uisettings.h"
@@ -135,6 +146,8 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
}
#endif
+constexpr int default_mouse_timeout = 2500;
+
constexpr u64 DLC_BASE_TITLE_ID_MASK = 0xFFFFFFFFFFFFE000;
/**
@@ -170,8 +183,8 @@ static void InitializeLogging() {
log_filter.ParseFilterString(Settings::values.log_filter);
Log::SetGlobalFilter(log_filter);
- const std::string& log_dir = FileUtil::GetUserPath(FileUtil::UserPath::LogDir);
- FileUtil::CreateFullPath(log_dir);
+ const std::string& log_dir = Common::FS::GetUserPath(Common::FS::UserPath::LogDir);
+ Common::FS::CreateFullPath(log_dir);
Log::AddBackend(std::make_unique<Log::FileBackend>(log_dir + LOG_FILE));
#ifdef _WIN32
Log::AddBackend(std::make_unique<Log::DebuggerBackend>());
@@ -179,11 +192,13 @@ static void InitializeLogging() {
}
GMainWindow::GMainWindow()
- : config(new Config()), emu_thread(nullptr),
- vfs(std::make_shared<FileSys::RealVfsFilesystem>()),
- provider(std::make_unique<FileSys::ManualContentProvider>()) {
+ : input_subsystem{std::make_shared<InputCommon::InputSubsystem>()},
+ config{std::make_unique<Config>()}, vfs{std::make_shared<FileSys::RealVfsFilesystem>()},
+ provider{std::make_unique<FileSys::ManualContentProvider>()} {
InitializeLogging();
+ LoadTranslation();
+
setAcceptDrops(true);
ui.setupUi(this);
statusBar()->hide();
@@ -214,9 +229,26 @@ GMainWindow::GMainWindow()
LOG_INFO(Frontend, "yuzu Version: {} | {}-{}", yuzu_build_version, Common::g_scm_branch,
Common::g_scm_desc);
#ifdef ARCHITECTURE_x86_64
- LOG_INFO(Frontend, "Host CPU: {}", Common::GetCPUCaps().cpu_string);
+ const auto& caps = Common::GetCPUCaps();
+ std::string cpu_string = caps.cpu_string;
+ if (caps.avx || caps.avx2 || caps.avx512) {
+ cpu_string += " | AVX";
+ if (caps.avx512) {
+ cpu_string += "512";
+ } else if (caps.avx2) {
+ cpu_string += '2';
+ }
+ if (caps.fma || caps.fma4) {
+ cpu_string += " | FMA";
+ }
+ }
+ LOG_INFO(Frontend, "Host CPU: {}", cpu_string);
#endif
LOG_INFO(Frontend, "Host OS: {}", QSysInfo::prettyProductName().toStdString());
+ LOG_INFO(Frontend, "Host RAM: {:.2f} GB",
+ Common::GetMemInfo().TotalPhysicalMemory / 1024.0f / 1024 / 1024);
+ LOG_INFO(Frontend, "Host Swap: {:.2f} GB",
+ Common::GetMemInfo().TotalSwapMemory / 1024.0f / 1024 / 1024);
UpdateWindowTitle();
show();
@@ -236,10 +268,20 @@ GMainWindow::GMainWindow()
// Show one-time "callout" messages to the user
ShowTelemetryCallout();
+ // make sure menubar has the arrow cursor instead of inheriting from this
+ ui.menubar->setCursor(QCursor());
+ statusBar()->setCursor(QCursor());
+
+ mouse_hide_timer.setInterval(default_mouse_timeout);
+ connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor);
+ connect(ui.menubar, &QMenuBar::hovered, this, &GMainWindow::ShowMouseCursor);
+
QStringList args = QApplication::arguments();
if (args.length() >= 2) {
BootGame(args[1]);
}
+
+ MigrateConfigFiles();
}
GMainWindow::~GMainWindow() {
@@ -248,17 +290,36 @@ GMainWindow::~GMainWindow() {
delete render_window;
}
+void GMainWindow::ControllerSelectorReconfigureControllers(
+ const Core::Frontend::ControllerParameters& parameters) {
+ QtControllerSelectorDialog dialog(this, parameters, input_subsystem.get());
+
+ dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowStaysOnTopHint |
+ Qt::WindowTitleHint | Qt::WindowSystemMenuHint);
+ dialog.setWindowModality(Qt::WindowModal);
+ dialog.exec();
+
+ emit ControllerSelectorReconfigureFinished();
+
+ // Don't forget to apply settings.
+ Settings::Apply();
+ config->Save();
+
+ UpdateStatusButtons();
+}
+
void GMainWindow::ProfileSelectorSelectProfile() {
QtProfileSelectionDialog dialog(this);
- dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint |
- Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint);
+ dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowStaysOnTopHint |
+ Qt::WindowTitleHint | Qt::WindowSystemMenuHint |
+ Qt::WindowCloseButtonHint);
dialog.setWindowModality(Qt::WindowModal);
if (dialog.exec() == QDialog::Rejected) {
emit ProfileSelectorFinishedSelection(std::nullopt);
return;
}
- Service::Account::ProfileManager manager;
+ const Service::Account::ProfileManager manager;
const auto uuid = manager.GetUser(static_cast<std::size_t>(dialog.GetIndex()));
if (!uuid.has_value()) {
emit ProfileSelectorFinishedSelection(std::nullopt);
@@ -271,8 +332,9 @@ void GMainWindow::ProfileSelectorSelectProfile() {
void GMainWindow::SoftwareKeyboardGetText(
const Core::Frontend::SoftwareKeyboardParameters& parameters) {
QtSoftwareKeyboardDialog dialog(this, parameters);
- dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint |
- Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint);
+ dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowStaysOnTopHint |
+ Qt::WindowTitleHint | Qt::WindowSystemMenuHint |
+ Qt::WindowCloseButtonHint);
dialog.setWindowModality(Qt::WindowModal);
if (dialog.exec() == QDialog::Rejected) {
@@ -435,7 +497,7 @@ void GMainWindow::InitializeWidgets() {
#ifdef YUZU_ENABLE_COMPATIBILITY_REPORTING
ui.action_Report_Compatibility->setVisible(true);
#endif
- render_window = new GRenderWindow(this, emu_thread.get());
+ render_window = new GRenderWindow(this, emu_thread.get(), input_subsystem);
render_window->hide();
game_list = new GameList(vfs, provider.get(), this);
@@ -464,6 +526,8 @@ void GMainWindow::InitializeWidgets() {
message_label->setAlignment(Qt::AlignLeft);
statusBar()->addPermanentWidget(message_label, 1);
+ shader_building_label = new QLabel();
+ shader_building_label->setToolTip(tr("The amount of shaders currently being built"));
emu_speed_label = new QLabel();
emu_speed_label->setToolTip(
tr("Current emulation speed. Values higher or lower than 100% "
@@ -476,7 +540,8 @@ void GMainWindow::InitializeWidgets() {
tr("Time taken to emulate a Switch frame, not counting framelimiting or v-sync. For "
"full-speed emulation this should be at most 16.67 ms."));
- for (auto& label : {emu_speed_label, game_fps_label, emu_frametime_label}) {
+ for (auto& label :
+ {shader_building_label, emu_speed_label, game_fps_label, emu_frametime_label}) {
label->setVisible(false);
label->setFrameStyle(QFrame::NoFrame);
label->setContentsMargins(4, 0, 4, 0);
@@ -488,13 +553,14 @@ void GMainWindow::InitializeWidgets() {
dock_status_button->setObjectName(QStringLiteral("TogglableStatusBarButton"));
dock_status_button->setFocusPolicy(Qt::NoFocus);
connect(dock_status_button, &QPushButton::clicked, [&] {
- Settings::values.use_docked_mode = !Settings::values.use_docked_mode;
- dock_status_button->setChecked(Settings::values.use_docked_mode);
- OnDockedModeChanged(!Settings::values.use_docked_mode, Settings::values.use_docked_mode);
+ Settings::values.use_docked_mode.SetValue(!Settings::values.use_docked_mode.GetValue());
+ dock_status_button->setChecked(Settings::values.use_docked_mode.GetValue());
+ OnDockedModeChanged(!Settings::values.use_docked_mode.GetValue(),
+ Settings::values.use_docked_mode.GetValue());
});
dock_status_button->setText(tr("DOCK"));
dock_status_button->setCheckable(true);
- dock_status_button->setChecked(Settings::values.use_docked_mode);
+ dock_status_button->setChecked(Settings::values.use_docked_mode.GetValue());
statusBar()->insertPermanentWidget(0, dock_status_button);
// Setup ASync button
@@ -505,14 +571,36 @@ void GMainWindow::InitializeWidgets() {
if (emulation_running) {
return;
}
- Settings::values.use_asynchronous_gpu_emulation =
- !Settings::values.use_asynchronous_gpu_emulation;
- async_status_button->setChecked(Settings::values.use_asynchronous_gpu_emulation);
+ bool is_async = !Settings::values.use_asynchronous_gpu_emulation.GetValue() ||
+ Settings::values.use_multi_core.GetValue();
+ Settings::values.use_asynchronous_gpu_emulation.SetValue(is_async);
+ async_status_button->setChecked(Settings::values.use_asynchronous_gpu_emulation.GetValue());
Settings::Apply();
});
async_status_button->setText(tr("ASYNC"));
async_status_button->setCheckable(true);
- async_status_button->setChecked(Settings::values.use_asynchronous_gpu_emulation);
+ async_status_button->setChecked(Settings::values.use_asynchronous_gpu_emulation.GetValue());
+
+ // Setup Multicore button
+ multicore_status_button = new QPushButton();
+ multicore_status_button->setObjectName(QStringLiteral("TogglableStatusBarButton"));
+ multicore_status_button->setFocusPolicy(Qt::NoFocus);
+ connect(multicore_status_button, &QPushButton::clicked, [&] {
+ if (emulation_running) {
+ return;
+ }
+ Settings::values.use_multi_core.SetValue(!Settings::values.use_multi_core.GetValue());
+ bool is_async = Settings::values.use_asynchronous_gpu_emulation.GetValue() ||
+ Settings::values.use_multi_core.GetValue();
+ Settings::values.use_asynchronous_gpu_emulation.SetValue(is_async);
+ async_status_button->setChecked(Settings::values.use_asynchronous_gpu_emulation.GetValue());
+ multicore_status_button->setChecked(Settings::values.use_multi_core.GetValue());
+ Settings::Apply();
+ });
+ multicore_status_button->setText(tr("MULTICORE"));
+ multicore_status_button->setCheckable(true);
+ multicore_status_button->setChecked(Settings::values.use_multi_core.GetValue());
+ statusBar()->insertPermanentWidget(0, multicore_status_button);
statusBar()->insertPermanentWidget(0, async_status_button);
// Setup Renderer API button
@@ -520,7 +608,7 @@ void GMainWindow::InitializeWidgets() {
renderer_status_button->setObjectName(QStringLiteral("RendererStatusBarButton"));
renderer_status_button->setCheckable(true);
renderer_status_button->setFocusPolicy(Qt::NoFocus);
- connect(renderer_status_button, &QPushButton::toggled, [=](bool checked) {
+ connect(renderer_status_button, &QPushButton::toggled, [this](bool checked) {
renderer_status_button->setText(checked ? tr("VULKAN") : tr("OPENGL"));
});
renderer_status_button->toggle();
@@ -530,16 +618,16 @@ void GMainWindow::InitializeWidgets() {
renderer_status_button->setCheckable(false);
renderer_status_button->setDisabled(true);
#else
- renderer_status_button->setChecked(Settings::values.renderer_backend ==
+ renderer_status_button->setChecked(Settings::values.renderer_backend.GetValue() ==
Settings::RendererBackend::Vulkan);
- connect(renderer_status_button, &QPushButton::clicked, [=] {
+ connect(renderer_status_button, &QPushButton::clicked, [this] {
if (emulation_running) {
return;
}
if (renderer_status_button->isChecked()) {
- Settings::values.renderer_backend = Settings::RendererBackend::Vulkan;
+ Settings::values.renderer_backend.SetValue(Settings::RendererBackend::Vulkan);
} else {
- Settings::values.renderer_backend = Settings::RendererBackend::OpenGL;
+ Settings::values.renderer_backend.SetValue(Settings::RendererBackend::OpenGL);
}
Settings::Apply();
@@ -638,6 +726,11 @@ void GMainWindow::InitializeHotkeys() {
ui.action_Capture_Screenshot->setShortcutContext(
hotkey_registry.GetShortcutContext(main_window, capture_screenshot));
+ ui.action_Fullscreen->setShortcut(
+ hotkey_registry.GetHotkey(main_window, fullscreen, this)->key());
+ ui.action_Fullscreen->setShortcutContext(
+ hotkey_registry.GetShortcutContext(main_window, fullscreen));
+
connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("Load File"), this),
&QShortcut::activated, this, &GMainWindow::OnMenuLoadFile);
connect(
@@ -671,24 +764,24 @@ void GMainWindow::InitializeHotkeys() {
});
connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("Toggle Speed Limit"), this),
&QShortcut::activated, this, [&] {
- Settings::values.use_frame_limit = !Settings::values.use_frame_limit;
+ Settings::values.use_frame_limit.SetValue(
+ !Settings::values.use_frame_limit.GetValue());
UpdateStatusBar();
});
- // TODO: Remove this comment/static whenever the next major release of
- // MSVC occurs and we make it a requirement (see:
- // https://developercommunity.visualstudio.com/content/problem/93922/constexprs-are-trying-to-be-captured-in-lambda-fun.html)
- static constexpr u16 SPEED_LIMIT_STEP = 5;
+ constexpr u16 SPEED_LIMIT_STEP = 5;
connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("Increase Speed Limit"), this),
&QShortcut::activated, this, [&] {
- if (Settings::values.frame_limit < 9999 - SPEED_LIMIT_STEP) {
- Settings::values.frame_limit += SPEED_LIMIT_STEP;
+ if (Settings::values.frame_limit.GetValue() < 9999 - SPEED_LIMIT_STEP) {
+ Settings::values.frame_limit.SetValue(SPEED_LIMIT_STEP +
+ Settings::values.frame_limit.GetValue());
UpdateStatusBar();
}
});
connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("Decrease Speed Limit"), this),
&QShortcut::activated, this, [&] {
- if (Settings::values.frame_limit > SPEED_LIMIT_STEP) {
- Settings::values.frame_limit -= SPEED_LIMIT_STEP;
+ if (Settings::values.frame_limit.GetValue() > SPEED_LIMIT_STEP) {
+ Settings::values.frame_limit.SetValue(Settings::values.frame_limit.GetValue() -
+ SPEED_LIMIT_STEP);
UpdateStatusBar();
}
});
@@ -700,27 +793,31 @@ void GMainWindow::InitializeHotkeys() {
});
connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("Capture Screenshot"), this),
&QShortcut::activated, this, [&] {
- if (emu_thread->IsRunning()) {
+ if (emu_thread != nullptr && emu_thread->IsRunning()) {
OnCaptureScreenshot();
}
});
connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("Change Docked Mode"), this),
&QShortcut::activated, this, [&] {
- Settings::values.use_docked_mode = !Settings::values.use_docked_mode;
- OnDockedModeChanged(!Settings::values.use_docked_mode,
- Settings::values.use_docked_mode);
- dock_status_button->setChecked(Settings::values.use_docked_mode);
+ Settings::values.use_docked_mode.SetValue(
+ !Settings::values.use_docked_mode.GetValue());
+ OnDockedModeChanged(!Settings::values.use_docked_mode.GetValue(),
+ Settings::values.use_docked_mode.GetValue());
+ dock_status_button->setChecked(Settings::values.use_docked_mode.GetValue());
});
+ connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("Mute Audio"), this),
+ &QShortcut::activated, this,
+ [] { Settings::values.audio_muted = !Settings::values.audio_muted; });
}
void GMainWindow::SetDefaultUIGeometry() {
- // geometry: 55% of the window contents are in the upper screen half, 45% in the lower half
+ // geometry: 53% of the window contents are in the upper screen half, 47% in the lower half
const QRect screenRect = QApplication::desktop()->screenGeometry(this);
const int w = screenRect.width() * 2 / 3;
- const int h = screenRect.height() / 2;
+ const int h = screenRect.height() * 2 / 3;
const int x = (screenRect.x() + screenRect.width()) / 2 - w / 2;
- const int y = (screenRect.y() + screenRect.height()) / 2 - h * 55 / 100;
+ const int y = (screenRect.y() + screenRect.height()) / 2 - h * 53 / 100;
setGeometry(x, y, w, h);
}
@@ -745,7 +842,7 @@ void GMainWindow::RestoreUIState() {
OnDisplayTitleBars(ui.action_Display_Dock_Widget_Headers->isChecked());
ui.action_Show_Filter_Bar->setChecked(UISettings::values.show_filter_bar);
- game_list->setFilterVisible(ui.action_Show_Filter_Bar->isChecked());
+ game_list->SetFilterVisible(ui.action_Show_Filter_Bar->isChecked());
ui.action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar);
statusBar()->setVisible(ui.action_Show_Status_Bar->isChecked());
@@ -776,6 +873,9 @@ void GMainWindow::ConnectWidgetEvents() {
connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder);
connect(game_list, &GameList::OpenTransferableShaderCacheRequested, this,
&GMainWindow::OnTransferableShaderCacheOpenFile);
+ connect(game_list, &GameList::RemoveInstalledEntryRequested, this,
+ &GMainWindow::OnGameListRemoveInstalledEntry);
+ connect(game_list, &GameList::RemoveFileRequested, this, &GMainWindow::OnGameListRemoveFile);
connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS);
connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID);
connect(game_list, &GameList::NavigateToGamedbEntryRequested, this,
@@ -788,6 +888,9 @@ void GMainWindow::ConnectWidgetEvents() {
connect(game_list, &GameList::OpenPerGameGeneralRequested, this,
&GMainWindow::OnGameListOpenPerGameProperties);
+ connect(this, &GMainWindow::UpdateInstallProgress, this,
+ &GMainWindow::IncrementInstallProgress);
+
connect(this, &GMainWindow::EmulationStarting, render_window,
&GRenderWindow::OnEmulationStarting);
connect(this, &GMainWindow::EmulationStopping, render_window,
@@ -802,10 +905,6 @@ void GMainWindow::ConnectMenuEvents() {
connect(ui.action_Load_Folder, &QAction::triggered, this, &GMainWindow::OnMenuLoadFolder);
connect(ui.action_Install_File_NAND, &QAction::triggered, this,
&GMainWindow::OnMenuInstallToNAND);
- connect(ui.action_Select_NAND_Directory, &QAction::triggered, this,
- [this] { OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget::NAND); });
- connect(ui.action_Select_SDMC_Directory, &QAction::triggered, this,
- [this] { OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget::SDMC); });
connect(ui.action_Exit, &QAction::triggered, this, &QMainWindow::close);
connect(ui.action_Load_Amiibo, &QAction::triggered, this, &GMainWindow::OnLoadAmiibo);
@@ -815,8 +914,14 @@ void GMainWindow::ConnectMenuEvents() {
connect(ui.action_Stop, &QAction::triggered, this, &GMainWindow::OnStopGame);
connect(ui.action_Report_Compatibility, &QAction::triggered, this,
&GMainWindow::OnMenuReportCompatibility);
+ connect(ui.action_Open_Mods_Page, &QAction::triggered, this, &GMainWindow::OnOpenModsPage);
+ connect(ui.action_Open_Quickstart_Guide, &QAction::triggered, this,
+ &GMainWindow::OnOpenQuickstartGuide);
+ connect(ui.action_Open_FAQ, &QAction::triggered, this, &GMainWindow::OnOpenFAQ);
connect(ui.action_Restart, &QAction::triggered, this, [this] { BootGame(QString(game_path)); });
connect(ui.action_Configure, &QAction::triggered, this, &GMainWindow::OnConfigure);
+ connect(ui.action_Configure_Current_Game, &QAction::triggered, this,
+ &GMainWindow::OnConfigurePerGame);
// View
connect(ui.action_Single_Window_Mode, &QAction::triggered, this,
@@ -825,12 +930,9 @@ void GMainWindow::ConnectMenuEvents() {
&GMainWindow::OnDisplayTitleBars);
connect(ui.action_Show_Filter_Bar, &QAction::triggered, this, &GMainWindow::OnToggleFilterBar);
connect(ui.action_Show_Status_Bar, &QAction::triggered, statusBar(), &QStatusBar::setVisible);
+ connect(ui.action_Reset_Window_Size, &QAction::triggered, this, &GMainWindow::ResetWindowSize);
// Fullscreen
- ui.action_Fullscreen->setShortcut(
- hotkey_registry
- .GetHotkey(QStringLiteral("Main Window"), QStringLiteral("Fullscreen"), this)
- ->key());
connect(ui.action_Fullscreen, &QAction::triggered, this, &GMainWindow::ToggleFullscreen);
// Movie
@@ -889,15 +991,18 @@ bool GMainWindow::LoadROM(const QString& filename) {
system.SetFilesystem(vfs);
system.SetAppletFrontendSet({
- nullptr, // Parental Controls
- std::make_unique<QtErrorDisplay>(*this), //
- nullptr, // Photo Viewer
- std::make_unique<QtProfileSelector>(*this), //
- std::make_unique<QtSoftwareKeyboard>(*this), //
- std::make_unique<QtWebBrowser>(*this), //
- nullptr, // E-Commerce
+ std::make_unique<QtControllerSelector>(*this), // Controller Selector
+ nullptr, // E-Commerce
+ std::make_unique<QtErrorDisplay>(*this), // Error Display
+ nullptr, // Parental Controls
+ nullptr, // Photo Viewer
+ std::make_unique<QtProfileSelector>(*this), // Profile Selector
+ std::make_unique<QtSoftwareKeyboard>(*this), // Software Keyboard
+ std::make_unique<QtWebBrowser>(*this), // Web Browser
});
+ system.RegisterHostThread();
+
const Core::System::ResultStatus result{system.Load(*render_window, filename.toStdString())};
const auto drd_callout =
@@ -940,16 +1045,18 @@ bool GMainWindow::LoadROM(const QString& filename) {
default:
if (static_cast<u32>(result) >
static_cast<u32>(Core::System::ResultStatus::ErrorLoader)) {
- LOG_CRITICAL(Frontend, "Failed to load ROM!");
const u16 loader_id = static_cast<u16>(Core::System::ResultStatus::ErrorLoader);
const u16 error_id = static_cast<u16>(result) - loader_id;
+ const std::string error_code = fmt::format("({:04X}-{:04X})", loader_id, error_id);
+ LOG_CRITICAL(Frontend, "Failed to load ROM! {}", error_code);
QMessageBox::critical(
- this, tr("Error while loading ROM!"),
+ this,
+ tr("Error while loading ROM! ").append(QString::fromStdString(error_code)),
QString::fromStdString(fmt::format(
- "While attempting to load the ROM requested, an error occured. Please "
- "refer to the yuzu wiki for more information or the yuzu discord for "
- "additional help.\n\nError Code: {:04X}-{:04X}\nError Description: {}",
- loader_id, error_id, static_cast<Loader::ResultStatus>(error_id))));
+ "{}<br>Please follow <a href='https://yuzu-emu.org/help/quickstart/'>the "
+ "yuzu quickstart guide</a> to redump your files.<br>You can refer "
+ "to the yuzu wiki</a> or the yuzu Discord</a> for help.",
+ static_cast<Loader::ResultStatus>(error_id))));
} else {
QMessageBox::critical(
this, tr("Error while loading ROM!"),
@@ -961,7 +1068,7 @@ bool GMainWindow::LoadROM(const QString& filename) {
}
game_path = filename;
- system.TelemetrySession().AddField(Telemetry::FieldType::App, "Frontend", "Qt");
+ system.TelemetrySession().AddField(Common::Telemetry::FieldType::App, "Frontend", "Qt");
return true;
}
@@ -982,6 +1089,19 @@ void GMainWindow::BootGame(const QString& filename) {
LOG_INFO(Frontend, "yuzu starting...");
StoreRecentFile(filename); // Put the filename on top of the list
+ u64 title_id{0};
+ auto& system = Core::System::GetInstance();
+ const auto v_file = Core::GetGameFileFromPath(vfs, filename.toUtf8().constData());
+ const auto loader = Loader::GetLoader(system, v_file);
+ if (!(loader == nullptr || loader->ReadProgramId(title_id) != Loader::ResultStatus::Success)) {
+ // Load per game settings
+ Config per_game_config(fmt::format("{:016X}", title_id), Config::ConfigType::PerGameConfig);
+ }
+
+ ConfigureVibration::SetAllVibrationDevices();
+
+ Settings::LogSettings();
+
if (UISettings::values.select_user_on_boot) {
SelectAndSetCurrentUser();
}
@@ -1006,30 +1126,42 @@ void GMainWindow::BootGame(const QString& filename) {
&LoadingScreen::OnLoadProgress, Qt::QueuedConnection);
// Update the GUI
+ UpdateStatusButtons();
if (ui.action_Single_Window_Mode->isChecked()) {
game_list->hide();
game_list_placeholder->hide();
}
status_bar_update_timer.start(2000);
async_status_button->setDisabled(true);
+ multicore_status_button->setDisabled(true);
renderer_status_button->setDisabled(true);
- const u64 title_id = Core::System::GetInstance().CurrentProcess()->GetTitleID();
+ if (UISettings::values.hide_mouse) {
+ mouse_hide_timer.start();
+ setMouseTracking(true);
+ ui.centralwidget->setMouseTracking(true);
+ }
std::string title_name;
- const auto res = Core::System::GetInstance().GetGameName(title_name);
- if (res != Loader::ResultStatus::Success) {
- const auto [nacp, icon_file] = FileSys::PatchManager(title_id).GetControlMetadata();
- if (nacp != nullptr)
- title_name = nacp->GetApplicationName();
+ std::string title_version;
+ const auto res = system.GetGameName(title_name);
- if (title_name.empty())
- title_name = FileUtil::GetFilename(filename.toStdString());
+ const auto metadata = [&system, title_id] {
+ const FileSys::PatchManager pm(title_id, system.GetFileSystemController(),
+ system.GetContentProvider());
+ return pm.GetControlMetadata();
+ }();
+ if (metadata.first != nullptr) {
+ title_version = metadata.first->GetVersionString();
+ title_name = metadata.first->GetApplicationName();
+ }
+ if (res != Loader::ResultStatus::Success || title_name.empty()) {
+ title_name = Common::FS::GetFilename(filename.toStdString());
}
- LOG_INFO(Frontend, "Booting game: {:016X} | {}", title_id, title_name);
- UpdateWindowTitle(QString::fromStdString(title_name));
+ LOG_INFO(Frontend, "Booting game: {:016X} | {} | {}", title_id, title_name, title_version);
+ UpdateWindowTitle(title_name, title_version);
- loading_screen->Prepare(Core::System::GetInstance().GetAppLoader());
+ loading_screen->Prepare(system.GetAppLoader());
loading_screen->show();
emulation_running = true;
@@ -1070,26 +1202,33 @@ void GMainWindow::ShutdownGame() {
ui.action_Pause->setEnabled(false);
ui.action_Stop->setEnabled(false);
ui.action_Restart->setEnabled(false);
+ ui.action_Configure_Current_Game->setEnabled(false);
ui.action_Report_Compatibility->setEnabled(false);
ui.action_Load_Amiibo->setEnabled(false);
ui.action_Capture_Screenshot->setEnabled(false);
render_window->hide();
loading_screen->hide();
loading_screen->Clear();
- if (game_list->isEmpty())
+ if (game_list->IsEmpty()) {
game_list_placeholder->show();
- else
+ } else {
game_list->show();
- game_list->setFilterFocus();
+ }
+ game_list->SetFilterFocus();
+
+ setMouseTracking(false);
+ ui.centralwidget->setMouseTracking(false);
UpdateWindowTitle();
// Disable status bar updates
status_bar_update_timer.stop();
+ shader_building_label->setVisible(false);
emu_speed_label->setVisible(false);
game_fps_label->setVisible(false);
emu_frametime_label->setVisible(false);
async_status_button->setEnabled(true);
+ multicore_status_button->setEnabled(true);
#ifdef HAS_VULKAN
renderer_status_button->setEnabled(true);
#endif
@@ -1137,50 +1276,82 @@ void GMainWindow::OnGameListLoadFile(QString game_path) {
BootGame(game_path);
}
-void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target) {
+void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target,
+ const std::string& game_path) {
std::string path;
QString open_target;
+ auto& system = Core::System::GetInstance();
+
+ const auto [user_save_size, device_save_size] = [this, &game_path, &program_id, &system] {
+ const FileSys::PatchManager pm{program_id, system.GetFileSystemController(),
+ system.GetContentProvider()};
+ const auto control = pm.GetControlMetadata().first;
+ if (control != nullptr) {
+ return std::make_pair(control->GetDefaultNormalSaveSize(),
+ control->GetDeviceSaveDataSize());
+ } else {
+ const auto file = Core::GetGameFileFromPath(vfs, game_path);
+ const auto loader = Loader::GetLoader(system, file);
+
+ FileSys::NACP nacp{};
+ loader->ReadControlData(nacp);
+ return std::make_pair(nacp.GetDefaultNormalSaveSize(), nacp.GetDeviceSaveDataSize());
+ }
+ }();
+
+ const bool has_user_save{user_save_size > 0};
+ const bool has_device_save{device_save_size > 0};
+
+ ASSERT_MSG(has_user_save != has_device_save, "Game uses both user and device savedata?");
+
switch (target) {
case GameListOpenTarget::SaveData: {
open_target = tr("Save Data");
- const std::string nand_dir = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir);
- ASSERT(program_id != 0);
+ const std::string nand_dir = Common::FS::GetUserPath(Common::FS::UserPath::NANDDir);
+
+ if (has_user_save) {
+ // User save data
+ const auto select_profile = [this] {
+ QtProfileSelectionDialog dialog(this);
+ dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint |
+ Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint);
+ dialog.setWindowModality(Qt::WindowModal);
+
+ if (dialog.exec() == QDialog::Rejected) {
+ return -1;
+ }
- const auto select_profile = [this] {
- QtProfileSelectionDialog dialog(this);
- dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint |
- Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint);
- dialog.setWindowModality(Qt::WindowModal);
+ return dialog.GetIndex();
+ };
- if (dialog.exec() == QDialog::Rejected) {
- return -1;
+ const auto index = select_profile();
+ if (index == -1) {
+ return;
}
- return dialog.GetIndex();
- };
-
- const auto index = select_profile();
- if (index == -1) {
- return;
+ Service::Account::ProfileManager manager;
+ const auto user_id = manager.GetUser(static_cast<std::size_t>(index));
+ ASSERT(user_id);
+ path = nand_dir + FileSys::SaveDataFactory::GetFullPath(
+ FileSys::SaveDataSpaceId::NandUser,
+ FileSys::SaveDataType::SaveData, program_id, user_id->uuid, 0);
+ } else {
+ // Device save data
+ path = nand_dir + FileSys::SaveDataFactory::GetFullPath(
+ FileSys::SaveDataSpaceId::NandUser,
+ FileSys::SaveDataType::SaveData, program_id, {}, 0);
}
- Service::Account::ProfileManager manager;
- const auto user_id = manager.GetUser(static_cast<std::size_t>(index));
- ASSERT(user_id);
- path = nand_dir + FileSys::SaveDataFactory::GetFullPath(FileSys::SaveDataSpaceId::NandUser,
- FileSys::SaveDataType::SaveData,
- program_id, user_id->uuid, 0);
-
- if (!FileUtil::Exists(path)) {
- FileUtil::CreateFullPath(path);
- FileUtil::CreateDir(path);
+ if (!Common::FS::Exists(path)) {
+ Common::FS::CreateFullPath(path);
+ Common::FS::CreateDir(path);
}
break;
}
case GameListOpenTarget::ModData: {
open_target = tr("Mod Data");
- const auto load_dir = FileUtil::GetUserPath(FileUtil::UserPath::LoadDir);
+ const auto load_dir = Common::FS::GetUserPath(Common::FS::UserPath::LoadDir);
path = fmt::format("{}{:016X}", load_dir, program_id);
break;
}
@@ -1201,14 +1372,12 @@ void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target
}
void GMainWindow::OnTransferableShaderCacheOpenFile(u64 program_id) {
- ASSERT(program_id != 0);
-
const QString shader_dir =
- QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::ShaderDir));
- const QString tranferable_shader_cache_folder_path =
+ QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::ShaderDir));
+ const QString transferable_shader_cache_folder_path =
shader_dir + QStringLiteral("opengl") + QDir::separator() + QStringLiteral("transferable");
const QString transferable_shader_cache_file_path =
- tranferable_shader_cache_folder_path + QDir::separator() +
+ transferable_shader_cache_folder_path + QDir::separator() +
QString::fromStdString(fmt::format("{:016X}.bin", program_id));
if (!QFile::exists(transferable_shader_cache_file_path)) {
@@ -1229,7 +1398,7 @@ void GMainWindow::OnTransferableShaderCacheOpenFile(u64 program_id) {
param << QDir::toNativeSeparators(transferable_shader_cache_file_path);
QProcess::startDetached(explorer, param);
#else
- QDesktopServices::openUrl(QUrl::fromLocalFile(tranferable_shader_cache_folder_path));
+ QDesktopServices::openUrl(QUrl::fromLocalFile(transferable_shader_cache_folder_path));
#endif
}
@@ -1273,6 +1442,175 @@ static bool RomFSRawCopy(QProgressDialog& dialog, const FileSys::VirtualDir& src
return true;
}
+void GMainWindow::OnGameListRemoveInstalledEntry(u64 program_id, InstalledEntryType type) {
+ const QString entry_type = [this, type] {
+ switch (type) {
+ case InstalledEntryType::Game:
+ return tr("Contents");
+ case InstalledEntryType::Update:
+ return tr("Update");
+ case InstalledEntryType::AddOnContent:
+ return tr("DLC");
+ default:
+ return QString{};
+ }
+ }();
+
+ if (QMessageBox::question(
+ this, tr("Remove Entry"), tr("Remove Installed Game %1?").arg(entry_type),
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) {
+ return;
+ }
+
+ switch (type) {
+ case InstalledEntryType::Game:
+ RemoveBaseContent(program_id, entry_type);
+ [[fallthrough]];
+ case InstalledEntryType::Update:
+ RemoveUpdateContent(program_id, entry_type);
+ if (type != InstalledEntryType::Game) {
+ break;
+ }
+ [[fallthrough]];
+ case InstalledEntryType::AddOnContent:
+ RemoveAddOnContent(program_id, entry_type);
+ break;
+ }
+ Common::FS::DeleteDirRecursively(Common::FS::GetUserPath(Common::FS::UserPath::CacheDir) +
+ DIR_SEP + "game_list");
+ game_list->PopulateAsync(UISettings::values.game_dirs);
+}
+
+void GMainWindow::RemoveBaseContent(u64 program_id, const QString& entry_type) {
+ const auto& fs_controller = Core::System::GetInstance().GetFileSystemController();
+ const auto res = fs_controller.GetUserNANDContents()->RemoveExistingEntry(program_id) ||
+ fs_controller.GetSDMCContents()->RemoveExistingEntry(program_id);
+
+ if (res) {
+ QMessageBox::information(this, tr("Successfully Removed"),
+ tr("Successfully removed the installed base game."));
+ } else {
+ QMessageBox::warning(
+ this, tr("Error Removing %1").arg(entry_type),
+ tr("The base game is not installed in the NAND and cannot be removed."));
+ }
+}
+
+void GMainWindow::RemoveUpdateContent(u64 program_id, const QString& entry_type) {
+ const auto update_id = program_id | 0x800;
+ const auto& fs_controller = Core::System::GetInstance().GetFileSystemController();
+ const auto res = fs_controller.GetUserNANDContents()->RemoveExistingEntry(update_id) ||
+ fs_controller.GetSDMCContents()->RemoveExistingEntry(update_id);
+
+ if (res) {
+ QMessageBox::information(this, tr("Successfully Removed"),
+ tr("Successfully removed the installed update."));
+ } else {
+ QMessageBox::warning(this, tr("Error Removing %1").arg(entry_type),
+ tr("There is no update installed for this title."));
+ }
+}
+
+void GMainWindow::RemoveAddOnContent(u64 program_id, const QString& entry_type) {
+ u32 count{};
+ const auto& fs_controller = Core::System::GetInstance().GetFileSystemController();
+ const auto dlc_entries = Core::System::GetInstance().GetContentProvider().ListEntriesFilter(
+ FileSys::TitleType::AOC, FileSys::ContentRecordType::Data);
+
+ for (const auto& entry : dlc_entries) {
+ if ((entry.title_id & DLC_BASE_TITLE_ID_MASK) == program_id) {
+ const auto res =
+ fs_controller.GetUserNANDContents()->RemoveExistingEntry(entry.title_id) ||
+ fs_controller.GetSDMCContents()->RemoveExistingEntry(entry.title_id);
+ if (res) {
+ ++count;
+ }
+ }
+ }
+
+ if (count == 0) {
+ QMessageBox::warning(this, tr("Error Removing %1").arg(entry_type),
+ tr("There are no DLC installed for this title."));
+ return;
+ }
+
+ QMessageBox::information(this, tr("Successfully Removed"),
+ tr("Successfully removed %1 installed DLC.").arg(count));
+}
+
+void GMainWindow::OnGameListRemoveFile(u64 program_id, GameListRemoveTarget target) {
+ const QString question = [this, target] {
+ switch (target) {
+ case GameListRemoveTarget::ShaderCache:
+ return tr("Delete Transferable Shader Cache?");
+ case GameListRemoveTarget::CustomConfiguration:
+ return tr("Remove Custom Game Configuration?");
+ default:
+ return QString{};
+ }
+ }();
+
+ if (QMessageBox::question(this, tr("Remove File"), question, QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::No) != QMessageBox::Yes) {
+ return;
+ }
+
+ switch (target) {
+ case GameListRemoveTarget::ShaderCache:
+ RemoveTransferableShaderCache(program_id);
+ break;
+ case GameListRemoveTarget::CustomConfiguration:
+ RemoveCustomConfiguration(program_id);
+ break;
+ }
+}
+
+void GMainWindow::RemoveTransferableShaderCache(u64 program_id) {
+ const QString shader_dir =
+ QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::ShaderDir));
+ const QString transferable_shader_cache_folder_path =
+ shader_dir + QStringLiteral("opengl") + QDir::separator() + QStringLiteral("transferable");
+ const QString transferable_shader_cache_file_path =
+ transferable_shader_cache_folder_path + QDir::separator() +
+ QString::fromStdString(fmt::format("{:016X}.bin", program_id));
+
+ if (!QFile::exists(transferable_shader_cache_file_path)) {
+ QMessageBox::warning(this, tr("Error Removing Transferable Shader Cache"),
+ tr("A shader cache for this title does not exist."));
+ return;
+ }
+
+ if (QFile::remove(transferable_shader_cache_file_path)) {
+ QMessageBox::information(this, tr("Successfully Removed"),
+ tr("Successfully removed the transferable shader cache."));
+ } else {
+ QMessageBox::warning(this, tr("Error Removing Transferable Shader Cache"),
+ tr("Failed to remove the transferable shader cache."));
+ }
+}
+
+void GMainWindow::RemoveCustomConfiguration(u64 program_id) {
+ const QString config_dir =
+ QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::ConfigDir));
+ const QString custom_config_file_path =
+ config_dir + QStringLiteral("custom") + QDir::separator() +
+ QString::fromStdString(fmt::format("{:016X}.ini", program_id));
+
+ if (!QFile::exists(custom_config_file_path)) {
+ QMessageBox::warning(this, tr("Error Removing Custom Configuration"),
+ tr("A custom configuration for this title does not exist."));
+ return;
+ }
+
+ if (QFile::remove(custom_config_file_path)) {
+ QMessageBox::information(this, tr("Successfully Removed"),
+ tr("Successfully removed the custom game configuration."));
+ } else {
+ QMessageBox::warning(this, tr("Error Removing Custom Configuration"),
+ tr("Failed to remove the custom game configuration."));
+ }
+}
+
void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_path) {
const auto failed = [this] {
QMessageBox::warning(this, tr("RomFS Extraction Failed!"),
@@ -1280,7 +1618,8 @@ void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_pa
"cancelled the operation."));
};
- const auto loader = Loader::GetLoader(vfs->OpenFile(game_path, FileSys::Mode::Read));
+ auto& system = Core::System::GetInstance();
+ const auto loader = Loader::GetLoader(system, vfs->OpenFile(game_path, FileSys::Mode::Read));
if (loader == nullptr) {
failed();
return;
@@ -1292,7 +1631,7 @@ void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_pa
return;
}
- const auto& installed = Core::System::GetInstance().GetContentProvider();
+ const auto& installed = system.GetContentProvider();
const auto romfs_title_id = SelectRomFSDumpTarget(installed, program_id);
if (!romfs_title_id) {
@@ -1301,12 +1640,14 @@ void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_pa
}
const auto path = fmt::format(
- "{}{:016X}/romfs", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), *romfs_title_id);
+ "{}{:016X}/romfs", Common::FS::GetUserPath(Common::FS::UserPath::DumpDir), *romfs_title_id);
FileSys::VirtualFile romfs;
if (*romfs_title_id == program_id) {
- romfs = file;
+ const u64 ivfc_offset = loader->ReadRomFSIVFCOffset();
+ const FileSys::PatchManager pm{program_id, system.GetFileSystemController(), installed};
+ romfs = pm.PatchRomFS(file, ivfc_offset, FileSys::ContentRecordType::Program);
} else {
romfs = installed.GetEntry(*romfs_title_id, FileSys::ContentRecordType::Data)->GetRomFS();
}
@@ -1379,13 +1720,13 @@ void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id,
void GMainWindow::OnGameListOpenDirectory(const QString& directory) {
QString path;
if (directory == QStringLiteral("SDMC")) {
- path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) +
+ path = QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::SDMCDir) +
"Nintendo/Contents/registered");
} else if (directory == QStringLiteral("UserNAND")) {
- path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) +
+ path = QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::NANDDir) +
"user/Contents/registered");
} else if (directory == QStringLiteral("SysNAND")) {
- path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) +
+ path = QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::NANDDir) +
"system/Contents/registered");
} else {
path = directory;
@@ -1399,8 +1740,10 @@ void GMainWindow::OnGameListOpenDirectory(const QString& directory) {
void GMainWindow::OnGameListAddDirectory() {
const QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory"));
- if (dir_path.isEmpty())
+ if (dir_path.isEmpty()) {
return;
+ }
+
UISettings::GameDir game_dir{dir_path, false, true};
if (!UISettings::values.game_dirs.contains(game_dir)) {
UISettings::values.game_dirs.append(game_dir);
@@ -1420,26 +1763,15 @@ void GMainWindow::OnGameListShowList(bool show) {
void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) {
u64 title_id{};
const auto v_file = Core::GetGameFileFromPath(vfs, file);
- const auto loader = Loader::GetLoader(v_file);
+ const auto loader = Loader::GetLoader(Core::System::GetInstance(), v_file);
+
if (loader == nullptr || loader->ReadProgramId(title_id) != Loader::ResultStatus::Success) {
QMessageBox::information(this, tr("Properties"),
tr("The game properties could not be loaded."));
return;
}
- ConfigurePerGameGeneral dialog(this, title_id);
- dialog.LoadFromFile(v_file);
- auto result = dialog.exec();
- if (result == QDialog::Accepted) {
- dialog.ApplyConfiguration();
-
- const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false);
- if (reload) {
- game_list->PopulateAsync(UISettings::values.game_dirs);
- }
-
- config->Save();
- }
+ OpenPerGameConfiguration(title_id, file);
}
void GMainWindow::OnMenuLoadFile() {
@@ -1479,209 +1811,255 @@ void GMainWindow::OnMenuLoadFolder() {
}
}
+void GMainWindow::IncrementInstallProgress() {
+ install_progress->setValue(install_progress->value() + 1);
+}
+
void GMainWindow::OnMenuInstallToNAND() {
const QString file_filter =
tr("Installable Switch File (*.nca *.nsp *.xci);;Nintendo Content Archive "
- "(*.nca);;Nintendo Submissions Package (*.nsp);;NX Cartridge "
+ "(*.nca);;Nintendo Submission Package (*.nsp);;NX Cartridge "
"Image (*.xci)");
- QString filename = QFileDialog::getOpenFileName(this, tr("Install File"),
- UISettings::values.roms_path, file_filter);
- if (filename.isEmpty()) {
+ QStringList filenames = QFileDialog::getOpenFileNames(
+ this, tr("Install Files"), UISettings::values.roms_path, file_filter);
+
+ if (filenames.isEmpty()) {
+ return;
+ }
+
+ InstallDialog installDialog(this, filenames);
+ if (installDialog.exec() == QDialog::Rejected) {
+ return;
+ }
+
+ const QStringList files = installDialog.GetFiles();
+
+ if (files.isEmpty()) {
return;
}
+ int remaining = filenames.size();
+
+ // This would only overflow above 2^43 bytes (8.796 TB)
+ int total_size = 0;
+ for (const QString& file : files) {
+ total_size += static_cast<int>(QFile(file).size() / 0x1000);
+ }
+ if (total_size < 0) {
+ LOG_CRITICAL(Frontend, "Attempting to install too many files, aborting.");
+ return;
+ }
+
+ QStringList new_files{}; // Newly installed files that do not yet exist in the NAND
+ QStringList overwritten_files{}; // Files that overwrote those existing in the NAND
+ QStringList failed_files{}; // Files that failed to install due to errors
+
+ ui.action_Install_File_NAND->setEnabled(false);
+
+ install_progress = new QProgressDialog(QString{}, tr("Cancel"), 0, total_size, this);
+ install_progress->setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint &
+ ~Qt::WindowMaximizeButtonHint);
+ install_progress->setAttribute(Qt::WA_DeleteOnClose, true);
+ install_progress->setFixedWidth(installDialog.GetMinimumWidth() + 40);
+ install_progress->show();
+
+ for (const QString& file : files) {
+ install_progress->setWindowTitle(tr("%n file(s) remaining", "", remaining));
+ install_progress->setLabelText(
+ tr("Installing file \"%1\"...").arg(QFileInfo(file).fileName()));
+
+ QFuture<InstallResult> future;
+ InstallResult result;
+
+ if (file.endsWith(QStringLiteral("xci"), Qt::CaseInsensitive) ||
+ file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
+
+ future = QtConcurrent::run([this, &file] { return InstallNSPXCI(file); });
+
+ while (!future.isFinished()) {
+ QCoreApplication::processEvents();
+ }
+
+ result = future.result();
+
+ } else {
+ result = InstallNCA(file);
+ }
+
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
+
+ switch (result) {
+ case InstallResult::Success:
+ new_files.append(QFileInfo(file).fileName());
+ break;
+ case InstallResult::Overwrite:
+ overwritten_files.append(QFileInfo(file).fileName());
+ break;
+ case InstallResult::Failure:
+ failed_files.append(QFileInfo(file).fileName());
+ break;
+ }
+
+ --remaining;
+ }
+
+ install_progress->close();
+
+ const QString install_results =
+ (new_files.isEmpty() ? QString{}
+ : tr("%n file(s) were newly installed\n", "", new_files.size())) +
+ (overwritten_files.isEmpty()
+ ? QString{}
+ : tr("%n file(s) were overwritten\n", "", overwritten_files.size())) +
+ (failed_files.isEmpty() ? QString{}
+ : tr("%n file(s) failed to install\n", "", failed_files.size()));
+
+ QMessageBox::information(this, tr("Install Results"), install_results);
+ Common::FS::DeleteDirRecursively(Common::FS::GetUserPath(Common::FS::UserPath::CacheDir) +
+ DIR_SEP + "game_list");
+ game_list->PopulateAsync(UISettings::values.game_dirs);
+ ui.action_Install_File_NAND->setEnabled(true);
+}
+
+InstallResult GMainWindow::InstallNSPXCI(const QString& filename) {
const auto qt_raw_copy = [this](const FileSys::VirtualFile& src,
const FileSys::VirtualFile& dest, std::size_t block_size) {
- if (src == nullptr || dest == nullptr)
+ if (src == nullptr || dest == nullptr) {
return false;
- if (!dest->Resize(src->GetSize()))
+ }
+ if (!dest->Resize(src->GetSize())) {
return false;
+ }
std::array<u8, 0x1000> buffer{};
- const int progress_maximum = static_cast<int>(src->GetSize() / buffer.size());
-
- QProgressDialog progress(
- tr("Installing file \"%1\"...").arg(QString::fromStdString(src->GetName())),
- tr("Cancel"), 0, progress_maximum, this);
- progress.setWindowModality(Qt::WindowModal);
for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
- if (progress.wasCanceled()) {
+ if (install_progress->wasCanceled()) {
dest->Resize(0);
return false;
}
- const int progress_value = static_cast<int>(i / buffer.size());
- progress.setValue(progress_value);
+ emit UpdateInstallProgress();
const auto read = src->Read(buffer.data(), buffer.size(), i);
dest->Write(buffer.data(), read, i);
}
-
return true;
};
- const auto success = [this]() {
- QMessageBox::information(this, tr("Successfully Installed"),
- tr("The file was successfully installed."));
- game_list->PopulateAsync(UISettings::values.game_dirs);
- FileUtil::DeleteDirRecursively(FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) +
- DIR_SEP + "game_list");
- };
-
- const auto failed = [this]() {
- QMessageBox::warning(
- this, tr("Failed to Install"),
- tr("There was an error while attempting to install the provided file. It "
- "could have an incorrect format or be missing metadata. Please "
- "double-check your file and try again."));
- };
-
- const auto overwrite = [this]() {
- return QMessageBox::question(this, tr("Failed to Install"),
- tr("The file you are attempting to install already exists "
- "in the cache. Would you like to overwrite it?")) ==
- QMessageBox::Yes;
- };
-
- if (filename.endsWith(QStringLiteral("xci"), Qt::CaseInsensitive) ||
- filename.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
- std::shared_ptr<FileSys::NSP> nsp;
- if (filename.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
- nsp = std::make_shared<FileSys::NSP>(
- vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
- if (nsp->IsExtractedType())
- failed();
- } else {
- const auto xci = std::make_shared<FileSys::XCI>(
- vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
- nsp = xci->GetSecurePartitionNSP();
- }
-
- if (nsp->GetStatus() != Loader::ResultStatus::Success) {
- failed();
- return;
- }
- const auto res = Core::System::GetInstance()
- .GetFileSystemController()
- .GetUserNANDContents()
- ->InstallEntry(*nsp, false, qt_raw_copy);
- if (res == FileSys::InstallResult::Success) {
- success();
- } else {
- if (res == FileSys::InstallResult::ErrorAlreadyExists) {
- if (overwrite()) {
- const auto res2 = Core::System::GetInstance()
- .GetFileSystemController()
- .GetUserNANDContents()
- ->InstallEntry(*nsp, true, qt_raw_copy);
- if (res2 == FileSys::InstallResult::Success) {
- success();
- } else {
- failed();
- }
- }
- } else {
- failed();
- }
+ std::shared_ptr<FileSys::NSP> nsp;
+ if (filename.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
+ nsp = std::make_shared<FileSys::NSP>(
+ vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
+ if (nsp->IsExtractedType()) {
+ return InstallResult::Failure;
}
} else {
- const auto nca = std::make_shared<FileSys::NCA>(
+ const auto xci = std::make_shared<FileSys::XCI>(
vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
- const auto id = nca->GetStatus();
+ nsp = xci->GetSecurePartitionNSP();
+ }
- // Game updates necessary are missing base RomFS
- if (id != Loader::ResultStatus::Success &&
- id != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) {
- failed();
- return;
- }
+ if (nsp->GetStatus() != Loader::ResultStatus::Success) {
+ return InstallResult::Failure;
+ }
+ const auto res =
+ Core::System::GetInstance().GetFileSystemController().GetUserNANDContents()->InstallEntry(
+ *nsp, true, qt_raw_copy);
+ if (res == FileSys::InstallResult::Success) {
+ return InstallResult::Success;
+ } else if (res == FileSys::InstallResult::OverwriteExisting) {
+ return InstallResult::Overwrite;
+ } else {
+ return InstallResult::Failure;
+ }
+}
- const QStringList tt_options{tr("System Application"),
- tr("System Archive"),
- tr("System Application Update"),
- tr("Firmware Package (Type A)"),
- tr("Firmware Package (Type B)"),
- tr("Game"),
- tr("Game Update"),
- tr("Game DLC"),
- tr("Delta Title")};
- bool ok;
- const auto item = QInputDialog::getItem(
- this, tr("Select NCA Install Type..."),
- tr("Please select the type of title you would like to install this NCA as:\n(In "
- "most instances, the default 'Game' is fine.)"),
- tt_options, 5, false, &ok);
-
- auto index = tt_options.indexOf(item);
- if (!ok || index == -1) {
- QMessageBox::warning(this, tr("Failed to Install"),
- tr("The title type you selected for the NCA is invalid."));
- return;
+InstallResult GMainWindow::InstallNCA(const QString& filename) {
+ const auto qt_raw_copy = [this](const FileSys::VirtualFile& src,
+ const FileSys::VirtualFile& dest, std::size_t block_size) {
+ if (src == nullptr || dest == nullptr) {
+ return false;
}
-
- // If index is equal to or past Game, add the jump in TitleType.
- if (index >= 5) {
- index += static_cast<size_t>(FileSys::TitleType::Application) -
- static_cast<size_t>(FileSys::TitleType::FirmwarePackageB);
+ if (!dest->Resize(src->GetSize())) {
+ return false;
}
- FileSys::InstallResult res;
- if (index >= static_cast<size_t>(FileSys::TitleType::Application)) {
- res = Core::System::GetInstance()
- .GetFileSystemController()
- .GetUserNANDContents()
- ->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), false,
- qt_raw_copy);
- } else {
- res = Core::System::GetInstance()
- .GetFileSystemController()
- .GetSystemNANDContents()
- ->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), false,
- qt_raw_copy);
- }
+ std::array<u8, 0x1000> buffer{};
- if (res == FileSys::InstallResult::Success) {
- success();
- } else if (res == FileSys::InstallResult::ErrorAlreadyExists) {
- if (overwrite()) {
- const auto res2 = Core::System::GetInstance()
- .GetFileSystemController()
- .GetUserNANDContents()
- ->InstallEntry(*nca, static_cast<FileSys::TitleType>(index),
- true, qt_raw_copy);
- if (res2 == FileSys::InstallResult::Success) {
- success();
- } else {
- failed();
- }
+ for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
+ if (install_progress->wasCanceled()) {
+ dest->Resize(0);
+ return false;
}
- } else {
- failed();
- }
- }
-}
-void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target) {
- const auto res = QMessageBox::information(
- this, tr("Changing Emulated Directory"),
- tr("You are about to change the emulated %1 directory of the system. Please note "
- "that this does not also move the contents of the previous directory to the "
- "new one and you will have to do that yourself.")
- .arg(target == EmulatedDirectoryTarget::SDMC ? tr("SD card") : tr("NAND")),
- QMessageBox::StandardButtons{QMessageBox::Ok, QMessageBox::Cancel});
+ emit UpdateInstallProgress();
- if (res == QMessageBox::Cancel)
- return;
+ const auto read = src->Read(buffer.data(), buffer.size(), i);
+ dest->Write(buffer.data(), read, i);
+ }
+ return true;
+ };
- QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory"));
- if (!dir_path.isEmpty()) {
- FileUtil::GetUserPath(target == EmulatedDirectoryTarget::SDMC ? FileUtil::UserPath::SDMCDir
- : FileUtil::UserPath::NANDDir,
- dir_path.toStdString());
- Core::System::GetInstance().GetFileSystemController().CreateFactories(*vfs);
- game_list->PopulateAsync(UISettings::values.game_dirs);
+ const auto nca =
+ std::make_shared<FileSys::NCA>(vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
+ const auto id = nca->GetStatus();
+
+ // Game updates necessary are missing base RomFS
+ if (id != Loader::ResultStatus::Success &&
+ id != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) {
+ return InstallResult::Failure;
+ }
+
+ const QStringList tt_options{tr("System Application"),
+ tr("System Archive"),
+ tr("System Application Update"),
+ tr("Firmware Package (Type A)"),
+ tr("Firmware Package (Type B)"),
+ tr("Game"),
+ tr("Game Update"),
+ tr("Game DLC"),
+ tr("Delta Title")};
+ bool ok;
+ const auto item = QInputDialog::getItem(
+ this, tr("Select NCA Install Type..."),
+ tr("Please select the type of title you would like to install this NCA as:\n(In "
+ "most instances, the default 'Game' is fine.)"),
+ tt_options, 5, false, &ok);
+
+ auto index = tt_options.indexOf(item);
+ if (!ok || index == -1) {
+ QMessageBox::warning(this, tr("Failed to Install"),
+ tr("The title type you selected for the NCA is invalid."));
+ return InstallResult::Failure;
+ }
+
+ // If index is equal to or past Game, add the jump in TitleType.
+ if (index >= 5) {
+ index += static_cast<size_t>(FileSys::TitleType::Application) -
+ static_cast<size_t>(FileSys::TitleType::FirmwarePackageB);
+ }
+
+ FileSys::InstallResult res;
+ if (index >= static_cast<s32>(FileSys::TitleType::Application)) {
+ res = Core::System::GetInstance()
+ .GetFileSystemController()
+ .GetUserNANDContents()
+ ->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), true, qt_raw_copy);
+ } else {
+ res = Core::System::GetInstance()
+ .GetFileSystemController()
+ .GetSystemNANDContents()
+ ->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), true, qt_raw_copy);
+ }
+
+ if (res == FileSys::InstallResult::Success) {
+ return InstallResult::Success;
+ } else if (res == FileSys::InstallResult::OverwriteExisting) {
+ return InstallResult::Overwrite;
+ } else {
+ return InstallResult::Failure;
}
}
@@ -1707,6 +2085,7 @@ void GMainWindow::OnStartGame() {
emu_thread->SetRunning(true);
+ qRegisterMetaType<Core::Frontend::ControllerParameters>("Core::Frontend::ControllerParameters");
qRegisterMetaType<Core::Frontend::SoftwareKeyboardParameters>(
"Core::Frontend::SoftwareKeyboardParameters");
qRegisterMetaType<Core::System::ResultStatus>("Core::System::ResultStatus");
@@ -1722,6 +2101,7 @@ void GMainWindow::OnStartGame() {
ui.action_Pause->setEnabled(true);
ui.action_Stop->setEnabled(true);
ui.action_Restart->setEnabled(true);
+ ui.action_Configure_Current_Game->setEnabled(true);
ui.action_Report_Compatibility->setEnabled(true);
discord_rpc->Update();
@@ -1747,6 +2127,9 @@ void GMainWindow::OnStopGame() {
}
ShutdownGame();
+
+ Settings::RestoreGlobalState();
+ UpdateStatusButtons();
}
void GMainWindow::OnLoadComplete() {
@@ -1772,6 +2155,26 @@ void GMainWindow::OnMenuReportCompatibility() {
}
}
+void GMainWindow::OpenURL(const QUrl& url) {
+ const bool open = QDesktopServices::openUrl(url);
+ if (!open) {
+ QMessageBox::warning(this, tr("Error opening URL"),
+ tr("Unable to open the URL \"%1\".").arg(url.toString()));
+ }
+}
+
+void GMainWindow::OnOpenModsPage() {
+ OpenURL(QUrl(QStringLiteral("https://github.com/yuzu-emu/yuzu/wiki/Switch-Mods")));
+}
+
+void GMainWindow::OnOpenQuickstartGuide() {
+ OpenURL(QUrl(QStringLiteral("https://yuzu-emu.org/help/quickstart/")));
+}
+
+void GMainWindow::OnOpenFAQ() {
+ OpenURL(QUrl(QStringLiteral("https://yuzu-emu.org/wiki/faq/")));
+}
+
void GMainWindow::ToggleFullscreen() {
if (!emulation_running) {
return;
@@ -1832,11 +2235,28 @@ void GMainWindow::ToggleWindowMode() {
}
}
+void GMainWindow::ResetWindowSize() {
+ const auto aspect_ratio = Layout::EmulationAspectRatio(
+ static_cast<Layout::AspectRatio>(Settings::values.aspect_ratio.GetValue()),
+ static_cast<float>(Layout::ScreenUndocked::Height) / Layout::ScreenUndocked::Width);
+ if (!ui.action_Single_Window_Mode->isChecked()) {
+ render_window->resize(Layout::ScreenUndocked::Height / aspect_ratio,
+ Layout::ScreenUndocked::Height);
+ } else {
+ resize(Layout::ScreenUndocked::Height / aspect_ratio,
+ Layout::ScreenUndocked::Height + menuBar()->height() +
+ (ui.action_Show_Status_Bar->isChecked() ? statusBar()->height() : 0));
+ }
+}
+
void GMainWindow::OnConfigure() {
const auto old_theme = UISettings::values.theme;
const bool old_discord_presence = UISettings::values.enable_discord_presence;
- ConfigureDialog configure_dialog(this, hotkey_registry);
+ ConfigureDialog configure_dialog(this, hotkey_registry, input_subsystem.get());
+ connect(&configure_dialog, &ConfigureDialog::LanguageChanged, this,
+ &GMainWindow::OnLanguageChanged);
+
const auto result = configure_dialog.exec();
if (result != QDialog::Accepted) {
return;
@@ -1859,12 +2279,46 @@ void GMainWindow::OnConfigure() {
config->Save();
- dock_status_button->setChecked(Settings::values.use_docked_mode);
- async_status_button->setChecked(Settings::values.use_asynchronous_gpu_emulation);
-#ifdef HAS_VULKAN
- renderer_status_button->setChecked(Settings::values.renderer_backend ==
- Settings::RendererBackend::Vulkan);
-#endif
+ if (UISettings::values.hide_mouse && emulation_running) {
+ setMouseTracking(true);
+ ui.centralwidget->setMouseTracking(true);
+ mouse_hide_timer.start();
+ } else {
+ setMouseTracking(false);
+ ui.centralwidget->setMouseTracking(false);
+ }
+
+ UpdateStatusButtons();
+}
+
+void GMainWindow::OnConfigurePerGame() {
+ const u64 title_id = Core::System::GetInstance().CurrentProcess()->GetTitleID();
+ OpenPerGameConfiguration(title_id, game_path.toStdString());
+}
+
+void GMainWindow::OpenPerGameConfiguration(u64 title_id, const std::string& file_name) {
+ const auto v_file = Core::GetGameFileFromPath(vfs, file_name);
+
+ ConfigurePerGame dialog(this, title_id);
+ dialog.LoadFromFile(v_file);
+ auto result = dialog.exec();
+ if (result == QDialog::Accepted) {
+ dialog.ApplyConfiguration();
+
+ const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false);
+ if (reload) {
+ game_list->PopulateAsync(UISettings::values.game_dirs);
+ }
+
+ // Do not cause the global config to write local settings into the config file
+ Settings::RestoreGlobalState();
+
+ if (!Core::System::GetInstance().IsPoweredOn()) {
+ config->Save();
+ }
+ } else {
+ Settings::RestoreGlobalState();
+ }
}
void GMainWindow::OnLoadAmiibo() {
@@ -1914,7 +2368,7 @@ void GMainWindow::LoadAmiibo(const QString& filename) {
void GMainWindow::OnOpenYuzuFolder() {
QDesktopServices::openUrl(QUrl::fromLocalFile(
- QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::UserDir))));
+ QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::UserDir))));
}
void GMainWindow::OnAbout() {
@@ -1923,31 +2377,66 @@ void GMainWindow::OnAbout() {
}
void GMainWindow::OnToggleFilterBar() {
- game_list->setFilterVisible(ui.action_Show_Filter_Bar->isChecked());
+ game_list->SetFilterVisible(ui.action_Show_Filter_Bar->isChecked());
if (ui.action_Show_Filter_Bar->isChecked()) {
- game_list->setFilterFocus();
+ game_list->SetFilterFocus();
} else {
- game_list->clearFilter();
+ game_list->ClearFilter();
}
}
void GMainWindow::OnCaptureScreenshot() {
OnPauseGame();
- QFileDialog png_dialog(this, tr("Capture Screenshot"), UISettings::values.screenshot_path,
- tr("PNG Image (*.png)"));
- png_dialog.setAcceptMode(QFileDialog::AcceptSave);
- png_dialog.setDefaultSuffix(QStringLiteral("png"));
- if (png_dialog.exec()) {
- const QString path = png_dialog.selectedFiles().first();
- if (!path.isEmpty()) {
- UISettings::values.screenshot_path = QFileInfo(path).path();
- render_window->CaptureScreenshot(UISettings::values.screenshot_resolution_factor, path);
+
+ const u64 title_id = Core::System::GetInstance().CurrentProcess()->GetTitleID();
+ const auto screenshot_path =
+ QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::ScreenshotsDir));
+ const auto date =
+ QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd_hh-mm-ss-zzz"));
+ QString filename = QStringLiteral("%1%2_%3.png")
+ .arg(screenshot_path)
+ .arg(title_id, 16, 16, QLatin1Char{'0'})
+ .arg(date);
+
+#ifdef _WIN32
+ if (UISettings::values.enable_screenshot_save_as) {
+ filename = QFileDialog::getSaveFileName(this, tr("Capture Screenshot"), filename,
+ tr("PNG Image (*.png)"));
+ if (filename.isEmpty()) {
+ OnStartGame();
+ return;
}
}
+#endif
+ render_window->CaptureScreenshot(UISettings::values.screenshot_resolution_factor, filename);
OnStartGame();
}
-void GMainWindow::UpdateWindowTitle(const QString& title_name) {
+// TODO: Written 2020-10-01: Remove per-game config migration code when it is irrelevant
+void GMainWindow::MigrateConfigFiles() {
+ const std::string& config_dir_str = Common::FS::GetUserPath(Common::FS::UserPath::ConfigDir);
+ const QDir config_dir = QDir(QString::fromStdString(config_dir_str));
+ const QStringList config_dir_list = config_dir.entryList(QStringList(QStringLiteral("*.ini")));
+
+ Common::FS::CreateFullPath(fmt::format("{}custom" DIR_SEP, config_dir_str));
+ for (QStringList::const_iterator it = config_dir_list.constBegin();
+ it != config_dir_list.constEnd(); ++it) {
+ const auto filename = it->toStdString();
+ if (filename.find_first_not_of("0123456789abcdefACBDEF", 0) < 16) {
+ continue;
+ }
+ const auto origin = fmt::format("{}{}", config_dir_str, filename);
+ const auto destination = fmt::format("{}custom" DIR_SEP "{}", config_dir_str, filename);
+ LOG_INFO(Frontend, "Migrating config file from {} to {}", origin, destination);
+ if (!Common::FS::Rename(origin, destination)) {
+ // Delete the old config file if one already exists in the new location.
+ Common::FS::Delete(origin);
+ }
+ }
+}
+
+void GMainWindow::UpdateWindowTitle(const std::string& title_name,
+ const std::string& title_version) {
const auto full_name = std::string(Common::g_build_fullname);
const auto branch_name = std::string(Common::g_scm_branch);
const auto description = std::string(Common::g_scm_desc);
@@ -1956,7 +2445,7 @@ void GMainWindow::UpdateWindowTitle(const QString& title_name) {
const auto date =
QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd")).toStdString();
- if (title_name.isEmpty()) {
+ if (title_name.empty()) {
const auto fmt = std::string(Common::g_title_bar_format_idle);
setWindowTitle(QString::fromStdString(fmt::format(fmt.empty() ? "yuzu {0}| {1}-{2}" : fmt,
full_name, branch_name, description,
@@ -1964,8 +2453,8 @@ void GMainWindow::UpdateWindowTitle(const QString& title_name) {
} else {
const auto fmt = std::string(Common::g_title_bar_format_running);
setWindowTitle(QString::fromStdString(
- fmt::format(fmt.empty() ? "yuzu {0}| {3} | {1}-{2}" : fmt, full_name, branch_name,
- description, title_name.toStdString(), date, build_id)));
+ fmt::format(fmt.empty() ? "yuzu {0}| {3} | {6} | {1}-{2}" : fmt, full_name, branch_name,
+ description, title_name, date, build_id, title_version)));
}
}
@@ -1976,22 +2465,69 @@ void GMainWindow::UpdateStatusBar() {
}
auto results = Core::System::GetInstance().GetAndResetPerfStats();
+ auto& shader_notify = Core::System::GetInstance().GPU().ShaderNotify();
+ const auto shaders_building = shader_notify.GetShadersBuilding();
- if (Settings::values.use_frame_limit) {
+ if (shaders_building != 0) {
+ shader_building_label->setText(
+ tr("Building: %n shader(s)", "", static_cast<int>(shaders_building)));
+ shader_building_label->setVisible(true);
+ } else {
+ shader_building_label->setVisible(false);
+ }
+
+ if (Settings::values.use_frame_limit.GetValue()) {
emu_speed_label->setText(tr("Speed: %1% / %2%")
.arg(results.emulation_speed * 100.0, 0, 'f', 0)
- .arg(Settings::values.frame_limit));
+ .arg(Settings::values.frame_limit.GetValue()));
} else {
emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0));
}
game_fps_label->setText(tr("Game: %1 FPS").arg(results.game_fps, 0, 'f', 0));
emu_frametime_label->setText(tr("Frame: %1 ms").arg(results.frametime * 1000.0, 0, 'f', 2));
- emu_speed_label->setVisible(true);
+ emu_speed_label->setVisible(!Settings::values.use_multi_core.GetValue());
game_fps_label->setVisible(true);
emu_frametime_label->setVisible(true);
}
+void GMainWindow::UpdateStatusButtons() {
+ dock_status_button->setChecked(Settings::values.use_docked_mode.GetValue());
+ multicore_status_button->setChecked(Settings::values.use_multi_core.GetValue());
+ Settings::values.use_asynchronous_gpu_emulation.SetValue(
+ Settings::values.use_asynchronous_gpu_emulation.GetValue() ||
+ Settings::values.use_multi_core.GetValue());
+ async_status_button->setChecked(Settings::values.use_asynchronous_gpu_emulation.GetValue());
+#ifdef HAS_VULKAN
+ renderer_status_button->setChecked(Settings::values.renderer_backend.GetValue() ==
+ Settings::RendererBackend::Vulkan);
+#endif
+}
+
+void GMainWindow::HideMouseCursor() {
+ if (emu_thread == nullptr || UISettings::values.hide_mouse == false) {
+ mouse_hide_timer.stop();
+ ShowMouseCursor();
+ return;
+ }
+ setCursor(QCursor(Qt::BlankCursor));
+}
+
+void GMainWindow::ShowMouseCursor() {
+ unsetCursor();
+ if (emu_thread != nullptr && UISettings::values.hide_mouse) {
+ mouse_hide_timer.start();
+ }
+}
+
+void GMainWindow::mouseMoveEvent(QMouseEvent* event) {
+ ShowMouseCursor();
+}
+
+void GMainWindow::mousePressEvent(QMouseEvent* event) {
+ ShowMouseCursor();
+}
+
void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string details) {
QMessageBox::StandardButton answer;
QString status_message;
@@ -2051,6 +2587,9 @@ void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string det
if (answer == QMessageBox::Yes) {
if (emu_thread) {
ShutdownGame();
+
+ Settings::RestoreGlobalState();
+ UpdateStatusButtons();
}
} else {
// Only show the message if the game is still running.
@@ -2065,57 +2604,60 @@ void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) {
if (behavior == ReinitializeKeyBehavior::Warning) {
const auto res = QMessageBox::information(
this, tr("Confirm Key Rederivation"),
- tr("You are about to force rederive all of your keys. \nIf you do not know what this "
- "means or what you are doing, \nthis is a potentially destructive action. \nPlease "
- "make sure this is what you want \nand optionally make backups.\n\nThis will delete "
+ tr("You are about to force rederive all of your keys. \nIf you do not know what "
+ "this "
+ "means or what you are doing, \nthis is a potentially destructive action. "
+ "\nPlease "
+ "make sure this is what you want \nand optionally make backups.\n\nThis will "
+ "delete "
"your autogenerated key files and re-run the key derivation module."),
QMessageBox::StandardButtons{QMessageBox::Ok, QMessageBox::Cancel});
if (res == QMessageBox::Cancel)
return;
- FileUtil::Delete(FileUtil::GetUserPath(FileUtil::UserPath::KeysDir) +
- "prod.keys_autogenerated");
- FileUtil::Delete(FileUtil::GetUserPath(FileUtil::UserPath::KeysDir) +
- "console.keys_autogenerated");
- FileUtil::Delete(FileUtil::GetUserPath(FileUtil::UserPath::KeysDir) +
- "title.keys_autogenerated");
+ Common::FS::Delete(Common::FS::GetUserPath(Common::FS::UserPath::KeysDir) +
+ "prod.keys_autogenerated");
+ Common::FS::Delete(Common::FS::GetUserPath(Common::FS::UserPath::KeysDir) +
+ "console.keys_autogenerated");
+ Common::FS::Delete(Common::FS::GetUserPath(Common::FS::UserPath::KeysDir) +
+ "title.keys_autogenerated");
}
- Core::Crypto::KeyManager keys{};
+ Core::Crypto::KeyManager& keys = Core::Crypto::KeyManager::Instance();
if (keys.BaseDeriveNecessary()) {
Core::Crypto::PartitionDataManager pdm{vfs->OpenDirectory(
- FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir), FileSys::Mode::Read)};
+ Common::FS::GetUserPath(Common::FS::UserPath::SysDataDir), FileSys::Mode::Read)};
const auto function = [this, &keys, &pdm] {
keys.PopulateFromPartitionData(pdm);
- Core::System::GetInstance().GetFileSystemController().CreateFactories(*vfs);
- keys.DeriveETicket(pdm);
+
+ auto& system = Core::System::GetInstance();
+ system.GetFileSystemController().CreateFactories(*vfs);
+ keys.DeriveETicket(pdm, system.GetContentProvider());
};
QString errors;
if (!pdm.HasFuses()) {
- errors += tr("- Missing fuses - Cannot derive SBK\n");
+ errors += tr("Missing fuses");
}
if (!pdm.HasBoot0()) {
- errors += tr("- Missing BOOT0 - Cannot derive master keys\n");
+ errors += tr(" - Missing BOOT0");
}
if (!pdm.HasPackage2()) {
- errors += tr("- Missing BCPKG2-1-Normal-Main - Cannot derive general keys\n");
+ errors += tr(" - Missing BCPKG2-1-Normal-Main");
}
if (!pdm.HasProdInfo()) {
- errors += tr("- Missing PRODINFO - Cannot derive title keys\n");
+ errors += tr(" - Missing PRODINFO");
}
if (!errors.isEmpty()) {
QMessageBox::warning(
- this, tr("Warning Missing Derivation Components"),
- tr("The following are missing from your configuration that may hinder key "
- "derivation. It will be attempted but may not complete.<br><br>") +
- errors +
- tr("<br><br>You can get all of these and dump all of your games easily by "
- "following <a href='https://yuzu-emu.org/help/quickstart/'>the "
- "quickstart guide</a>. Alternatively, you can use another method of dumping "
- "to obtain all of your keys."));
+ this, tr("Derivation Components Missing"),
+ tr("Components are missing that may hinder key derivation from completing. "
+ "<br>Please follow <a href='https://yuzu-emu.org/help/quickstart/'>the yuzu "
+ "quickstart guide</a> to get all your keys and "
+ "games.<br><br><small>(%1)</small>")
+ .arg(errors));
}
QProgressDialog prog;
@@ -2215,9 +2757,13 @@ void GMainWindow::closeEvent(QCloseEvent* event) {
hotkey_registry.SaveHotkeys();
// Shutdown session if the emu thread is active...
- if (emu_thread != nullptr)
+ if (emu_thread != nullptr) {
ShutdownGame();
+ Settings::RestoreGlobalState();
+ UpdateStatusButtons();
+ }
+
render_window->close();
QWidget::closeEvent(event);
@@ -2348,6 +2894,43 @@ void GMainWindow::UpdateUITheme() {
QIcon::setThemeSearchPaths(theme_paths);
}
+void GMainWindow::LoadTranslation() {
+ // If the selected language is English, no need to install any translation
+ if (UISettings::values.language == QStringLiteral("en")) {
+ return;
+ }
+
+ bool loaded;
+
+ if (UISettings::values.language.isEmpty()) {
+ // If the selected language is empty, use system locale
+ loaded = translator.load(QLocale(), {}, {}, QStringLiteral(":/languages/"));
+ } else {
+ // Otherwise load from the specified file
+ loaded = translator.load(UISettings::values.language, QStringLiteral(":/languages/"));
+ }
+
+ if (loaded) {
+ qApp->installTranslator(&translator);
+ } else {
+ UISettings::values.language = QStringLiteral("en");
+ }
+}
+
+void GMainWindow::OnLanguageChanged(const QString& locale) {
+ if (UISettings::values.language != QStringLiteral("en")) {
+ qApp->removeTranslator(&translator);
+ }
+
+ UISettings::values.language = locale;
+ LoadTranslation();
+ ui.retranslateUi(this);
+ UpdateWindowTitle();
+
+ if (emulation_running)
+ ui.action_Start->setText(tr("Continue"));
+}
+
void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) {
#ifdef USE_DISCORD_PRESENCE
if (state) {
@@ -2376,9 +2959,9 @@ int main(int argc, char* argv[]) {
#ifdef __APPLE__
// If you start a bundle (binary) on OSX without the Terminal, the working directory is "/".
- // But since we require the working directory to be the executable path for the location of the
- // user folder in the Qt Frontend, we need to cd into that working directory
- const std::string bin_path = FileUtil::GetBundleDirectory() + DIR_SEP + "..";
+ // But since we require the working directory to be the executable path for the location of
+ // the user folder in the Qt Frontend, we need to cd into that working directory
+ const std::string bin_path = Common::FS::GetBundleDirectory() + DIR_SEP + "..";
chdir(bin_path.c_str());
#endif
@@ -2397,8 +2980,6 @@ int main(int argc, char* argv[]) {
QObject::connect(&app, &QGuiApplication::applicationStateChanged, &main_window,
&GMainWindow::OnAppFocusStateChanged);
- Settings::LogSettings();
-
int result = app.exec();
detached_tasks.WaitForAllTasks();
return result;