main.cpp 67 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746
  1. // Copyright 2014 Citra Emulator Project
  2. // Licensed under GPLv2 or any later version
  3. // Refer to the license.txt file included.
  4. #include <cinttypes>
  5. #include <clocale>
  6. #include <memory>
  7. #include <thread>
  8. // VFS includes must be before glad as they will conflict with Windows file api, which uses defines.
  9. #include "core/file_sys/vfs.h"
  10. #include "core/file_sys/vfs_real.h"
  11. #include "core/hle/service/acc/profile_manager.h"
  12. // These are wrappers to avoid the calls to CreateDirectory and CreateFile becuase of the Windows
  13. // defines.
  14. static FileSys::VirtualDir VfsFilesystemCreateDirectoryWrapper(
  15. const FileSys::VirtualFilesystem& vfs, const std::string& path, FileSys::Mode mode) {
  16. return vfs->CreateDirectory(path, mode);
  17. }
  18. static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::VirtualDir& dir,
  19. const std::string& path) {
  20. return dir->CreateFile(path);
  21. }
  22. #include <fmt/ostream.h>
  23. #include <glad/glad.h>
  24. #define QT_NO_OPENGL
  25. #include <QDesktopWidget>
  26. #include <QDialogButtonBox>
  27. #include <QFile>
  28. #include <QFileDialog>
  29. #include <QMessageBox>
  30. #include <QtConcurrent/QtConcurrent>
  31. #include <QtGui>
  32. #include <QtWidgets>
  33. #include <fmt/format.h>
  34. #include "common/common_paths.h"
  35. #include "common/detached_tasks.h"
  36. #include "common/file_util.h"
  37. #include "common/logging/backend.h"
  38. #include "common/logging/filter.h"
  39. #include "common/logging/log.h"
  40. #include "common/microprofile.h"
  41. #include "common/scm_rev.h"
  42. #include "common/scope_exit.h"
  43. #include "common/string_util.h"
  44. #include "common/telemetry.h"
  45. #include "core/core.h"
  46. #include "core/crypto/key_manager.h"
  47. #include "core/file_sys/bis_factory.h"
  48. #include "core/file_sys/card_image.h"
  49. #include "core/file_sys/content_archive.h"
  50. #include "core/file_sys/control_metadata.h"
  51. #include "core/file_sys/patch_manager.h"
  52. #include "core/file_sys/registered_cache.h"
  53. #include "core/file_sys/romfs.h"
  54. #include "core/file_sys/savedata_factory.h"
  55. #include "core/file_sys/submission_package.h"
  56. #include "core/hle/kernel/process.h"
  57. #include "core/hle/service/filesystem/filesystem.h"
  58. #include "core/hle/service/filesystem/fsp_ldr.h"
  59. #include "core/hle/service/nfp/nfp.h"
  60. #include "core/hle/service/sm/sm.h"
  61. #include "core/loader/loader.h"
  62. #include "core/perf_stats.h"
  63. #include "core/settings.h"
  64. #include "core/telemetry_session.h"
  65. #include "video_core/debug_utils/debug_utils.h"
  66. #include "yuzu/about_dialog.h"
  67. #include "yuzu/bootmanager.h"
  68. #include "yuzu/compatdb.h"
  69. #include "yuzu/compatibility_list.h"
  70. #include "yuzu/configuration/config.h"
  71. #include "yuzu/configuration/configure_dialog.h"
  72. #include "yuzu/debugger/console.h"
  73. #include "yuzu/debugger/graphics/graphics_breakpoints.h"
  74. #include "yuzu/debugger/graphics/graphics_surface.h"
  75. #include "yuzu/debugger/profiler.h"
  76. #include "yuzu/debugger/wait_tree.h"
  77. #include "yuzu/discord.h"
  78. #include "yuzu/game_list.h"
  79. #include "yuzu/game_list_p.h"
  80. #include "yuzu/hotkeys.h"
  81. #include "yuzu/main.h"
  82. #include "yuzu/ui_settings.h"
  83. #ifdef USE_DISCORD_PRESENCE
  84. #include "yuzu/discord_impl.h"
  85. #endif
  86. #ifdef QT_STATICPLUGIN
  87. Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin);
  88. #endif
  89. #ifdef _WIN32
  90. extern "C" {
  91. // tells Nvidia and AMD drivers to use the dedicated GPU by default on laptops with switchable
  92. // graphics
  93. __declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001;
  94. __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
  95. }
  96. #endif
  97. constexpr u64 DLC_BASE_TITLE_ID_MASK = 0xFFFFFFFFFFFFE000;
  98. /**
  99. * "Callouts" are one-time instructional messages shown to the user. In the config settings, there
  100. * is a bitfield "callout_flags" options, used to track if a message has already been shown to the
  101. * user. This is 32-bits - if we have more than 32 callouts, we should retire and recyle old ones.
  102. */
  103. enum class CalloutFlag : uint32_t {
  104. Telemetry = 0x1,
  105. DRDDeprecation = 0x2,
  106. };
  107. void GMainWindow::ShowTelemetryCallout() {
  108. if (UISettings::values.callout_flags & static_cast<uint32_t>(CalloutFlag::Telemetry)) {
  109. return;
  110. }
  111. UISettings::values.callout_flags |= static_cast<uint32_t>(CalloutFlag::Telemetry);
  112. const QString telemetry_message =
  113. tr("<a href='https://yuzu-emu.org/help/feature/telemetry/'>Anonymous "
  114. "data is collected</a> to help improve yuzu. "
  115. "<br/><br/>Would you like to share your usage data with us?");
  116. if (QMessageBox::question(this, tr("Telemetry"), telemetry_message) != QMessageBox::Yes) {
  117. Settings::values.enable_telemetry = false;
  118. Settings::Apply();
  119. }
  120. }
  121. const int GMainWindow::max_recent_files_item;
  122. static void InitializeLogging() {
  123. Log::Filter log_filter;
  124. log_filter.ParseFilterString(Settings::values.log_filter);
  125. Log::SetGlobalFilter(log_filter);
  126. const std::string& log_dir = FileUtil::GetUserPath(FileUtil::UserPath::LogDir);
  127. FileUtil::CreateFullPath(log_dir);
  128. Log::AddBackend(std::make_unique<Log::FileBackend>(log_dir + LOG_FILE));
  129. }
  130. GMainWindow::GMainWindow()
  131. : config(new Config()), emu_thread(nullptr),
  132. vfs(std::make_shared<FileSys::RealVfsFilesystem>()) {
  133. InitializeLogging();
  134. debug_context = Tegra::DebugContext::Construct();
  135. setAcceptDrops(true);
  136. ui.setupUi(this);
  137. statusBar()->hide();
  138. default_theme_paths = QIcon::themeSearchPaths();
  139. UpdateUITheme();
  140. SetDiscordEnabled(UISettings::values.enable_discord_presence);
  141. discord_rpc->Update();
  142. InitializeWidgets();
  143. InitializeDebugWidgets();
  144. InitializeRecentFileMenuActions();
  145. InitializeHotkeys();
  146. SetDefaultUIGeometry();
  147. RestoreUIState();
  148. ConnectMenuEvents();
  149. ConnectWidgetEvents();
  150. LOG_INFO(Frontend, "yuzu Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch,
  151. Common::g_scm_desc);
  152. setWindowTitle(QString("yuzu %1| %2-%3")
  153. .arg(Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc));
  154. show();
  155. // Gen keys if necessary
  156. OnReinitializeKeys(ReinitializeKeyBehavior::NoWarning);
  157. // Necessary to load titles from nand in gamelist.
  158. Service::FileSystem::CreateFactories(*vfs);
  159. game_list->LoadCompatibilityList();
  160. game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan);
  161. // Show one-time "callout" messages to the user
  162. ShowTelemetryCallout();
  163. QStringList args = QApplication::arguments();
  164. if (args.length() >= 2) {
  165. BootGame(args[1]);
  166. }
  167. }
  168. GMainWindow::~GMainWindow() {
  169. // will get automatically deleted otherwise
  170. if (render_window->parent() == nullptr)
  171. delete render_window;
  172. }
  173. void GMainWindow::InitializeWidgets() {
  174. #ifdef YUZU_ENABLE_COMPATIBILITY_REPORTING
  175. ui.action_Report_Compatibility->setVisible(true);
  176. #endif
  177. render_window = new GRenderWindow(this, emu_thread.get());
  178. render_window->hide();
  179. game_list = new GameList(vfs, this);
  180. ui.horizontalLayout->addWidget(game_list);
  181. // Create status bar
  182. message_label = new QLabel();
  183. // Configured separately for left alignment
  184. message_label->setVisible(false);
  185. message_label->setFrameStyle(QFrame::NoFrame);
  186. message_label->setContentsMargins(4, 0, 4, 0);
  187. message_label->setAlignment(Qt::AlignLeft);
  188. statusBar()->addPermanentWidget(message_label, 1);
  189. emu_speed_label = new QLabel();
  190. emu_speed_label->setToolTip(
  191. tr("Current emulation speed. Values higher or lower than 100% "
  192. "indicate emulation is running faster or slower than a Switch."));
  193. game_fps_label = new QLabel();
  194. game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. "
  195. "This will vary from game to game and scene to scene."));
  196. emu_frametime_label = new QLabel();
  197. emu_frametime_label->setToolTip(
  198. tr("Time taken to emulate a Switch frame, not counting framelimiting or v-sync. For "
  199. "full-speed emulation this should be at most 16.67 ms."));
  200. for (auto& label : {emu_speed_label, game_fps_label, emu_frametime_label}) {
  201. label->setVisible(false);
  202. label->setFrameStyle(QFrame::NoFrame);
  203. label->setContentsMargins(4, 0, 4, 0);
  204. statusBar()->addPermanentWidget(label, 0);
  205. }
  206. statusBar()->setVisible(true);
  207. setStyleSheet("QStatusBar::item{border: none;}");
  208. }
  209. void GMainWindow::InitializeDebugWidgets() {
  210. QMenu* debug_menu = ui.menu_View_Debugging;
  211. #if MICROPROFILE_ENABLED
  212. microProfileDialog = new MicroProfileDialog(this);
  213. microProfileDialog->hide();
  214. debug_menu->addAction(microProfileDialog->toggleViewAction());
  215. #endif
  216. graphicsBreakpointsWidget = new GraphicsBreakPointsWidget(debug_context, this);
  217. addDockWidget(Qt::RightDockWidgetArea, graphicsBreakpointsWidget);
  218. graphicsBreakpointsWidget->hide();
  219. debug_menu->addAction(graphicsBreakpointsWidget->toggleViewAction());
  220. graphicsSurfaceWidget = new GraphicsSurfaceWidget(debug_context, this);
  221. addDockWidget(Qt::RightDockWidgetArea, graphicsSurfaceWidget);
  222. graphicsSurfaceWidget->hide();
  223. debug_menu->addAction(graphicsSurfaceWidget->toggleViewAction());
  224. waitTreeWidget = new WaitTreeWidget(this);
  225. addDockWidget(Qt::LeftDockWidgetArea, waitTreeWidget);
  226. waitTreeWidget->hide();
  227. debug_menu->addAction(waitTreeWidget->toggleViewAction());
  228. connect(this, &GMainWindow::EmulationStarting, waitTreeWidget,
  229. &WaitTreeWidget::OnEmulationStarting);
  230. connect(this, &GMainWindow::EmulationStopping, waitTreeWidget,
  231. &WaitTreeWidget::OnEmulationStopping);
  232. }
  233. void GMainWindow::InitializeRecentFileMenuActions() {
  234. for (int i = 0; i < max_recent_files_item; ++i) {
  235. actions_recent_files[i] = new QAction(this);
  236. actions_recent_files[i]->setVisible(false);
  237. connect(actions_recent_files[i], &QAction::triggered, this, &GMainWindow::OnMenuRecentFile);
  238. ui.menu_recent_files->addAction(actions_recent_files[i]);
  239. }
  240. ui.menu_recent_files->addSeparator();
  241. QAction* action_clear_recent_files = new QAction(this);
  242. action_clear_recent_files->setText(tr("Clear Recent Files"));
  243. connect(action_clear_recent_files, &QAction::triggered, this, [this] {
  244. UISettings::values.recent_files.clear();
  245. UpdateRecentFiles();
  246. });
  247. ui.menu_recent_files->addAction(action_clear_recent_files);
  248. UpdateRecentFiles();
  249. }
  250. void GMainWindow::InitializeHotkeys() {
  251. hotkey_registry.RegisterHotkey("Main Window", "Load File", QKeySequence::Open);
  252. hotkey_registry.RegisterHotkey("Main Window", "Start Emulation");
  253. hotkey_registry.RegisterHotkey("Main Window", "Continue/Pause", QKeySequence(Qt::Key_F4));
  254. hotkey_registry.RegisterHotkey("Main Window", "Restart", QKeySequence(Qt::Key_F5));
  255. hotkey_registry.RegisterHotkey("Main Window", "Fullscreen", QKeySequence::FullScreen);
  256. hotkey_registry.RegisterHotkey("Main Window", "Exit Fullscreen", QKeySequence(Qt::Key_Escape),
  257. Qt::ApplicationShortcut);
  258. hotkey_registry.RegisterHotkey("Main Window", "Toggle Speed Limit", QKeySequence("CTRL+Z"),
  259. Qt::ApplicationShortcut);
  260. hotkey_registry.RegisterHotkey("Main Window", "Increase Speed Limit", QKeySequence("+"),
  261. Qt::ApplicationShortcut);
  262. hotkey_registry.RegisterHotkey("Main Window", "Decrease Speed Limit", QKeySequence("-"),
  263. Qt::ApplicationShortcut);
  264. hotkey_registry.LoadHotkeys();
  265. connect(hotkey_registry.GetHotkey("Main Window", "Load File", this), &QShortcut::activated,
  266. this, &GMainWindow::OnMenuLoadFile);
  267. connect(hotkey_registry.GetHotkey("Main Window", "Start Emulation", this),
  268. &QShortcut::activated, this, &GMainWindow::OnStartGame);
  269. connect(hotkey_registry.GetHotkey("Main Window", "Continue/Pause", this), &QShortcut::activated,
  270. this, [&] {
  271. if (emulation_running) {
  272. if (emu_thread->IsRunning()) {
  273. OnPauseGame();
  274. } else {
  275. OnStartGame();
  276. }
  277. }
  278. });
  279. connect(hotkey_registry.GetHotkey("Main Window", "Restart", this), &QShortcut::activated, this,
  280. [this] {
  281. if (!Core::System::GetInstance().IsPoweredOn())
  282. return;
  283. BootGame(QString(game_path));
  284. });
  285. connect(hotkey_registry.GetHotkey("Main Window", "Fullscreen", render_window),
  286. &QShortcut::activated, ui.action_Fullscreen, &QAction::trigger);
  287. connect(hotkey_registry.GetHotkey("Main Window", "Fullscreen", render_window),
  288. &QShortcut::activatedAmbiguously, ui.action_Fullscreen, &QAction::trigger);
  289. connect(hotkey_registry.GetHotkey("Main Window", "Exit Fullscreen", this),
  290. &QShortcut::activated, this, [&] {
  291. if (emulation_running) {
  292. ui.action_Fullscreen->setChecked(false);
  293. ToggleFullscreen();
  294. }
  295. });
  296. connect(hotkey_registry.GetHotkey("Main Window", "Toggle Speed Limit", this),
  297. &QShortcut::activated, this, [&] {
  298. Settings::values.use_frame_limit = !Settings::values.use_frame_limit;
  299. UpdateStatusBar();
  300. });
  301. constexpr u16 SPEED_LIMIT_STEP = 5;
  302. connect(hotkey_registry.GetHotkey("Main Window", "Increase Speed Limit", this),
  303. &QShortcut::activated, this, [&] {
  304. if (Settings::values.frame_limit < 9999 - SPEED_LIMIT_STEP) {
  305. Settings::values.frame_limit += SPEED_LIMIT_STEP;
  306. UpdateStatusBar();
  307. }
  308. });
  309. connect(hotkey_registry.GetHotkey("Main Window", "Decrease Speed Limit", this),
  310. &QShortcut::activated, this, [&] {
  311. if (Settings::values.frame_limit > SPEED_LIMIT_STEP) {
  312. Settings::values.frame_limit -= SPEED_LIMIT_STEP;
  313. UpdateStatusBar();
  314. }
  315. });
  316. }
  317. void GMainWindow::SetDefaultUIGeometry() {
  318. // geometry: 55% of the window contents are in the upper screen half, 45% in the lower half
  319. const QRect screenRect = QApplication::desktop()->screenGeometry(this);
  320. const int w = screenRect.width() * 2 / 3;
  321. const int h = screenRect.height() / 2;
  322. const int x = (screenRect.x() + screenRect.width()) / 2 - w / 2;
  323. const int y = (screenRect.y() + screenRect.height()) / 2 - h * 55 / 100;
  324. setGeometry(x, y, w, h);
  325. }
  326. void GMainWindow::RestoreUIState() {
  327. restoreGeometry(UISettings::values.geometry);
  328. restoreState(UISettings::values.state);
  329. render_window->restoreGeometry(UISettings::values.renderwindow_geometry);
  330. #if MICROPROFILE_ENABLED
  331. microProfileDialog->restoreGeometry(UISettings::values.microprofile_geometry);
  332. microProfileDialog->setVisible(UISettings::values.microprofile_visible);
  333. #endif
  334. game_list->LoadInterfaceLayout();
  335. ui.action_Single_Window_Mode->setChecked(UISettings::values.single_window_mode);
  336. ToggleWindowMode();
  337. ui.action_Fullscreen->setChecked(UISettings::values.fullscreen);
  338. ui.action_Display_Dock_Widget_Headers->setChecked(UISettings::values.display_titlebar);
  339. OnDisplayTitleBars(ui.action_Display_Dock_Widget_Headers->isChecked());
  340. ui.action_Show_Filter_Bar->setChecked(UISettings::values.show_filter_bar);
  341. game_list->setFilterVisible(ui.action_Show_Filter_Bar->isChecked());
  342. ui.action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar);
  343. statusBar()->setVisible(ui.action_Show_Status_Bar->isChecked());
  344. Debugger::ToggleConsole();
  345. }
  346. void GMainWindow::ConnectWidgetEvents() {
  347. connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile);
  348. connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder);
  349. connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS);
  350. connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID);
  351. connect(game_list, &GameList::NavigateToGamedbEntryRequested, this,
  352. &GMainWindow::OnGameListNavigateToGamedbEntry);
  353. connect(this, &GMainWindow::EmulationStarting, render_window,
  354. &GRenderWindow::OnEmulationStarting);
  355. connect(this, &GMainWindow::EmulationStopping, render_window,
  356. &GRenderWindow::OnEmulationStopping);
  357. connect(&status_bar_update_timer, &QTimer::timeout, this, &GMainWindow::UpdateStatusBar);
  358. }
  359. void GMainWindow::ConnectMenuEvents() {
  360. // File
  361. connect(ui.action_Load_File, &QAction::triggered, this, &GMainWindow::OnMenuLoadFile);
  362. connect(ui.action_Load_Folder, &QAction::triggered, this, &GMainWindow::OnMenuLoadFolder);
  363. connect(ui.action_Install_File_NAND, &QAction::triggered, this,
  364. &GMainWindow::OnMenuInstallToNAND);
  365. connect(ui.action_Select_Game_List_Root, &QAction::triggered, this,
  366. &GMainWindow::OnMenuSelectGameListRoot);
  367. connect(ui.action_Select_NAND_Directory, &QAction::triggered, this,
  368. [this] { OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget::NAND); });
  369. connect(ui.action_Select_SDMC_Directory, &QAction::triggered, this,
  370. [this] { OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget::SDMC); });
  371. connect(ui.action_Exit, &QAction::triggered, this, &QMainWindow::close);
  372. connect(ui.action_Load_Amiibo, &QAction::triggered, this, &GMainWindow::OnLoadAmiibo);
  373. // Emulation
  374. connect(ui.action_Start, &QAction::triggered, this, &GMainWindow::OnStartGame);
  375. connect(ui.action_Pause, &QAction::triggered, this, &GMainWindow::OnPauseGame);
  376. connect(ui.action_Stop, &QAction::triggered, this, &GMainWindow::OnStopGame);
  377. connect(ui.action_Report_Compatibility, &QAction::triggered, this,
  378. &GMainWindow::OnMenuReportCompatibility);
  379. connect(ui.action_Restart, &QAction::triggered, this, [this] { BootGame(QString(game_path)); });
  380. connect(ui.action_Configure, &QAction::triggered, this, &GMainWindow::OnConfigure);
  381. // View
  382. connect(ui.action_Single_Window_Mode, &QAction::triggered, this,
  383. &GMainWindow::ToggleWindowMode);
  384. connect(ui.action_Display_Dock_Widget_Headers, &QAction::triggered, this,
  385. &GMainWindow::OnDisplayTitleBars);
  386. ui.action_Show_Filter_Bar->setShortcut(tr("CTRL+F"));
  387. connect(ui.action_Show_Filter_Bar, &QAction::triggered, this, &GMainWindow::OnToggleFilterBar);
  388. connect(ui.action_Show_Status_Bar, &QAction::triggered, statusBar(), &QStatusBar::setVisible);
  389. // Fullscreen
  390. ui.action_Fullscreen->setShortcut(
  391. hotkey_registry.GetHotkey("Main Window", "Fullscreen", this)->key());
  392. connect(ui.action_Fullscreen, &QAction::triggered, this, &GMainWindow::ToggleFullscreen);
  393. // Help
  394. connect(ui.action_Rederive, &QAction::triggered, this,
  395. std::bind(&GMainWindow::OnReinitializeKeys, this, ReinitializeKeyBehavior::Warning));
  396. connect(ui.action_About, &QAction::triggered, this, &GMainWindow::OnAbout);
  397. }
  398. void GMainWindow::OnDisplayTitleBars(bool show) {
  399. QList<QDockWidget*> widgets = findChildren<QDockWidget*>();
  400. if (show) {
  401. for (QDockWidget* widget : widgets) {
  402. QWidget* old = widget->titleBarWidget();
  403. widget->setTitleBarWidget(nullptr);
  404. if (old != nullptr)
  405. delete old;
  406. }
  407. } else {
  408. for (QDockWidget* widget : widgets) {
  409. QWidget* old = widget->titleBarWidget();
  410. widget->setTitleBarWidget(new QWidget());
  411. if (old != nullptr)
  412. delete old;
  413. }
  414. }
  415. }
  416. QStringList GMainWindow::GetUnsupportedGLExtensions() {
  417. QStringList unsupported_ext;
  418. if (!GLAD_GL_ARB_program_interface_query)
  419. unsupported_ext.append("ARB_program_interface_query");
  420. if (!GLAD_GL_ARB_separate_shader_objects)
  421. unsupported_ext.append("ARB_separate_shader_objects");
  422. if (!GLAD_GL_ARB_vertex_attrib_binding)
  423. unsupported_ext.append("ARB_vertex_attrib_binding");
  424. if (!GLAD_GL_ARB_vertex_type_10f_11f_11f_rev)
  425. unsupported_ext.append("ARB_vertex_type_10f_11f_11f_rev");
  426. if (!GLAD_GL_ARB_texture_mirror_clamp_to_edge)
  427. unsupported_ext.append("ARB_texture_mirror_clamp_to_edge");
  428. if (!GLAD_GL_ARB_base_instance)
  429. unsupported_ext.append("ARB_base_instance");
  430. if (!GLAD_GL_ARB_texture_storage)
  431. unsupported_ext.append("ARB_texture_storage");
  432. if (!GLAD_GL_ARB_multi_bind)
  433. unsupported_ext.append("ARB_multi_bind");
  434. if (!GLAD_GL_ARB_copy_image)
  435. unsupported_ext.append("ARB_copy_image");
  436. // Extensions required to support some texture formats.
  437. if (!GLAD_GL_EXT_texture_compression_s3tc)
  438. unsupported_ext.append("EXT_texture_compression_s3tc");
  439. if (!GLAD_GL_ARB_texture_compression_rgtc)
  440. unsupported_ext.append("ARB_texture_compression_rgtc");
  441. if (!GLAD_GL_ARB_texture_compression_bptc)
  442. unsupported_ext.append("ARB_texture_compression_bptc");
  443. if (!GLAD_GL_ARB_depth_buffer_float)
  444. unsupported_ext.append("ARB_depth_buffer_float");
  445. for (const QString& ext : unsupported_ext)
  446. LOG_CRITICAL(Frontend, "Unsupported GL extension: {}", ext.toStdString());
  447. return unsupported_ext;
  448. }
  449. bool GMainWindow::LoadROM(const QString& filename) {
  450. // Shutdown previous session if the emu thread is still active...
  451. if (emu_thread != nullptr)
  452. ShutdownGame();
  453. render_window->InitRenderTarget();
  454. render_window->MakeCurrent();
  455. if (!gladLoadGL()) {
  456. QMessageBox::critical(this, tr("Error while initializing OpenGL 3.3 Core!"),
  457. tr("Your GPU may not support OpenGL 3.3, or you do not "
  458. "have the latest graphics driver."));
  459. return false;
  460. }
  461. QStringList unsupported_gl_extensions = GetUnsupportedGLExtensions();
  462. if (!unsupported_gl_extensions.empty()) {
  463. QMessageBox::critical(this, tr("Error while initializing OpenGL Core!"),
  464. tr("Your GPU may not support one or more required OpenGL"
  465. "extensions. Please ensure you have the latest graphics "
  466. "driver.<br><br>Unsupported extensions:<br>") +
  467. unsupported_gl_extensions.join("<br>"));
  468. return false;
  469. }
  470. Core::System& system{Core::System::GetInstance()};
  471. system.SetFilesystem(vfs);
  472. system.SetGPUDebugContext(debug_context);
  473. const Core::System::ResultStatus result{system.Load(*render_window, filename.toStdString())};
  474. const auto drd_callout =
  475. (UISettings::values.callout_flags & static_cast<u32>(CalloutFlag::DRDDeprecation)) == 0;
  476. if (result == Core::System::ResultStatus::Success &&
  477. system.GetAppLoader().GetFileType() == Loader::FileType::DeconstructedRomDirectory &&
  478. drd_callout) {
  479. UISettings::values.callout_flags |= static_cast<u32>(CalloutFlag::DRDDeprecation);
  480. QMessageBox::warning(
  481. this, tr("Warning Outdated Game Format"),
  482. tr("You are using the deconstructed ROM directory format for this game, which is an "
  483. "outdated format that has been superseded by others such as NCA, NAX, XCI, or "
  484. "NSP. Deconstructed ROM directories lack icons, metadata, and update "
  485. "support.<br><br>For an explanation of the various Switch formats yuzu supports, <a "
  486. "href='https://yuzu-emu.org/wiki/overview-of-switch-game-formats'>check out our "
  487. "wiki</a>. This message will not be shown again."));
  488. }
  489. render_window->DoneCurrent();
  490. if (result != Core::System::ResultStatus::Success) {
  491. switch (result) {
  492. case Core::System::ResultStatus::ErrorGetLoader:
  493. LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filename.toStdString());
  494. QMessageBox::critical(this, tr("Error while loading ROM!"),
  495. tr("The ROM format is not supported."));
  496. break;
  497. case Core::System::ResultStatus::ErrorSystemMode:
  498. LOG_CRITICAL(Frontend, "Failed to load ROM!");
  499. QMessageBox::critical(this, tr("Error while loading ROM!"),
  500. tr("Could not determine the system mode."));
  501. break;
  502. case Core::System::ResultStatus::ErrorVideoCore:
  503. QMessageBox::critical(
  504. this, tr("An error occurred initializing the video core."),
  505. tr("yuzu has encountered an error while running the video core, please see the "
  506. "log for more details."
  507. "For more information on accessing the log, please see the following page: "
  508. "<a href='https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>How "
  509. "to "
  510. "Upload the Log File</a>."
  511. "Ensure that you have the latest graphics drivers for your GPU."));
  512. break;
  513. default:
  514. if (static_cast<u32>(result) >
  515. static_cast<u32>(Core::System::ResultStatus::ErrorLoader)) {
  516. LOG_CRITICAL(Frontend, "Failed to load ROM!");
  517. const u16 loader_id = static_cast<u16>(Core::System::ResultStatus::ErrorLoader);
  518. const u16 error_id = static_cast<u16>(result) - loader_id;
  519. QMessageBox::critical(
  520. this, tr("Error while loading ROM!"),
  521. QString::fromStdString(fmt::format(
  522. "While attempting to load the ROM requested, an error occured. Please "
  523. "refer to the yuzu wiki for more information or the yuzu discord for "
  524. "additional help.\n\nError Code: {:04X}-{:04X}\nError Description: {}",
  525. loader_id, error_id, static_cast<Loader::ResultStatus>(error_id))));
  526. } else {
  527. QMessageBox::critical(
  528. this, tr("Error while loading ROM!"),
  529. tr("An unknown error occurred. Please see the log for more details."));
  530. }
  531. break;
  532. }
  533. return false;
  534. }
  535. game_path = filename;
  536. Core::Telemetry().AddField(Telemetry::FieldType::App, "Frontend", "Qt");
  537. return true;
  538. }
  539. void GMainWindow::BootGame(const QString& filename) {
  540. LOG_INFO(Frontend, "yuzu starting...");
  541. StoreRecentFile(filename); // Put the filename on top of the list
  542. if (!LoadROM(filename))
  543. return;
  544. // Create and start the emulation thread
  545. emu_thread = std::make_unique<EmuThread>(render_window);
  546. emit EmulationStarting(emu_thread.get());
  547. render_window->moveContext();
  548. emu_thread->start();
  549. connect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame);
  550. // BlockingQueuedConnection is important here, it makes sure we've finished refreshing our views
  551. // before the CPU continues
  552. connect(emu_thread.get(), &EmuThread::DebugModeEntered, waitTreeWidget,
  553. &WaitTreeWidget::OnDebugModeEntered, Qt::BlockingQueuedConnection);
  554. connect(emu_thread.get(), &EmuThread::DebugModeLeft, waitTreeWidget,
  555. &WaitTreeWidget::OnDebugModeLeft, Qt::BlockingQueuedConnection);
  556. // Update the GUI
  557. if (ui.action_Single_Window_Mode->isChecked()) {
  558. game_list->hide();
  559. }
  560. status_bar_update_timer.start(2000);
  561. std::string title_name;
  562. const auto res = Core::System::GetInstance().GetGameName(title_name);
  563. if (res != Loader::ResultStatus::Success) {
  564. const u64 title_id = Core::System::GetInstance().CurrentProcess()->GetTitleID();
  565. const auto [nacp, icon_file] = FileSys::PatchManager(title_id).GetControlMetadata();
  566. if (nacp != nullptr)
  567. title_name = nacp->GetApplicationName();
  568. if (title_name.empty())
  569. title_name = FileUtil::GetFilename(filename.toStdString());
  570. }
  571. setWindowTitle(QString("yuzu %1| %4 | %2-%3")
  572. .arg(Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc,
  573. QString::fromStdString(title_name)));
  574. render_window->show();
  575. render_window->setFocus();
  576. emulation_running = true;
  577. if (ui.action_Fullscreen->isChecked()) {
  578. ShowFullscreen();
  579. }
  580. OnStartGame();
  581. }
  582. void GMainWindow::ShutdownGame() {
  583. discord_rpc->Pause();
  584. emu_thread->RequestStop();
  585. emit EmulationStopping();
  586. // Wait for emulation thread to complete and delete it
  587. emu_thread->wait();
  588. emu_thread = nullptr;
  589. discord_rpc->Update();
  590. // The emulation is stopped, so closing the window or not does not matter anymore
  591. disconnect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame);
  592. // Update the GUI
  593. ui.action_Start->setEnabled(false);
  594. ui.action_Start->setText(tr("Start"));
  595. ui.action_Pause->setEnabled(false);
  596. ui.action_Stop->setEnabled(false);
  597. ui.action_Restart->setEnabled(false);
  598. ui.action_Report_Compatibility->setEnabled(false);
  599. ui.action_Load_Amiibo->setEnabled(false);
  600. render_window->hide();
  601. game_list->show();
  602. game_list->setFilterFocus();
  603. setWindowTitle(QString("yuzu %1| %2-%3")
  604. .arg(Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc));
  605. // Disable status bar updates
  606. status_bar_update_timer.stop();
  607. message_label->setVisible(false);
  608. emu_speed_label->setVisible(false);
  609. game_fps_label->setVisible(false);
  610. emu_frametime_label->setVisible(false);
  611. emulation_running = false;
  612. game_path.clear();
  613. }
  614. void GMainWindow::StoreRecentFile(const QString& filename) {
  615. UISettings::values.recent_files.prepend(filename);
  616. UISettings::values.recent_files.removeDuplicates();
  617. while (UISettings::values.recent_files.size() > max_recent_files_item) {
  618. UISettings::values.recent_files.removeLast();
  619. }
  620. UpdateRecentFiles();
  621. }
  622. void GMainWindow::UpdateRecentFiles() {
  623. const int num_recent_files =
  624. std::min(UISettings::values.recent_files.size(), max_recent_files_item);
  625. for (int i = 0; i < num_recent_files; i++) {
  626. const QString text = QString("&%1. %2").arg(i + 1).arg(
  627. QFileInfo(UISettings::values.recent_files[i]).fileName());
  628. actions_recent_files[i]->setText(text);
  629. actions_recent_files[i]->setData(UISettings::values.recent_files[i]);
  630. actions_recent_files[i]->setToolTip(UISettings::values.recent_files[i]);
  631. actions_recent_files[i]->setVisible(true);
  632. }
  633. for (int j = num_recent_files; j < max_recent_files_item; ++j) {
  634. actions_recent_files[j]->setVisible(false);
  635. }
  636. // Enable the recent files menu if the list isn't empty
  637. ui.menu_recent_files->setEnabled(num_recent_files != 0);
  638. }
  639. void GMainWindow::OnGameListLoadFile(QString game_path) {
  640. BootGame(game_path);
  641. }
  642. void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target) {
  643. std::string path;
  644. std::string open_target;
  645. switch (target) {
  646. case GameListOpenTarget::SaveData: {
  647. open_target = "Save Data";
  648. const std::string nand_dir = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir);
  649. ASSERT(program_id != 0);
  650. Service::Account::ProfileManager manager{};
  651. const auto user_ids = manager.GetAllUsers();
  652. QStringList list;
  653. for (const auto& user_id : user_ids) {
  654. if (user_id == Service::Account::UUID{})
  655. continue;
  656. Service::Account::ProfileBase base;
  657. if (!manager.GetProfileBase(user_id, base))
  658. continue;
  659. list.push_back(QString::fromStdString(Common::StringFromFixedZeroTerminatedBuffer(
  660. reinterpret_cast<const char*>(base.username.data()), base.username.size())));
  661. }
  662. bool ok = false;
  663. const auto index_string =
  664. QInputDialog::getItem(this, tr("Select User"),
  665. tr("Please select the user's save data you would like to open."),
  666. list, Settings::values.current_user, false, &ok);
  667. if (!ok)
  668. return;
  669. const auto index = list.indexOf(index_string);
  670. ASSERT(index != -1 && index < 8);
  671. const auto user_id = manager.GetUser(index);
  672. ASSERT(user_id);
  673. path = nand_dir + FileSys::SaveDataFactory::GetFullPath(FileSys::SaveDataSpaceId::NandUser,
  674. FileSys::SaveDataType::SaveData,
  675. program_id, user_id->uuid, 0);
  676. if (!FileUtil::Exists(path)) {
  677. FileUtil::CreateFullPath(path);
  678. FileUtil::CreateDir(path);
  679. }
  680. break;
  681. }
  682. case GameListOpenTarget::ModData: {
  683. open_target = "Mod Data";
  684. const auto load_dir = FileUtil::GetUserPath(FileUtil::UserPath::LoadDir);
  685. path = fmt::format("{}{:016X}", load_dir, program_id);
  686. break;
  687. }
  688. default:
  689. UNIMPLEMENTED();
  690. }
  691. const QString qpath = QString::fromStdString(path);
  692. const QDir dir(qpath);
  693. if (!dir.exists()) {
  694. QMessageBox::warning(this,
  695. tr("Error Opening %1 Folder").arg(QString::fromStdString(open_target)),
  696. tr("Folder does not exist!"));
  697. return;
  698. }
  699. LOG_INFO(Frontend, "Opening {} path for program_id={:016x}", open_target, program_id);
  700. QDesktopServices::openUrl(QUrl::fromLocalFile(qpath));
  701. }
  702. static std::size_t CalculateRomFSEntrySize(const FileSys::VirtualDir& dir, bool full) {
  703. std::size_t out = 0;
  704. for (const auto& subdir : dir->GetSubdirectories()) {
  705. out += 1 + CalculateRomFSEntrySize(subdir, full);
  706. }
  707. return out + (full ? dir->GetFiles().size() : 0);
  708. }
  709. static bool RomFSRawCopy(QProgressDialog& dialog, const FileSys::VirtualDir& src,
  710. const FileSys::VirtualDir& dest, std::size_t block_size, bool full) {
  711. if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable())
  712. return false;
  713. if (dialog.wasCanceled())
  714. return false;
  715. if (full) {
  716. for (const auto& file : src->GetFiles()) {
  717. const auto out = VfsDirectoryCreateFileWrapper(dest, file->GetName());
  718. if (!FileSys::VfsRawCopy(file, out, block_size))
  719. return false;
  720. dialog.setValue(dialog.value() + 1);
  721. if (dialog.wasCanceled())
  722. return false;
  723. }
  724. }
  725. for (const auto& dir : src->GetSubdirectories()) {
  726. const auto out = dest->CreateSubdirectory(dir->GetName());
  727. if (!RomFSRawCopy(dialog, dir, out, block_size, full))
  728. return false;
  729. dialog.setValue(dialog.value() + 1);
  730. if (dialog.wasCanceled())
  731. return false;
  732. }
  733. return true;
  734. }
  735. void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_path) {
  736. const auto failed = [this] {
  737. QMessageBox::warning(this, tr("RomFS Extraction Failed!"),
  738. tr("There was an error copying the RomFS files or the user "
  739. "cancelled the operation."));
  740. };
  741. const auto loader = Loader::GetLoader(vfs->OpenFile(game_path, FileSys::Mode::Read));
  742. if (loader == nullptr) {
  743. failed();
  744. return;
  745. }
  746. FileSys::VirtualFile file;
  747. if (loader->ReadRomFS(file) != Loader::ResultStatus::Success) {
  748. failed();
  749. return;
  750. }
  751. const auto installed = Service::FileSystem::GetUnionContents();
  752. auto romfs_title_id = SelectRomFSDumpTarget(*installed, program_id);
  753. if (!romfs_title_id) {
  754. failed();
  755. return;
  756. }
  757. const auto path = fmt::format(
  758. "{}{:016X}/romfs", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), *romfs_title_id);
  759. FileSys::VirtualFile romfs;
  760. if (*romfs_title_id == program_id) {
  761. romfs = file;
  762. } else {
  763. romfs = installed->GetEntry(*romfs_title_id, FileSys::ContentRecordType::Data)->GetRomFS();
  764. }
  765. const auto extracted = FileSys::ExtractRomFS(romfs, FileSys::RomFSExtractionType::Full);
  766. if (extracted == nullptr) {
  767. failed();
  768. return;
  769. }
  770. const auto out = VfsFilesystemCreateDirectoryWrapper(vfs, path, FileSys::Mode::ReadWrite);
  771. if (out == nullptr) {
  772. failed();
  773. vfs->DeleteDirectory(path);
  774. return;
  775. }
  776. bool ok;
  777. const auto res = QInputDialog::getItem(
  778. this, tr("Select RomFS Dump Mode"),
  779. tr("Please select the how you would like the RomFS dumped.<br>Full will copy all of the "
  780. "files into the new directory while <br>skeleton will only create the directory "
  781. "structure."),
  782. {"Full", "Skeleton"}, 0, false, &ok);
  783. if (!ok) {
  784. failed();
  785. vfs->DeleteDirectory(path);
  786. return;
  787. }
  788. const auto full = res == "Full";
  789. const auto entry_size = CalculateRomFSEntrySize(extracted, full);
  790. QProgressDialog progress(tr("Extracting RomFS..."), tr("Cancel"), 0, entry_size, this);
  791. progress.setWindowModality(Qt::WindowModal);
  792. progress.setMinimumDuration(100);
  793. if (RomFSRawCopy(progress, extracted, out, 0x400000, full)) {
  794. progress.close();
  795. QMessageBox::information(this, tr("RomFS Extraction Succeeded!"),
  796. tr("The operation completed successfully."));
  797. QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(path)));
  798. } else {
  799. progress.close();
  800. failed();
  801. vfs->DeleteDirectory(path);
  802. }
  803. }
  804. void GMainWindow::OnGameListCopyTID(u64 program_id) {
  805. QClipboard* clipboard = QGuiApplication::clipboard();
  806. clipboard->setText(QString::fromStdString(fmt::format("{:016X}", program_id)));
  807. }
  808. void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id,
  809. const CompatibilityList& compatibility_list) {
  810. const auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
  811. QString directory;
  812. if (it != compatibility_list.end())
  813. directory = it->second.second;
  814. QDesktopServices::openUrl(QUrl("https://yuzu-emu.org/game/" + directory));
  815. }
  816. void GMainWindow::OnMenuLoadFile() {
  817. const QString extensions =
  818. QString("*.").append(GameList::supported_file_extensions.join(" *.")).append(" main");
  819. const QString file_filter = tr("Switch Executable (%1);;All Files (*.*)",
  820. "%1 is an identifier for the Switch executable file extensions.")
  821. .arg(extensions);
  822. const QString filename = QFileDialog::getOpenFileName(
  823. this, tr("Load File"), UISettings::values.roms_path, file_filter);
  824. if (filename.isEmpty()) {
  825. return;
  826. }
  827. UISettings::values.roms_path = QFileInfo(filename).path();
  828. BootGame(filename);
  829. }
  830. void GMainWindow::OnMenuLoadFolder() {
  831. const QString dir_path =
  832. QFileDialog::getExistingDirectory(this, tr("Open Extracted ROM Directory"));
  833. if (dir_path.isNull()) {
  834. return;
  835. }
  836. const QDir dir{dir_path};
  837. const QStringList matching_main = dir.entryList(QStringList("main"), QDir::Files);
  838. if (matching_main.size() == 1) {
  839. BootGame(dir.path() + DIR_SEP + matching_main[0]);
  840. } else {
  841. QMessageBox::warning(this, tr("Invalid Directory Selected"),
  842. tr("The directory you have selected does not contain a 'main' file."));
  843. }
  844. }
  845. void GMainWindow::OnMenuInstallToNAND() {
  846. const QString file_filter =
  847. tr("Installable Switch File (*.nca *.nsp *.xci);;Nintendo Content Archive "
  848. "(*.nca);;Nintendo Submissions Package (*.nsp);;NX Cartridge "
  849. "Image (*.xci)");
  850. QString filename = QFileDialog::getOpenFileName(this, tr("Install File"),
  851. UISettings::values.roms_path, file_filter);
  852. if (filename.isEmpty()) {
  853. return;
  854. }
  855. const auto qt_raw_copy = [this](const FileSys::VirtualFile& src,
  856. const FileSys::VirtualFile& dest, std::size_t block_size) {
  857. if (src == nullptr || dest == nullptr)
  858. return false;
  859. if (!dest->Resize(src->GetSize()))
  860. return false;
  861. std::array<u8, 0x1000> buffer{};
  862. const int progress_maximum = static_cast<int>(src->GetSize() / buffer.size());
  863. QProgressDialog progress(
  864. tr("Installing file \"%1\"...").arg(QString::fromStdString(src->GetName())),
  865. tr("Cancel"), 0, progress_maximum, this);
  866. progress.setWindowModality(Qt::WindowModal);
  867. for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
  868. if (progress.wasCanceled()) {
  869. dest->Resize(0);
  870. return false;
  871. }
  872. const int progress_value = static_cast<int>(i / buffer.size());
  873. progress.setValue(progress_value);
  874. const auto read = src->Read(buffer.data(), buffer.size(), i);
  875. dest->Write(buffer.data(), read, i);
  876. }
  877. return true;
  878. };
  879. const auto success = [this]() {
  880. QMessageBox::information(this, tr("Successfully Installed"),
  881. tr("The file was successfully installed."));
  882. game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan);
  883. };
  884. const auto failed = [this]() {
  885. QMessageBox::warning(
  886. this, tr("Failed to Install"),
  887. tr("There was an error while attempting to install the provided file. It "
  888. "could have an incorrect format or be missing metadata. Please "
  889. "double-check your file and try again."));
  890. };
  891. const auto overwrite = [this]() {
  892. return QMessageBox::question(this, tr("Failed to Install"),
  893. tr("The file you are attempting to install already exists "
  894. "in the cache. Would you like to overwrite it?")) ==
  895. QMessageBox::Yes;
  896. };
  897. if (filename.endsWith("xci", Qt::CaseInsensitive) ||
  898. filename.endsWith("nsp", Qt::CaseInsensitive)) {
  899. std::shared_ptr<FileSys::NSP> nsp;
  900. if (filename.endsWith("nsp", Qt::CaseInsensitive)) {
  901. nsp = std::make_shared<FileSys::NSP>(
  902. vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
  903. if (nsp->IsExtractedType())
  904. failed();
  905. } else {
  906. const auto xci = std::make_shared<FileSys::XCI>(
  907. vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
  908. nsp = xci->GetSecurePartitionNSP();
  909. }
  910. if (nsp->GetStatus() != Loader::ResultStatus::Success) {
  911. failed();
  912. return;
  913. }
  914. const auto res =
  915. Service::FileSystem::GetUserNANDContents()->InstallEntry(nsp, false, qt_raw_copy);
  916. if (res == FileSys::InstallResult::Success) {
  917. success();
  918. } else {
  919. if (res == FileSys::InstallResult::ErrorAlreadyExists) {
  920. if (overwrite()) {
  921. const auto res2 = Service::FileSystem::GetUserNANDContents()->InstallEntry(
  922. nsp, true, qt_raw_copy);
  923. if (res2 == FileSys::InstallResult::Success) {
  924. success();
  925. } else {
  926. failed();
  927. }
  928. }
  929. } else {
  930. failed();
  931. }
  932. }
  933. } else {
  934. const auto nca = std::make_shared<FileSys::NCA>(
  935. vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
  936. const auto id = nca->GetStatus();
  937. // Game updates necessary are missing base RomFS
  938. if (id != Loader::ResultStatus::Success &&
  939. id != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) {
  940. failed();
  941. return;
  942. }
  943. const QStringList tt_options{tr("System Application"),
  944. tr("System Archive"),
  945. tr("System Application Update"),
  946. tr("Firmware Package (Type A)"),
  947. tr("Firmware Package (Type B)"),
  948. tr("Game"),
  949. tr("Game Update"),
  950. tr("Game DLC"),
  951. tr("Delta Title")};
  952. bool ok;
  953. const auto item = QInputDialog::getItem(
  954. this, tr("Select NCA Install Type..."),
  955. tr("Please select the type of title you would like to install this NCA as:\n(In "
  956. "most instances, the default 'Game' is fine.)"),
  957. tt_options, 5, false, &ok);
  958. auto index = tt_options.indexOf(item);
  959. if (!ok || index == -1) {
  960. QMessageBox::warning(this, tr("Failed to Install"),
  961. tr("The title type you selected for the NCA is invalid."));
  962. return;
  963. }
  964. // If index is equal to or past Game, add the jump in TitleType.
  965. if (index >= 5) {
  966. index += static_cast<size_t>(FileSys::TitleType::Application) -
  967. static_cast<size_t>(FileSys::TitleType::FirmwarePackageB);
  968. }
  969. FileSys::InstallResult res;
  970. if (index >= static_cast<size_t>(FileSys::TitleType::Application)) {
  971. res = Service::FileSystem::GetUserNANDContents()->InstallEntry(
  972. nca, static_cast<FileSys::TitleType>(index), false, qt_raw_copy);
  973. } else {
  974. res = Service::FileSystem::GetSystemNANDContents()->InstallEntry(
  975. nca, static_cast<FileSys::TitleType>(index), false, qt_raw_copy);
  976. }
  977. if (res == FileSys::InstallResult::Success) {
  978. success();
  979. } else if (res == FileSys::InstallResult::ErrorAlreadyExists) {
  980. if (overwrite()) {
  981. const auto res2 = Service::FileSystem::GetUserNANDContents()->InstallEntry(
  982. nca, static_cast<FileSys::TitleType>(index), true, qt_raw_copy);
  983. if (res2 == FileSys::InstallResult::Success) {
  984. success();
  985. } else {
  986. failed();
  987. }
  988. }
  989. } else {
  990. failed();
  991. }
  992. }
  993. }
  994. void GMainWindow::OnMenuSelectGameListRoot() {
  995. QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory"));
  996. if (!dir_path.isEmpty()) {
  997. UISettings::values.gamedir = dir_path;
  998. game_list->PopulateAsync(dir_path, UISettings::values.gamedir_deepscan);
  999. }
  1000. }
  1001. void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target) {
  1002. const auto res = QMessageBox::information(
  1003. this, tr("Changing Emulated Directory"),
  1004. tr("You are about to change the emulated %1 directory of the system. Please note "
  1005. "that this does not also move the contents of the previous directory to the "
  1006. "new one and you will have to do that yourself.")
  1007. .arg(target == EmulatedDirectoryTarget::SDMC ? tr("SD card") : tr("NAND")),
  1008. QMessageBox::StandardButtons{QMessageBox::Ok, QMessageBox::Cancel});
  1009. if (res == QMessageBox::Cancel)
  1010. return;
  1011. QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory"));
  1012. if (!dir_path.isEmpty()) {
  1013. FileUtil::GetUserPath(target == EmulatedDirectoryTarget::SDMC ? FileUtil::UserPath::SDMCDir
  1014. : FileUtil::UserPath::NANDDir,
  1015. dir_path.toStdString());
  1016. Service::FileSystem::CreateFactories(*vfs);
  1017. game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan);
  1018. }
  1019. }
  1020. void GMainWindow::OnMenuRecentFile() {
  1021. QAction* action = qobject_cast<QAction*>(sender());
  1022. assert(action);
  1023. const QString filename = action->data().toString();
  1024. if (QFileInfo::exists(filename)) {
  1025. BootGame(filename);
  1026. } else {
  1027. // Display an error message and remove the file from the list.
  1028. QMessageBox::information(this, tr("File not found"),
  1029. tr("File \"%1\" not found").arg(filename));
  1030. UISettings::values.recent_files.removeOne(filename);
  1031. UpdateRecentFiles();
  1032. }
  1033. }
  1034. void GMainWindow::OnStartGame() {
  1035. emu_thread->SetRunning(true);
  1036. qRegisterMetaType<Core::System::ResultStatus>("Core::System::ResultStatus");
  1037. qRegisterMetaType<std::string>("std::string");
  1038. connect(emu_thread.get(), &EmuThread::ErrorThrown, this, &GMainWindow::OnCoreError);
  1039. ui.action_Start->setEnabled(false);
  1040. ui.action_Start->setText(tr("Continue"));
  1041. ui.action_Pause->setEnabled(true);
  1042. ui.action_Stop->setEnabled(true);
  1043. ui.action_Restart->setEnabled(true);
  1044. ui.action_Report_Compatibility->setEnabled(true);
  1045. discord_rpc->Update();
  1046. ui.action_Load_Amiibo->setEnabled(true);
  1047. }
  1048. void GMainWindow::OnPauseGame() {
  1049. emu_thread->SetRunning(false);
  1050. ui.action_Start->setEnabled(true);
  1051. ui.action_Pause->setEnabled(false);
  1052. ui.action_Stop->setEnabled(true);
  1053. }
  1054. void GMainWindow::OnStopGame() {
  1055. ShutdownGame();
  1056. }
  1057. void GMainWindow::OnMenuReportCompatibility() {
  1058. if (!Settings::values.yuzu_token.empty() && !Settings::values.yuzu_username.empty()) {
  1059. CompatDB compatdb{this};
  1060. compatdb.exec();
  1061. } else {
  1062. QMessageBox::critical(
  1063. this, tr("Missing yuzu Account"),
  1064. tr("In order to submit a game compatibility test case, you must link your yuzu "
  1065. "account.<br><br/>To link your yuzu account, go to Emulation &gt; Configuration "
  1066. "&gt; "
  1067. "Web."));
  1068. }
  1069. }
  1070. void GMainWindow::ToggleFullscreen() {
  1071. if (!emulation_running) {
  1072. return;
  1073. }
  1074. if (ui.action_Fullscreen->isChecked()) {
  1075. ShowFullscreen();
  1076. } else {
  1077. HideFullscreen();
  1078. }
  1079. }
  1080. void GMainWindow::ShowFullscreen() {
  1081. if (ui.action_Single_Window_Mode->isChecked()) {
  1082. UISettings::values.geometry = saveGeometry();
  1083. ui.menubar->hide();
  1084. statusBar()->hide();
  1085. showFullScreen();
  1086. } else {
  1087. UISettings::values.renderwindow_geometry = render_window->saveGeometry();
  1088. render_window->showFullScreen();
  1089. }
  1090. }
  1091. void GMainWindow::HideFullscreen() {
  1092. if (ui.action_Single_Window_Mode->isChecked()) {
  1093. statusBar()->setVisible(ui.action_Show_Status_Bar->isChecked());
  1094. ui.menubar->show();
  1095. showNormal();
  1096. restoreGeometry(UISettings::values.geometry);
  1097. } else {
  1098. render_window->showNormal();
  1099. render_window->restoreGeometry(UISettings::values.renderwindow_geometry);
  1100. }
  1101. }
  1102. void GMainWindow::ToggleWindowMode() {
  1103. if (ui.action_Single_Window_Mode->isChecked()) {
  1104. // Render in the main window...
  1105. render_window->BackupGeometry();
  1106. ui.horizontalLayout->addWidget(render_window);
  1107. render_window->setFocusPolicy(Qt::ClickFocus);
  1108. if (emulation_running) {
  1109. render_window->setVisible(true);
  1110. render_window->setFocus();
  1111. game_list->hide();
  1112. }
  1113. } else {
  1114. // Render in a separate window...
  1115. ui.horizontalLayout->removeWidget(render_window);
  1116. render_window->setParent(nullptr);
  1117. render_window->setFocusPolicy(Qt::NoFocus);
  1118. if (emulation_running) {
  1119. render_window->setVisible(true);
  1120. render_window->RestoreGeometry();
  1121. game_list->show();
  1122. }
  1123. }
  1124. }
  1125. void GMainWindow::OnConfigure() {
  1126. ConfigureDialog configureDialog(this, hotkey_registry);
  1127. auto old_theme = UISettings::values.theme;
  1128. const bool old_discord_presence = UISettings::values.enable_discord_presence;
  1129. auto result = configureDialog.exec();
  1130. if (result == QDialog::Accepted) {
  1131. configureDialog.applyConfiguration();
  1132. if (UISettings::values.theme != old_theme)
  1133. UpdateUITheme();
  1134. if (UISettings::values.enable_discord_presence != old_discord_presence)
  1135. SetDiscordEnabled(UISettings::values.enable_discord_presence);
  1136. game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan);
  1137. config->Save();
  1138. }
  1139. }
  1140. void GMainWindow::OnLoadAmiibo() {
  1141. const QString extensions{"*.bin"};
  1142. const QString file_filter = tr("Amiibo File (%1);; All Files (*.*)").arg(extensions);
  1143. const QString filename = QFileDialog::getOpenFileName(this, tr("Load Amiibo"), "", file_filter);
  1144. if (filename.isEmpty()) {
  1145. return;
  1146. }
  1147. Core::System& system{Core::System::GetInstance()};
  1148. Service::SM::ServiceManager& sm = system.ServiceManager();
  1149. auto nfc = sm.GetService<Service::NFP::Module::Interface>("nfp:user");
  1150. if (nfc == nullptr) {
  1151. return;
  1152. }
  1153. QFile nfc_file{filename};
  1154. if (!nfc_file.open(QIODevice::ReadOnly)) {
  1155. QMessageBox::warning(this, tr("Error opening Amiibo data file"),
  1156. tr("Unable to open Amiibo file \"%1\" for reading.").arg(filename));
  1157. return;
  1158. }
  1159. const u64 nfc_file_size = nfc_file.size();
  1160. std::vector<u8> buffer(nfc_file_size);
  1161. const u64 read_size = nfc_file.read(reinterpret_cast<char*>(buffer.data()), nfc_file_size);
  1162. if (nfc_file_size != read_size) {
  1163. QMessageBox::warning(this, tr("Error reading Amiibo data file"),
  1164. tr("Unable to fully read Amiibo data. Expected to read %1 bytes, but "
  1165. "was only able to read %2 bytes.")
  1166. .arg(nfc_file_size)
  1167. .arg(read_size));
  1168. return;
  1169. }
  1170. if (!nfc->LoadAmiibo(buffer)) {
  1171. QMessageBox::warning(this, tr("Error loading Amiibo data"),
  1172. tr("Unable to load Amiibo data."));
  1173. }
  1174. }
  1175. void GMainWindow::OnAbout() {
  1176. AboutDialog aboutDialog(this);
  1177. aboutDialog.exec();
  1178. }
  1179. void GMainWindow::OnToggleFilterBar() {
  1180. game_list->setFilterVisible(ui.action_Show_Filter_Bar->isChecked());
  1181. if (ui.action_Show_Filter_Bar->isChecked()) {
  1182. game_list->setFilterFocus();
  1183. } else {
  1184. game_list->clearFilter();
  1185. }
  1186. }
  1187. void GMainWindow::UpdateStatusBar() {
  1188. if (emu_thread == nullptr) {
  1189. status_bar_update_timer.stop();
  1190. return;
  1191. }
  1192. auto results = Core::System::GetInstance().GetAndResetPerfStats();
  1193. if (Settings::values.use_frame_limit) {
  1194. emu_speed_label->setText(tr("Speed: %1% / %2%")
  1195. .arg(results.emulation_speed * 100.0, 0, 'f', 0)
  1196. .arg(Settings::values.frame_limit));
  1197. } else {
  1198. emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0));
  1199. }
  1200. game_fps_label->setText(tr("Game: %1 FPS").arg(results.game_fps, 0, 'f', 0));
  1201. emu_frametime_label->setText(tr("Frame: %1 ms").arg(results.frametime * 1000.0, 0, 'f', 2));
  1202. emu_speed_label->setVisible(true);
  1203. game_fps_label->setVisible(true);
  1204. emu_frametime_label->setVisible(true);
  1205. }
  1206. void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string details) {
  1207. QMessageBox::StandardButton answer;
  1208. QString status_message;
  1209. const QString common_message =
  1210. tr("The game you are trying to load requires additional files from your Switch to be "
  1211. "dumped "
  1212. "before playing.<br/><br/>For more information on dumping these files, please see the "
  1213. "following wiki page: <a "
  1214. "href='https://yuzu-emu.org/wiki/"
  1215. "dumping-system-archives-and-the-shared-fonts-from-a-switch-console/'>Dumping System "
  1216. "Archives and the Shared Fonts from a Switch Console</a>.<br/><br/>Would you like to "
  1217. "quit "
  1218. "back to the game list? Continuing emulation may result in crashes, corrupted save "
  1219. "data, or other bugs.");
  1220. switch (result) {
  1221. case Core::System::ResultStatus::ErrorSystemFiles: {
  1222. QString message = "yuzu was unable to locate a Switch system archive";
  1223. if (!details.empty()) {
  1224. message.append(tr(": %1. ").arg(details.c_str()));
  1225. } else {
  1226. message.append(". ");
  1227. }
  1228. message.append(common_message);
  1229. answer = QMessageBox::question(this, tr("System Archive Not Found"), message,
  1230. QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
  1231. status_message = "System Archive Missing";
  1232. break;
  1233. }
  1234. case Core::System::ResultStatus::ErrorSharedFont: {
  1235. QString message = tr("yuzu was unable to locate the Switch shared fonts. ");
  1236. message.append(common_message);
  1237. answer = QMessageBox::question(this, tr("Shared Fonts Not Found"), message,
  1238. QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
  1239. status_message = "Shared Font Missing";
  1240. break;
  1241. }
  1242. default:
  1243. answer = QMessageBox::question(
  1244. this, tr("Fatal Error"),
  1245. tr("yuzu has encountered a fatal error, please see the log for more details. "
  1246. "For more information on accessing the log, please see the following page: "
  1247. "<a href='https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>How "
  1248. "to "
  1249. "Upload the Log File</a>.<br/><br/>Would you like to quit back to the game "
  1250. "list? "
  1251. "Continuing emulation may result in crashes, corrupted save data, or other "
  1252. "bugs."),
  1253. QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
  1254. status_message = "Fatal Error encountered";
  1255. break;
  1256. }
  1257. if (answer == QMessageBox::Yes) {
  1258. if (emu_thread) {
  1259. ShutdownGame();
  1260. }
  1261. } else {
  1262. // Only show the message if the game is still running.
  1263. if (emu_thread) {
  1264. emu_thread->SetRunning(true);
  1265. message_label->setText(status_message);
  1266. message_label->setVisible(true);
  1267. }
  1268. }
  1269. }
  1270. void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) {
  1271. if (behavior == ReinitializeKeyBehavior::Warning) {
  1272. const auto res = QMessageBox::information(
  1273. this, tr("Confirm Key Rederivation"),
  1274. tr("You are about to force rederive all of your keys. \nIf you do not know what this "
  1275. "means or what you are doing, \nthis is a potentially destructive action. \nPlease "
  1276. "make "
  1277. "sure this is what you want \nand optionally make backups.\n\nThis will delete your "
  1278. "autogenerated key files and re-run the key derivation module."),
  1279. QMessageBox::StandardButtons{QMessageBox::Ok, QMessageBox::Cancel});
  1280. if (res == QMessageBox::Cancel)
  1281. return;
  1282. FileUtil::Delete(FileUtil::GetUserPath(FileUtil::UserPath::KeysDir) +
  1283. "prod.keys_autogenerated");
  1284. FileUtil::Delete(FileUtil::GetUserPath(FileUtil::UserPath::KeysDir) +
  1285. "console.keys_autogenerated");
  1286. FileUtil::Delete(FileUtil::GetUserPath(FileUtil::UserPath::KeysDir) +
  1287. "title.keys_autogenerated");
  1288. }
  1289. Core::Crypto::KeyManager keys{};
  1290. if (keys.BaseDeriveNecessary()) {
  1291. Core::Crypto::PartitionDataManager pdm{vfs->OpenDirectory(
  1292. FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir), FileSys::Mode::Read)};
  1293. const auto function = [this, &keys, &pdm] {
  1294. keys.PopulateFromPartitionData(pdm);
  1295. Service::FileSystem::CreateFactories(*vfs);
  1296. keys.DeriveETicket(pdm);
  1297. };
  1298. QString errors;
  1299. if (!pdm.HasFuses())
  1300. errors += tr("- Missing fuses - Cannot derive SBK\n");
  1301. if (!pdm.HasBoot0())
  1302. errors += tr("- Missing BOOT0 - Cannot derive master keys\n");
  1303. if (!pdm.HasPackage2())
  1304. errors += tr("- Missing BCPKG2-1-Normal-Main - Cannot derive general keys\n");
  1305. if (!pdm.HasProdInfo())
  1306. errors += tr("- Missing PRODINFO - Cannot derive title keys\n");
  1307. if (!errors.isEmpty()) {
  1308. QMessageBox::warning(
  1309. this, tr("Warning Missing Derivation Components"),
  1310. tr("The following are missing from your configuration that may hinder key "
  1311. "derivation. It will be attempted but may not complete.<br><br>") +
  1312. errors +
  1313. tr("<br><br>You can get all of these and dump all of your games easily by "
  1314. "following <a href='https://yuzu-emu.org/help/quickstart/'>the "
  1315. "quickstart guide</a>. Alternatively, you can use another method of dumping "
  1316. "to obtain all of your keys."));
  1317. }
  1318. QProgressDialog prog;
  1319. prog.setRange(0, 0);
  1320. prog.setLabelText(tr("Deriving keys...\nThis may take up to a minute depending \non your "
  1321. "system's performance."));
  1322. prog.setWindowTitle(tr("Deriving Keys"));
  1323. prog.show();
  1324. auto future = QtConcurrent::run(function);
  1325. while (!future.isFinished()) {
  1326. QCoreApplication::processEvents();
  1327. }
  1328. prog.close();
  1329. }
  1330. Service::FileSystem::CreateFactories(*vfs);
  1331. if (behavior == ReinitializeKeyBehavior::Warning) {
  1332. game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan);
  1333. }
  1334. }
  1335. std::optional<u64> GMainWindow::SelectRomFSDumpTarget(
  1336. const FileSys::RegisteredCacheUnion& installed, u64 program_id) {
  1337. const auto dlc_entries =
  1338. installed.ListEntriesFilter(FileSys::TitleType::AOC, FileSys::ContentRecordType::Data);
  1339. std::vector<FileSys::RegisteredCacheEntry> dlc_match;
  1340. dlc_match.reserve(dlc_entries.size());
  1341. std::copy_if(dlc_entries.begin(), dlc_entries.end(), std::back_inserter(dlc_match),
  1342. [&program_id, &installed](const FileSys::RegisteredCacheEntry& entry) {
  1343. return (entry.title_id & DLC_BASE_TITLE_ID_MASK) == program_id &&
  1344. installed.GetEntry(entry)->GetStatus() == Loader::ResultStatus::Success;
  1345. });
  1346. std::vector<u64> romfs_tids;
  1347. romfs_tids.push_back(program_id);
  1348. for (const auto& entry : dlc_match)
  1349. romfs_tids.push_back(entry.title_id);
  1350. if (romfs_tids.size() > 1) {
  1351. QStringList list{"Base"};
  1352. for (std::size_t i = 1; i < romfs_tids.size(); ++i)
  1353. list.push_back(QStringLiteral("DLC %1").arg(romfs_tids[i] & 0x7FF));
  1354. bool ok;
  1355. const auto res = QInputDialog::getItem(
  1356. this, tr("Select RomFS Dump Target"),
  1357. tr("Please select which RomFS you would like to dump."), list, 0, false, &ok);
  1358. if (!ok) {
  1359. return {};
  1360. }
  1361. return romfs_tids[list.indexOf(res)];
  1362. }
  1363. return program_id;
  1364. }
  1365. bool GMainWindow::ConfirmClose() {
  1366. if (emu_thread == nullptr || !UISettings::values.confirm_before_closing)
  1367. return true;
  1368. QMessageBox::StandardButton answer =
  1369. QMessageBox::question(this, tr("yuzu"), tr("Are you sure you want to close yuzu?"),
  1370. QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
  1371. return answer != QMessageBox::No;
  1372. }
  1373. void GMainWindow::closeEvent(QCloseEvent* event) {
  1374. if (!ConfirmClose()) {
  1375. event->ignore();
  1376. return;
  1377. }
  1378. if (ui.action_Fullscreen->isChecked()) {
  1379. UISettings::values.geometry = saveGeometry();
  1380. UISettings::values.renderwindow_geometry = render_window->saveGeometry();
  1381. }
  1382. UISettings::values.state = saveState();
  1383. #if MICROPROFILE_ENABLED
  1384. UISettings::values.microprofile_geometry = microProfileDialog->saveGeometry();
  1385. UISettings::values.microprofile_visible = microProfileDialog->isVisible();
  1386. #endif
  1387. UISettings::values.single_window_mode = ui.action_Single_Window_Mode->isChecked();
  1388. UISettings::values.fullscreen = ui.action_Fullscreen->isChecked();
  1389. UISettings::values.display_titlebar = ui.action_Display_Dock_Widget_Headers->isChecked();
  1390. UISettings::values.show_filter_bar = ui.action_Show_Filter_Bar->isChecked();
  1391. UISettings::values.show_status_bar = ui.action_Show_Status_Bar->isChecked();
  1392. UISettings::values.first_start = false;
  1393. game_list->SaveInterfaceLayout();
  1394. hotkey_registry.SaveHotkeys();
  1395. // Shutdown session if the emu thread is active...
  1396. if (emu_thread != nullptr)
  1397. ShutdownGame();
  1398. render_window->close();
  1399. QWidget::closeEvent(event);
  1400. }
  1401. static bool IsSingleFileDropEvent(QDropEvent* event) {
  1402. const QMimeData* mimeData = event->mimeData();
  1403. return mimeData->hasUrls() && mimeData->urls().length() == 1;
  1404. }
  1405. void GMainWindow::dropEvent(QDropEvent* event) {
  1406. if (IsSingleFileDropEvent(event) && ConfirmChangeGame()) {
  1407. const QMimeData* mimeData = event->mimeData();
  1408. QString filename = mimeData->urls().at(0).toLocalFile();
  1409. BootGame(filename);
  1410. }
  1411. }
  1412. void GMainWindow::dragEnterEvent(QDragEnterEvent* event) {
  1413. if (IsSingleFileDropEvent(event)) {
  1414. event->acceptProposedAction();
  1415. }
  1416. }
  1417. void GMainWindow::dragMoveEvent(QDragMoveEvent* event) {
  1418. event->acceptProposedAction();
  1419. }
  1420. bool GMainWindow::ConfirmChangeGame() {
  1421. if (emu_thread == nullptr)
  1422. return true;
  1423. auto answer = QMessageBox::question(
  1424. this, tr("yuzu"),
  1425. tr("Are you sure you want to stop the emulation? Any unsaved progress will be lost."),
  1426. QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
  1427. return answer != QMessageBox::No;
  1428. }
  1429. void GMainWindow::filterBarSetChecked(bool state) {
  1430. ui.action_Show_Filter_Bar->setChecked(state);
  1431. emit(OnToggleFilterBar());
  1432. }
  1433. void GMainWindow::UpdateUITheme() {
  1434. QStringList theme_paths(default_theme_paths);
  1435. if (UISettings::values.theme != UISettings::themes[0].second &&
  1436. !UISettings::values.theme.isEmpty()) {
  1437. const QString theme_uri(":" + UISettings::values.theme + "/style.qss");
  1438. QFile f(theme_uri);
  1439. if (f.open(QFile::ReadOnly | QFile::Text)) {
  1440. QTextStream ts(&f);
  1441. qApp->setStyleSheet(ts.readAll());
  1442. GMainWindow::setStyleSheet(ts.readAll());
  1443. } else {
  1444. LOG_ERROR(Frontend, "Unable to set style, stylesheet file not found");
  1445. }
  1446. theme_paths.append(QStringList{":/icons/default", ":/icons/" + UISettings::values.theme});
  1447. QIcon::setThemeName(":/icons/" + UISettings::values.theme);
  1448. } else {
  1449. qApp->setStyleSheet("");
  1450. GMainWindow::setStyleSheet("");
  1451. theme_paths.append(QStringList{":/icons/default"});
  1452. QIcon::setThemeName(":/icons/default");
  1453. }
  1454. QIcon::setThemeSearchPaths(theme_paths);
  1455. emit UpdateThemedIcons();
  1456. }
  1457. void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) {
  1458. #ifdef USE_DISCORD_PRESENCE
  1459. if (state) {
  1460. discord_rpc = std::make_unique<DiscordRPC::DiscordImpl>();
  1461. } else {
  1462. discord_rpc = std::make_unique<DiscordRPC::NullImpl>();
  1463. }
  1464. #else
  1465. discord_rpc = std::make_unique<DiscordRPC::NullImpl>();
  1466. #endif
  1467. discord_rpc->Update();
  1468. }
  1469. #ifdef main
  1470. #undef main
  1471. #endif
  1472. int main(int argc, char* argv[]) {
  1473. Common::DetachedTasks detached_tasks;
  1474. MicroProfileOnThreadCreate("Frontend");
  1475. SCOPE_EXIT({ MicroProfileShutdown(); });
  1476. // Init settings params
  1477. QCoreApplication::setOrganizationName("yuzu team");
  1478. QCoreApplication::setApplicationName("yuzu");
  1479. QApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity);
  1480. QApplication app(argc, argv);
  1481. // Qt changes the locale and causes issues in float conversion using std::to_string() when
  1482. // generating shaders
  1483. setlocale(LC_ALL, "C");
  1484. GMainWindow main_window;
  1485. // After settings have been loaded by GMainWindow, apply the filter
  1486. main_window.show();
  1487. int result = app.exec();
  1488. detached_tasks.WaitForAllTasks();
  1489. return result;
  1490. }