main.cpp 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009
  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. #include <glad/glad.h>
  9. #define QT_NO_OPENGL
  10. #include <QDesktopWidget>
  11. #include <QFileDialog>
  12. #include <QMessageBox>
  13. #include <QtGui>
  14. #include <QtWidgets>
  15. #include "common/common_paths.h"
  16. #include "common/logging/backend.h"
  17. #include "common/logging/filter.h"
  18. #include "common/logging/log.h"
  19. #include "common/logging/text_formatter.h"
  20. #include "common/microprofile.h"
  21. #include "common/scm_rev.h"
  22. #include "common/scope_exit.h"
  23. #include "common/string_util.h"
  24. #include "core/core.h"
  25. #include "core/crypto/key_manager.h"
  26. #include "core/gdbstub/gdbstub.h"
  27. #include "core/loader/loader.h"
  28. #include "core/settings.h"
  29. #include "video_core/debug_utils/debug_utils.h"
  30. #include "yuzu/about_dialog.h"
  31. #include "yuzu/bootmanager.h"
  32. #include "yuzu/configuration/config.h"
  33. #include "yuzu/configuration/configure_dialog.h"
  34. #include "yuzu/debugger/console.h"
  35. #include "yuzu/debugger/graphics/graphics_breakpoints.h"
  36. #include "yuzu/debugger/graphics/graphics_surface.h"
  37. #include "yuzu/debugger/profiler.h"
  38. #include "yuzu/debugger/wait_tree.h"
  39. #include "yuzu/game_list.h"
  40. #include "yuzu/hotkeys.h"
  41. #include "yuzu/main.h"
  42. #include "yuzu/ui_settings.h"
  43. #ifdef QT_STATICPLUGIN
  44. Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin);
  45. #endif
  46. #ifdef _WIN32
  47. extern "C" {
  48. // tells Nvidia and AMD drivers to use the dedicated GPU by default on laptops with switchable
  49. // graphics
  50. __declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001;
  51. __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
  52. }
  53. #endif
  54. /**
  55. * "Callouts" are one-time instructional messages shown to the user. In the config settings, there
  56. * is a bitfield "callout_flags" options, used to track if a message has already been shown to the
  57. * user. This is 32-bits - if we have more than 32 callouts, we should retire and recyle old ones.
  58. */
  59. enum class CalloutFlag : uint32_t {
  60. Telemetry = 0x1,
  61. };
  62. static void ShowCalloutMessage(const QString& message, CalloutFlag flag) {
  63. if (UISettings::values.callout_flags & static_cast<uint32_t>(flag)) {
  64. return;
  65. }
  66. UISettings::values.callout_flags |= static_cast<uint32_t>(flag);
  67. QMessageBox msg;
  68. msg.setText(message);
  69. msg.setStandardButtons(QMessageBox::Ok);
  70. msg.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
  71. msg.setStyleSheet("QLabel{min-width: 900px;}");
  72. msg.exec();
  73. }
  74. void GMainWindow::ShowCallouts() {}
  75. const int GMainWindow::max_recent_files_item;
  76. GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) {
  77. debug_context = Tegra::DebugContext::Construct();
  78. setAcceptDrops(true);
  79. ui.setupUi(this);
  80. statusBar()->hide();
  81. default_theme_paths = QIcon::themeSearchPaths();
  82. UpdateUITheme();
  83. InitializeWidgets();
  84. InitializeDebugWidgets();
  85. InitializeRecentFileMenuActions();
  86. InitializeHotkeys();
  87. SetDefaultUIGeometry();
  88. RestoreUIState();
  89. ConnectMenuEvents();
  90. ConnectWidgetEvents();
  91. LOG_INFO(Frontend, "yuzu Version: {} | {}-{}", Common::g_build_name, Common::g_scm_branch,
  92. Common::g_scm_desc);
  93. setWindowTitle(QString("yuzu %1| %2-%3")
  94. .arg(Common::g_build_name, Common::g_scm_branch, Common::g_scm_desc));
  95. show();
  96. game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan);
  97. // Show one-time "callout" messages to the user
  98. ShowCallouts();
  99. QStringList args = QApplication::arguments();
  100. if (args.length() >= 2) {
  101. BootGame(args[1]);
  102. }
  103. }
  104. GMainWindow::~GMainWindow() {
  105. // will get automatically deleted otherwise
  106. if (render_window->parent() == nullptr)
  107. delete render_window;
  108. }
  109. void GMainWindow::InitializeWidgets() {
  110. render_window = new GRenderWindow(this, emu_thread.get());
  111. render_window->hide();
  112. game_list = new GameList(this);
  113. ui.horizontalLayout->addWidget(game_list);
  114. // Create status bar
  115. message_label = new QLabel();
  116. // Configured separately for left alignment
  117. message_label->setVisible(false);
  118. message_label->setFrameStyle(QFrame::NoFrame);
  119. message_label->setContentsMargins(4, 0, 4, 0);
  120. message_label->setAlignment(Qt::AlignLeft);
  121. statusBar()->addPermanentWidget(message_label, 1);
  122. emu_speed_label = new QLabel();
  123. emu_speed_label->setToolTip(
  124. tr("Current emulation speed. Values higher or lower than 100% "
  125. "indicate emulation is running faster or slower than a Switch."));
  126. game_fps_label = new QLabel();
  127. game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. "
  128. "This will vary from game to game and scene to scene."));
  129. emu_frametime_label = new QLabel();
  130. emu_frametime_label->setToolTip(
  131. tr("Time taken to emulate a Switch frame, not counting framelimiting or v-sync. For "
  132. "full-speed emulation this should be at most 16.67 ms."));
  133. for (auto& label : {emu_speed_label, game_fps_label, emu_frametime_label}) {
  134. label->setVisible(false);
  135. label->setFrameStyle(QFrame::NoFrame);
  136. label->setContentsMargins(4, 0, 4, 0);
  137. statusBar()->addPermanentWidget(label, 0);
  138. }
  139. statusBar()->setVisible(true);
  140. setStyleSheet("QStatusBar::item{border: none;}");
  141. }
  142. void GMainWindow::InitializeDebugWidgets() {
  143. QMenu* debug_menu = ui.menu_View_Debugging;
  144. #if MICROPROFILE_ENABLED
  145. microProfileDialog = new MicroProfileDialog(this);
  146. microProfileDialog->hide();
  147. debug_menu->addAction(microProfileDialog->toggleViewAction());
  148. #endif
  149. graphicsBreakpointsWidget = new GraphicsBreakPointsWidget(debug_context, this);
  150. addDockWidget(Qt::RightDockWidgetArea, graphicsBreakpointsWidget);
  151. graphicsBreakpointsWidget->hide();
  152. debug_menu->addAction(graphicsBreakpointsWidget->toggleViewAction());
  153. graphicsSurfaceWidget = new GraphicsSurfaceWidget(debug_context, this);
  154. addDockWidget(Qt::RightDockWidgetArea, graphicsSurfaceWidget);
  155. graphicsSurfaceWidget->hide();
  156. debug_menu->addAction(graphicsSurfaceWidget->toggleViewAction());
  157. waitTreeWidget = new WaitTreeWidget(this);
  158. addDockWidget(Qt::LeftDockWidgetArea, waitTreeWidget);
  159. waitTreeWidget->hide();
  160. debug_menu->addAction(waitTreeWidget->toggleViewAction());
  161. connect(this, &GMainWindow::EmulationStarting, waitTreeWidget,
  162. &WaitTreeWidget::OnEmulationStarting);
  163. connect(this, &GMainWindow::EmulationStopping, waitTreeWidget,
  164. &WaitTreeWidget::OnEmulationStopping);
  165. }
  166. void GMainWindow::InitializeRecentFileMenuActions() {
  167. for (int i = 0; i < max_recent_files_item; ++i) {
  168. actions_recent_files[i] = new QAction(this);
  169. actions_recent_files[i]->setVisible(false);
  170. connect(actions_recent_files[i], &QAction::triggered, this, &GMainWindow::OnMenuRecentFile);
  171. ui.menu_recent_files->addAction(actions_recent_files[i]);
  172. }
  173. UpdateRecentFiles();
  174. }
  175. void GMainWindow::InitializeHotkeys() {
  176. RegisterHotkey("Main Window", "Load File", QKeySequence::Open);
  177. RegisterHotkey("Main Window", "Start Emulation");
  178. RegisterHotkey("Main Window", "Continue/Pause", QKeySequence(Qt::Key_F4));
  179. RegisterHotkey("Main Window", "Fullscreen", QKeySequence::FullScreen);
  180. RegisterHotkey("Main Window", "Exit Fullscreen", QKeySequence(Qt::Key_Escape),
  181. Qt::ApplicationShortcut);
  182. RegisterHotkey("Main Window", "Toggle Speed Limit", QKeySequence("CTRL+Z"),
  183. Qt::ApplicationShortcut);
  184. LoadHotkeys();
  185. connect(GetHotkey("Main Window", "Load File", this), &QShortcut::activated, this,
  186. &GMainWindow::OnMenuLoadFile);
  187. connect(GetHotkey("Main Window", "Start Emulation", this), &QShortcut::activated, this,
  188. &GMainWindow::OnStartGame);
  189. connect(GetHotkey("Main Window", "Continue/Pause", this), &QShortcut::activated, this, [&] {
  190. if (emulation_running) {
  191. if (emu_thread->IsRunning()) {
  192. OnPauseGame();
  193. } else {
  194. OnStartGame();
  195. }
  196. }
  197. });
  198. connect(GetHotkey("Main Window", "Fullscreen", render_window), &QShortcut::activated,
  199. ui.action_Fullscreen, &QAction::trigger);
  200. connect(GetHotkey("Main Window", "Fullscreen", render_window), &QShortcut::activatedAmbiguously,
  201. ui.action_Fullscreen, &QAction::trigger);
  202. connect(GetHotkey("Main Window", "Exit Fullscreen", this), &QShortcut::activated, this, [&] {
  203. if (emulation_running) {
  204. ui.action_Fullscreen->setChecked(false);
  205. ToggleFullscreen();
  206. }
  207. });
  208. connect(GetHotkey("Main Window", "Toggle Speed Limit", this), &QShortcut::activated, this, [&] {
  209. Settings::values.toggle_framelimit = !Settings::values.toggle_framelimit;
  210. UpdateStatusBar();
  211. });
  212. }
  213. void GMainWindow::SetDefaultUIGeometry() {
  214. // geometry: 55% of the window contents are in the upper screen half, 45% in the lower half
  215. const QRect screenRect = QApplication::desktop()->screenGeometry(this);
  216. const int w = screenRect.width() * 2 / 3;
  217. const int h = screenRect.height() / 2;
  218. const int x = (screenRect.x() + screenRect.width()) / 2 - w / 2;
  219. const int y = (screenRect.y() + screenRect.height()) / 2 - h * 55 / 100;
  220. setGeometry(x, y, w, h);
  221. }
  222. void GMainWindow::RestoreUIState() {
  223. restoreGeometry(UISettings::values.geometry);
  224. restoreState(UISettings::values.state);
  225. render_window->restoreGeometry(UISettings::values.renderwindow_geometry);
  226. #if MICROPROFILE_ENABLED
  227. microProfileDialog->restoreGeometry(UISettings::values.microprofile_geometry);
  228. microProfileDialog->setVisible(UISettings::values.microprofile_visible);
  229. #endif
  230. game_list->LoadInterfaceLayout();
  231. ui.action_Single_Window_Mode->setChecked(UISettings::values.single_window_mode);
  232. ToggleWindowMode();
  233. ui.action_Fullscreen->setChecked(UISettings::values.fullscreen);
  234. ui.action_Display_Dock_Widget_Headers->setChecked(UISettings::values.display_titlebar);
  235. OnDisplayTitleBars(ui.action_Display_Dock_Widget_Headers->isChecked());
  236. ui.action_Show_Filter_Bar->setChecked(UISettings::values.show_filter_bar);
  237. game_list->setFilterVisible(ui.action_Show_Filter_Bar->isChecked());
  238. ui.action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar);
  239. statusBar()->setVisible(ui.action_Show_Status_Bar->isChecked());
  240. Debugger::ToggleConsole();
  241. }
  242. void GMainWindow::ConnectWidgetEvents() {
  243. connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile);
  244. connect(game_list, &GameList::OpenSaveFolderRequested, this,
  245. &GMainWindow::OnGameListOpenSaveFolder);
  246. connect(this, &GMainWindow::EmulationStarting, render_window,
  247. &GRenderWindow::OnEmulationStarting);
  248. connect(this, &GMainWindow::EmulationStopping, render_window,
  249. &GRenderWindow::OnEmulationStopping);
  250. connect(&status_bar_update_timer, &QTimer::timeout, this, &GMainWindow::UpdateStatusBar);
  251. }
  252. void GMainWindow::ConnectMenuEvents() {
  253. // File
  254. connect(ui.action_Load_File, &QAction::triggered, this, &GMainWindow::OnMenuLoadFile);
  255. connect(ui.action_Load_Folder, &QAction::triggered, this, &GMainWindow::OnMenuLoadFolder);
  256. connect(ui.action_Select_Game_List_Root, &QAction::triggered, this,
  257. &GMainWindow::OnMenuSelectGameListRoot);
  258. connect(ui.action_Exit, &QAction::triggered, this, &QMainWindow::close);
  259. // Emulation
  260. connect(ui.action_Start, &QAction::triggered, this, &GMainWindow::OnStartGame);
  261. connect(ui.action_Pause, &QAction::triggered, this, &GMainWindow::OnPauseGame);
  262. connect(ui.action_Stop, &QAction::triggered, this, &GMainWindow::OnStopGame);
  263. connect(ui.action_Configure, &QAction::triggered, this, &GMainWindow::OnConfigure);
  264. // View
  265. connect(ui.action_Single_Window_Mode, &QAction::triggered, this,
  266. &GMainWindow::ToggleWindowMode);
  267. connect(ui.action_Display_Dock_Widget_Headers, &QAction::triggered, this,
  268. &GMainWindow::OnDisplayTitleBars);
  269. ui.action_Show_Filter_Bar->setShortcut(tr("CTRL+F"));
  270. connect(ui.action_Show_Filter_Bar, &QAction::triggered, this, &GMainWindow::OnToggleFilterBar);
  271. connect(ui.action_Show_Status_Bar, &QAction::triggered, statusBar(), &QStatusBar::setVisible);
  272. // Fullscreen
  273. ui.action_Fullscreen->setShortcut(GetHotkey("Main Window", "Fullscreen", this)->key());
  274. connect(ui.action_Fullscreen, &QAction::triggered, this, &GMainWindow::ToggleFullscreen);
  275. // Help
  276. connect(ui.action_About, &QAction::triggered, this, &GMainWindow::OnAbout);
  277. }
  278. void GMainWindow::OnDisplayTitleBars(bool show) {
  279. QList<QDockWidget*> widgets = findChildren<QDockWidget*>();
  280. if (show) {
  281. for (QDockWidget* widget : widgets) {
  282. QWidget* old = widget->titleBarWidget();
  283. widget->setTitleBarWidget(nullptr);
  284. if (old != nullptr)
  285. delete old;
  286. }
  287. } else {
  288. for (QDockWidget* widget : widgets) {
  289. QWidget* old = widget->titleBarWidget();
  290. widget->setTitleBarWidget(new QWidget());
  291. if (old != nullptr)
  292. delete old;
  293. }
  294. }
  295. }
  296. bool GMainWindow::SupportsRequiredGLExtensions() {
  297. QStringList unsupported_ext;
  298. if (!GLAD_GL_ARB_program_interface_query)
  299. unsupported_ext.append("ARB_program_interface_query");
  300. if (!GLAD_GL_ARB_separate_shader_objects)
  301. unsupported_ext.append("ARB_separate_shader_objects");
  302. if (!GLAD_GL_ARB_vertex_attrib_binding)
  303. unsupported_ext.append("ARB_vertex_attrib_binding");
  304. if (!GLAD_GL_ARB_vertex_type_10f_11f_11f_rev)
  305. unsupported_ext.append("ARB_vertex_type_10f_11f_11f_rev");
  306. // Extensions required to support some texture formats.
  307. if (!GLAD_GL_EXT_texture_compression_s3tc)
  308. unsupported_ext.append("EXT_texture_compression_s3tc");
  309. if (!GLAD_GL_ARB_texture_compression_rgtc)
  310. unsupported_ext.append("ARB_texture_compression_rgtc");
  311. if (!GLAD_GL_ARB_texture_compression_bptc)
  312. unsupported_ext.append("ARB_texture_compression_bptc");
  313. if (!GLAD_GL_ARB_depth_buffer_float)
  314. unsupported_ext.append("ARB_depth_buffer_float");
  315. for (const QString& ext : unsupported_ext)
  316. LOG_CRITICAL(Frontend, "Unsupported GL extension: {}", ext.toStdString());
  317. return unsupported_ext.empty();
  318. }
  319. bool GMainWindow::LoadROM(const QString& filename) {
  320. // Shutdown previous session if the emu thread is still active...
  321. if (emu_thread != nullptr)
  322. ShutdownGame();
  323. render_window->InitRenderTarget();
  324. render_window->MakeCurrent();
  325. if (!gladLoadGL()) {
  326. QMessageBox::critical(this, tr("Error while initializing OpenGL 3.3 Core!"),
  327. tr("Your GPU may not support OpenGL 3.3, or you do not "
  328. "have the latest graphics driver."));
  329. return false;
  330. }
  331. if (!SupportsRequiredGLExtensions()) {
  332. QMessageBox::critical(
  333. this, tr("Error while initializing OpenGL Core!"),
  334. tr("Your GPU may not support one or more required OpenGL extensions. Please "
  335. "ensure you have the latest graphics driver. See the log for more details."));
  336. return false;
  337. }
  338. Core::System& system{Core::System::GetInstance()};
  339. system.SetGPUDebugContext(debug_context);
  340. const Core::System::ResultStatus result{system.Load(*render_window, filename.toStdString())};
  341. render_window->DoneCurrent();
  342. if (result != Core::System::ResultStatus::Success) {
  343. switch (result) {
  344. case Core::System::ResultStatus::ErrorGetLoader:
  345. LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filename.toStdString());
  346. QMessageBox::critical(this, tr("Error while loading ROM!"),
  347. tr("The ROM format is not supported."));
  348. break;
  349. case Core::System::ResultStatus::ErrorUnsupportedArch:
  350. LOG_CRITICAL(Frontend, "Unsupported architecture detected!", filename.toStdString());
  351. QMessageBox::critical(this, tr("Error while loading ROM!"),
  352. tr("The ROM uses currently unusable 32-bit architecture"));
  353. break;
  354. case Core::System::ResultStatus::ErrorSystemMode:
  355. LOG_CRITICAL(Frontend, "Failed to load ROM!");
  356. QMessageBox::critical(this, tr("Error while loading ROM!"),
  357. tr("Could not determine the system mode."));
  358. break;
  359. case Core::System::ResultStatus::ErrorLoader_ErrorMissingKeys: {
  360. const auto reg_found = Core::Crypto::KeyManager::KeyFileExists(false);
  361. const auto title_found = Core::Crypto::KeyManager::KeyFileExists(true);
  362. std::string file_text;
  363. if (!reg_found && !title_found) {
  364. file_text = "A proper key file (prod.keys, dev.keys, or title.keys) could not be "
  365. "found. You will need to dump your keys from your switch to continue.";
  366. } else if (reg_found && title_found) {
  367. file_text =
  368. "Both key files were found in your config directory, but the correct key could"
  369. "not be found. You may be missing a titlekey or general key, depending on "
  370. "the game.";
  371. } else if (reg_found) {
  372. file_text =
  373. "The regular keys file (prod.keys/dev.keys) was found in your config, but the "
  374. "titlekeys file (title.keys) was not. You are either missing the correct "
  375. "titlekey or missing a general key required to decrypt the game.";
  376. } else {
  377. file_text = "The title keys file (title.keys) was found in your config, but "
  378. "the regular keys file (prod.keys/dev.keys) was not. Unfortunately, "
  379. "having the titlekey is not enough, you need additional general keys "
  380. "to properly decrypt the game. You should double-check to make sure "
  381. "your keys are correct.";
  382. }
  383. QMessageBox::critical(
  384. this, tr("Error while loading ROM!"),
  385. tr(("The game you are trying to load is encrypted and the required keys to load "
  386. "the game could not be found in your configuration. " +
  387. file_text + " Please refer to the yuzu wiki for help.")
  388. .c_str()));
  389. break;
  390. }
  391. case Core::System::ResultStatus::ErrorLoader_ErrorDecrypting: {
  392. QMessageBox::critical(
  393. this, tr("Error while loading ROM!"),
  394. tr("There was a general error while decrypting the game. This means that the keys "
  395. "necessary were found, but were either incorrect, the game itself was not a "
  396. "valid game or the game uses an unhandled cryptographic scheme. Please double "
  397. "check that you have the correct "
  398. "keys."));
  399. break;
  400. }
  401. case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat:
  402. QMessageBox::critical(this, tr("Error while loading ROM!"),
  403. tr("The ROM format is not supported."));
  404. break;
  405. case Core::System::ResultStatus::ErrorVideoCore:
  406. QMessageBox::critical(
  407. this, tr("An error occurred initializing the video core."),
  408. tr("yuzu has encountered an error while running the video core, please see the "
  409. "log for more details."
  410. "For more information on accessing the log, please see the following page: "
  411. "<a href='https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>How "
  412. "to "
  413. "Upload the Log File</a>."
  414. "Ensure that you have the latest graphics drivers for your GPU."));
  415. break;
  416. default:
  417. QMessageBox::critical(
  418. this, tr("Error while loading ROM!"),
  419. tr("An unknown error occurred. Please see the log for more details."));
  420. break;
  421. }
  422. return false;
  423. }
  424. Core::Telemetry().AddField(Telemetry::FieldType::App, "Frontend", "Qt");
  425. return true;
  426. }
  427. void GMainWindow::BootGame(const QString& filename) {
  428. LOG_INFO(Frontend, "yuzu starting...");
  429. StoreRecentFile(filename); // Put the filename on top of the list
  430. if (!LoadROM(filename))
  431. return;
  432. // Create and start the emulation thread
  433. emu_thread = std::make_unique<EmuThread>(render_window);
  434. emit EmulationStarting(emu_thread.get());
  435. render_window->moveContext();
  436. emu_thread->start();
  437. connect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame);
  438. // BlockingQueuedConnection is important here, it makes sure we've finished refreshing our views
  439. // before the CPU continues
  440. connect(emu_thread.get(), &EmuThread::DebugModeEntered, waitTreeWidget,
  441. &WaitTreeWidget::OnDebugModeEntered, Qt::BlockingQueuedConnection);
  442. connect(emu_thread.get(), &EmuThread::DebugModeLeft, waitTreeWidget,
  443. &WaitTreeWidget::OnDebugModeLeft, Qt::BlockingQueuedConnection);
  444. // Update the GUI
  445. if (ui.action_Single_Window_Mode->isChecked()) {
  446. game_list->hide();
  447. }
  448. status_bar_update_timer.start(2000);
  449. render_window->show();
  450. render_window->setFocus();
  451. emulation_running = true;
  452. if (ui.action_Fullscreen->isChecked()) {
  453. ShowFullscreen();
  454. }
  455. OnStartGame();
  456. }
  457. void GMainWindow::ShutdownGame() {
  458. emu_thread->RequestStop();
  459. emit EmulationStopping();
  460. // Wait for emulation thread to complete and delete it
  461. emu_thread->wait();
  462. emu_thread = nullptr;
  463. // The emulation is stopped, so closing the window or not does not matter anymore
  464. disconnect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame);
  465. // Update the GUI
  466. ui.action_Start->setEnabled(false);
  467. ui.action_Start->setText(tr("Start"));
  468. ui.action_Pause->setEnabled(false);
  469. ui.action_Stop->setEnabled(false);
  470. render_window->hide();
  471. game_list->show();
  472. game_list->setFilterFocus();
  473. // Disable status bar updates
  474. status_bar_update_timer.stop();
  475. message_label->setVisible(false);
  476. emu_speed_label->setVisible(false);
  477. game_fps_label->setVisible(false);
  478. emu_frametime_label->setVisible(false);
  479. emulation_running = false;
  480. }
  481. void GMainWindow::StoreRecentFile(const QString& filename) {
  482. UISettings::values.recent_files.prepend(filename);
  483. UISettings::values.recent_files.removeDuplicates();
  484. while (UISettings::values.recent_files.size() > max_recent_files_item) {
  485. UISettings::values.recent_files.removeLast();
  486. }
  487. UpdateRecentFiles();
  488. }
  489. void GMainWindow::UpdateRecentFiles() {
  490. const int num_recent_files =
  491. std::min(UISettings::values.recent_files.size(), max_recent_files_item);
  492. for (int i = 0; i < num_recent_files; i++) {
  493. const QString text = QString("&%1. %2").arg(i + 1).arg(
  494. QFileInfo(UISettings::values.recent_files[i]).fileName());
  495. actions_recent_files[i]->setText(text);
  496. actions_recent_files[i]->setData(UISettings::values.recent_files[i]);
  497. actions_recent_files[i]->setToolTip(UISettings::values.recent_files[i]);
  498. actions_recent_files[i]->setVisible(true);
  499. }
  500. for (int j = num_recent_files; j < max_recent_files_item; ++j) {
  501. actions_recent_files[j]->setVisible(false);
  502. }
  503. // Enable the recent files menu if the list isn't empty
  504. ui.menu_recent_files->setEnabled(num_recent_files != 0);
  505. }
  506. void GMainWindow::OnGameListLoadFile(QString game_path) {
  507. BootGame(game_path);
  508. }
  509. void GMainWindow::OnGameListOpenSaveFolder(u64 program_id) {
  510. UNIMPLEMENTED();
  511. }
  512. void GMainWindow::OnMenuLoadFile() {
  513. QString extensions;
  514. for (const auto& piece : game_list->supported_file_extensions)
  515. extensions += "*." + piece + " ";
  516. extensions += "main ";
  517. QString file_filter = tr("Switch Executable") + " (" + extensions + ")";
  518. file_filter += ";;" + tr("All Files (*.*)");
  519. QString filename = QFileDialog::getOpenFileName(this, tr("Load File"),
  520. UISettings::values.roms_path, file_filter);
  521. if (!filename.isEmpty()) {
  522. UISettings::values.roms_path = QFileInfo(filename).path();
  523. BootGame(filename);
  524. }
  525. }
  526. void GMainWindow::OnMenuLoadFolder() {
  527. const QString dir_path =
  528. QFileDialog::getExistingDirectory(this, tr("Open Extracted ROM Directory"));
  529. if (dir_path.isNull()) {
  530. return;
  531. }
  532. const QDir dir{dir_path};
  533. const QStringList matching_main = dir.entryList(QStringList("main"), QDir::Files);
  534. if (matching_main.size() == 1) {
  535. BootGame(dir.path() + DIR_SEP + matching_main[0]);
  536. } else {
  537. QMessageBox::warning(this, tr("Invalid Directory Selected"),
  538. tr("The directory you have selected does not contain a 'main' file."));
  539. }
  540. }
  541. void GMainWindow::OnMenuSelectGameListRoot() {
  542. QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory"));
  543. if (!dir_path.isEmpty()) {
  544. UISettings::values.gamedir = dir_path;
  545. game_list->PopulateAsync(dir_path, UISettings::values.gamedir_deepscan);
  546. }
  547. }
  548. void GMainWindow::OnMenuRecentFile() {
  549. QAction* action = qobject_cast<QAction*>(sender());
  550. assert(action);
  551. const QString filename = action->data().toString();
  552. if (QFileInfo::exists(filename)) {
  553. BootGame(filename);
  554. } else {
  555. // Display an error message and remove the file from the list.
  556. QMessageBox::information(this, tr("File not found"),
  557. tr("File \"%1\" not found").arg(filename));
  558. UISettings::values.recent_files.removeOne(filename);
  559. UpdateRecentFiles();
  560. }
  561. }
  562. void GMainWindow::OnStartGame() {
  563. emu_thread->SetRunning(true);
  564. qRegisterMetaType<Core::System::ResultStatus>("Core::System::ResultStatus");
  565. qRegisterMetaType<std::string>("std::string");
  566. connect(emu_thread.get(), &EmuThread::ErrorThrown, this, &GMainWindow::OnCoreError);
  567. ui.action_Start->setEnabled(false);
  568. ui.action_Start->setText(tr("Continue"));
  569. ui.action_Pause->setEnabled(true);
  570. ui.action_Stop->setEnabled(true);
  571. }
  572. void GMainWindow::OnPauseGame() {
  573. emu_thread->SetRunning(false);
  574. ui.action_Start->setEnabled(true);
  575. ui.action_Pause->setEnabled(false);
  576. ui.action_Stop->setEnabled(true);
  577. }
  578. void GMainWindow::OnStopGame() {
  579. ShutdownGame();
  580. }
  581. void GMainWindow::ToggleFullscreen() {
  582. if (!emulation_running) {
  583. return;
  584. }
  585. if (ui.action_Fullscreen->isChecked()) {
  586. ShowFullscreen();
  587. } else {
  588. HideFullscreen();
  589. }
  590. }
  591. void GMainWindow::ShowFullscreen() {
  592. if (ui.action_Single_Window_Mode->isChecked()) {
  593. UISettings::values.geometry = saveGeometry();
  594. ui.menubar->hide();
  595. statusBar()->hide();
  596. showFullScreen();
  597. } else {
  598. UISettings::values.renderwindow_geometry = render_window->saveGeometry();
  599. render_window->showFullScreen();
  600. }
  601. }
  602. void GMainWindow::HideFullscreen() {
  603. if (ui.action_Single_Window_Mode->isChecked()) {
  604. statusBar()->setVisible(ui.action_Show_Status_Bar->isChecked());
  605. ui.menubar->show();
  606. showNormal();
  607. restoreGeometry(UISettings::values.geometry);
  608. } else {
  609. render_window->showNormal();
  610. render_window->restoreGeometry(UISettings::values.renderwindow_geometry);
  611. }
  612. }
  613. void GMainWindow::ToggleWindowMode() {
  614. if (ui.action_Single_Window_Mode->isChecked()) {
  615. // Render in the main window...
  616. render_window->BackupGeometry();
  617. ui.horizontalLayout->addWidget(render_window);
  618. render_window->setFocusPolicy(Qt::ClickFocus);
  619. if (emulation_running) {
  620. render_window->setVisible(true);
  621. render_window->setFocus();
  622. game_list->hide();
  623. }
  624. } else {
  625. // Render in a separate window...
  626. ui.horizontalLayout->removeWidget(render_window);
  627. render_window->setParent(nullptr);
  628. render_window->setFocusPolicy(Qt::NoFocus);
  629. if (emulation_running) {
  630. render_window->setVisible(true);
  631. render_window->RestoreGeometry();
  632. game_list->show();
  633. }
  634. }
  635. }
  636. void GMainWindow::OnConfigure() {
  637. ConfigureDialog configureDialog(this);
  638. auto old_theme = UISettings::values.theme;
  639. auto result = configureDialog.exec();
  640. if (result == QDialog::Accepted) {
  641. configureDialog.applyConfiguration();
  642. if (UISettings::values.theme != old_theme)
  643. UpdateUITheme();
  644. game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan);
  645. config->Save();
  646. }
  647. }
  648. void GMainWindow::OnAbout() {
  649. AboutDialog aboutDialog(this);
  650. aboutDialog.exec();
  651. }
  652. void GMainWindow::OnToggleFilterBar() {
  653. game_list->setFilterVisible(ui.action_Show_Filter_Bar->isChecked());
  654. if (ui.action_Show_Filter_Bar->isChecked()) {
  655. game_list->setFilterFocus();
  656. } else {
  657. game_list->clearFilter();
  658. }
  659. }
  660. void GMainWindow::UpdateStatusBar() {
  661. if (emu_thread == nullptr) {
  662. status_bar_update_timer.stop();
  663. return;
  664. }
  665. auto results = Core::System::GetInstance().GetAndResetPerfStats();
  666. emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0));
  667. game_fps_label->setText(tr("Game: %1 FPS").arg(results.game_fps, 0, 'f', 0));
  668. emu_frametime_label->setText(tr("Frame: %1 ms").arg(results.frametime * 1000.0, 0, 'f', 2));
  669. emu_speed_label->setVisible(true);
  670. game_fps_label->setVisible(true);
  671. emu_frametime_label->setVisible(true);
  672. }
  673. void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string details) {
  674. QMessageBox::StandardButton answer;
  675. QString status_message;
  676. const QString common_message = tr(
  677. "The game you are trying to load requires additional files from your Switch to be dumped "
  678. "before playing.<br/><br/>For more information on dumping these files, please see the "
  679. "following wiki page: <a "
  680. "href='https://yuzu-emu.org/wiki/"
  681. "dumping-system-archives-and-the-shared-fonts-from-a-switch-console/'>Dumping System "
  682. "Archives and the Shared Fonts from a Switch Console</a>.<br/><br/>Would you like to quit "
  683. "back to the game list? Continuing emulation may result in crashes, corrupted save "
  684. "data, or other bugs.");
  685. switch (result) {
  686. case Core::System::ResultStatus::ErrorSystemFiles: {
  687. QString message = "yuzu was unable to locate a Switch system archive";
  688. if (!details.empty()) {
  689. message.append(tr(": %1. ").arg(details.c_str()));
  690. } else {
  691. message.append(". ");
  692. }
  693. message.append(common_message);
  694. answer = QMessageBox::question(this, tr("System Archive Not Found"), message,
  695. QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
  696. status_message = "System Archive Missing";
  697. break;
  698. }
  699. case Core::System::ResultStatus::ErrorSharedFont: {
  700. QString message = tr("yuzu was unable to locate the Switch shared fonts. ");
  701. message.append(common_message);
  702. answer = QMessageBox::question(this, tr("Shared Fonts Not Found"), message,
  703. QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
  704. status_message = "Shared Font Missing";
  705. break;
  706. }
  707. default:
  708. answer = QMessageBox::question(
  709. this, tr("Fatal Error"),
  710. tr("yuzu has encountered a fatal error, please see the log for more details. "
  711. "For more information on accessing the log, please see the following page: "
  712. "<a href='https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>How to "
  713. "Upload the Log File</a>.<br/><br/>Would you like to quit back to the game list? "
  714. "Continuing emulation may result in crashes, corrupted save data, or other bugs."),
  715. QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
  716. status_message = "Fatal Error encountered";
  717. break;
  718. }
  719. if (answer == QMessageBox::Yes) {
  720. if (emu_thread) {
  721. ShutdownGame();
  722. }
  723. } else {
  724. // Only show the message if the game is still running.
  725. if (emu_thread) {
  726. emu_thread->SetRunning(true);
  727. message_label->setText(status_message);
  728. message_label->setVisible(true);
  729. }
  730. }
  731. }
  732. bool GMainWindow::ConfirmClose() {
  733. if (emu_thread == nullptr || !UISettings::values.confirm_before_closing)
  734. return true;
  735. QMessageBox::StandardButton answer =
  736. QMessageBox::question(this, tr("yuzu"), tr("Are you sure you want to close yuzu?"),
  737. QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
  738. return answer != QMessageBox::No;
  739. }
  740. void GMainWindow::closeEvent(QCloseEvent* event) {
  741. if (!ConfirmClose()) {
  742. event->ignore();
  743. return;
  744. }
  745. if (ui.action_Fullscreen->isChecked()) {
  746. UISettings::values.geometry = saveGeometry();
  747. UISettings::values.renderwindow_geometry = render_window->saveGeometry();
  748. }
  749. UISettings::values.state = saveState();
  750. #if MICROPROFILE_ENABLED
  751. UISettings::values.microprofile_geometry = microProfileDialog->saveGeometry();
  752. UISettings::values.microprofile_visible = microProfileDialog->isVisible();
  753. #endif
  754. UISettings::values.single_window_mode = ui.action_Single_Window_Mode->isChecked();
  755. UISettings::values.fullscreen = ui.action_Fullscreen->isChecked();
  756. UISettings::values.display_titlebar = ui.action_Display_Dock_Widget_Headers->isChecked();
  757. UISettings::values.show_filter_bar = ui.action_Show_Filter_Bar->isChecked();
  758. UISettings::values.show_status_bar = ui.action_Show_Status_Bar->isChecked();
  759. UISettings::values.first_start = false;
  760. game_list->SaveInterfaceLayout();
  761. SaveHotkeys();
  762. // Shutdown session if the emu thread is active...
  763. if (emu_thread != nullptr)
  764. ShutdownGame();
  765. render_window->close();
  766. QWidget::closeEvent(event);
  767. }
  768. static bool IsSingleFileDropEvent(QDropEvent* event) {
  769. const QMimeData* mimeData = event->mimeData();
  770. return mimeData->hasUrls() && mimeData->urls().length() == 1;
  771. }
  772. void GMainWindow::dropEvent(QDropEvent* event) {
  773. if (IsSingleFileDropEvent(event) && ConfirmChangeGame()) {
  774. const QMimeData* mimeData = event->mimeData();
  775. QString filename = mimeData->urls().at(0).toLocalFile();
  776. BootGame(filename);
  777. }
  778. }
  779. void GMainWindow::dragEnterEvent(QDragEnterEvent* event) {
  780. if (IsSingleFileDropEvent(event)) {
  781. event->acceptProposedAction();
  782. }
  783. }
  784. void GMainWindow::dragMoveEvent(QDragMoveEvent* event) {
  785. event->acceptProposedAction();
  786. }
  787. bool GMainWindow::ConfirmChangeGame() {
  788. if (emu_thread == nullptr)
  789. return true;
  790. auto answer = QMessageBox::question(
  791. this, tr("yuzu"),
  792. tr("Are you sure you want to stop the emulation? Any unsaved progress will be lost."),
  793. QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
  794. return answer != QMessageBox::No;
  795. }
  796. void GMainWindow::filterBarSetChecked(bool state) {
  797. ui.action_Show_Filter_Bar->setChecked(state);
  798. emit(OnToggleFilterBar());
  799. }
  800. void GMainWindow::UpdateUITheme() {
  801. QStringList theme_paths(default_theme_paths);
  802. if (UISettings::values.theme != UISettings::themes[0].second &&
  803. !UISettings::values.theme.isEmpty()) {
  804. const QString theme_uri(":" + UISettings::values.theme + "/style.qss");
  805. QFile f(theme_uri);
  806. if (f.open(QFile::ReadOnly | QFile::Text)) {
  807. QTextStream ts(&f);
  808. qApp->setStyleSheet(ts.readAll());
  809. GMainWindow::setStyleSheet(ts.readAll());
  810. } else {
  811. LOG_ERROR(Frontend, "Unable to set style, stylesheet file not found");
  812. }
  813. theme_paths.append(QStringList{":/icons/default", ":/icons/" + UISettings::values.theme});
  814. QIcon::setThemeName(":/icons/" + UISettings::values.theme);
  815. } else {
  816. qApp->setStyleSheet("");
  817. GMainWindow::setStyleSheet("");
  818. theme_paths.append(QStringList{":/icons/default"});
  819. QIcon::setThemeName(":/icons/default");
  820. }
  821. QIcon::setThemeSearchPaths(theme_paths);
  822. emit UpdateThemedIcons();
  823. }
  824. #ifdef main
  825. #undef main
  826. #endif
  827. static void InitializeLogging() {
  828. Log::Filter log_filter;
  829. log_filter.ParseFilterString(Settings::values.log_filter);
  830. Log::SetGlobalFilter(log_filter);
  831. const std::string& log_dir = FileUtil::GetUserPath(FileUtil::UserPath::LogDir);
  832. FileUtil::CreateFullPath(log_dir);
  833. Log::AddBackend(std::make_unique<Log::FileBackend>(log_dir + LOG_FILE));
  834. }
  835. int main(int argc, char* argv[]) {
  836. MicroProfileOnThreadCreate("Frontend");
  837. SCOPE_EXIT({ MicroProfileShutdown(); });
  838. // Init settings params
  839. QCoreApplication::setOrganizationName("yuzu team");
  840. QCoreApplication::setApplicationName("yuzu");
  841. QApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity);
  842. QApplication app(argc, argv);
  843. // Qt changes the locale and causes issues in float conversion using std::to_string() when
  844. // generating shaders
  845. setlocale(LC_ALL, "C");
  846. GMainWindow main_window;
  847. // After settings have been loaded by GMainWindow, apply the filter
  848. InitializeLogging();
  849. main_window.show();
  850. return app.exec();
  851. }