Lime-3DS-Emulator/src/lime_qt/main.cpp
2024-04-14 23:27:39 -04:00

3254 lines
121 KiB
C++

// Copyright 2014 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <clocale>
#include <memory>
#include <thread>
#include <QFileDialog>
#include <QFutureWatcher>
#include <QLabel>
#include <QMessageBox>
#include <QSysInfo>
#include <QtConcurrent/QtConcurrentMap>
#include <QtConcurrent/QtConcurrentRun>
#include <QtGui>
#include <QtWidgets>
#include <QtNetwork/QtNetwork>
#include <fmt/format.h>
#ifdef __APPLE__
#include <unistd.h> // for chdir
#endif
#ifdef _WIN32
#include <windows.h>
#endif
#ifdef __unix__
#include <QVariant>
#include <QtDBus/QDBusInterface>
#include <QtDBus/QtDBus>
#include "common/linux/gamemode.h"
#endif
#include "common/arch.h"
#include "common/common_paths.h"
#include "common/detached_tasks.h"
#include "common/dynamic_library/dynamic_library.h"
#include "common/file_util.h"
#include "common/literals.h"
#include "common/logging/backend.h"
#include "common/logging/log.h"
#include "common/memory_detect.h"
#include "common/scm_rev.h"
#include "common/scope_exit.h"
#include "lime_qt/aboutdialog.h"
#include "lime_qt/applets/mii_selector.h"
#include "lime_qt/applets/swkbd.h"
#include "lime_qt/bootmanager.h"
#include "lime_qt/camera/qt_multimedia_camera.h"
#include "lime_qt/camera/still_image_camera.h"
#include "lime_qt/compatdb.h"
#include "lime_qt/compatibility_list.h"
#include "lime_qt/configuration/config.h"
#include "lime_qt/configuration/configure_dialog.h"
#include "lime_qt/configuration/configure_per_game.h"
#include "lime_qt/debugger/console.h"
#include "lime_qt/debugger/graphics/graphics.h"
#include "lime_qt/debugger/graphics/graphics_breakpoints.h"
#include "lime_qt/debugger/graphics/graphics_cmdlists.h"
#include "lime_qt/debugger/graphics/graphics_surface.h"
#include "lime_qt/debugger/graphics/graphics_tracing.h"
#include "lime_qt/debugger/graphics/graphics_vertex_shader.h"
#include "lime_qt/debugger/ipc/recorder.h"
#include "lime_qt/debugger/lle_service_modules.h"
#include "lime_qt/debugger/profiler.h"
#include "lime_qt/debugger/registers.h"
#include "lime_qt/debugger/wait_tree.h"
#include "lime_qt/discord.h"
#include "lime_qt/dumping/dumping_dialog.h"
#include "lime_qt/game_list.h"
#include "lime_qt/hotkeys.h"
#include "lime_qt/loading_screen.h"
#include "lime_qt/main.h"
#include "lime_qt/movie/movie_play_dialog.h"
#include "lime_qt/movie/movie_record_dialog.h"
#include "lime_qt/multiplayer/state.h"
#include "lime_qt/qt_image_interface.h"
#include "lime_qt/uisettings.h"
#include "lime_qt/updater/updater.h"
#include "lime_qt/util/clickable_label.h"
#include "lime_qt/util/graphics_device_info.h"
#if CITRA_ARCH(x86_64)
#include "common/x64/cpu_detect.h"
#endif
#include "common/settings.h"
#include "core/core.h"
#include "core/dumping/backend.h"
#include "core/file_sys/archive_extsavedata.h"
#include "core/file_sys/archive_source_sd_savedata.h"
#include "core/frontend/applets/default_applets.h"
#include "core/hle/service/am/am.h"
#include "core/hle/service/fs/archive.h"
#include "core/hle/service/nfc/nfc.h"
#include "core/loader/loader.h"
#include "core/movie.h"
#include "core/savestate.h"
#include "core/system_titles.h"
#include "input_common/main.h"
#include "network/network_settings.h"
#include "ui_main.h"
#include "video_core/gpu.h"
#include "video_core/renderer_base.h"
#ifdef __APPLE__
#include "common/apple_authorization.h"
#endif
#ifdef USE_DISCORD_PRESENCE
#include "lime_qt/discord_impl.h"
#endif
#ifdef QT_STATICPLUGIN
Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin);
#endif
#ifdef _WIN32
extern "C" {
// tells Nvidia drivers to use the dedicated GPU by default on laptops with switchable graphics
__declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001;
}
#endif
#ifdef HAVE_SDL2
#include <SDL.h>
#endif
constexpr int default_mouse_timeout = 2500;
/**
* "Callouts" are one-time instructional messages shown to the user. In the config settings, there
* is a bitfield "callout_flags" options, used to track if a message has already been shown to the
* user. This is 32-bits - if we have more than 32 callouts, we should retire and recycle old ones.
*/
const int GMainWindow::max_recent_files_item;
static QString PrettyProductName() {
#ifdef _WIN32
// After Windows 10 Version 2004, Microsoft decided to switch to a different notation: 20H2
// With that notation change they changed the registry key used to denote the current version
QSettings windows_registry(
QStringLiteral("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"),
QSettings::NativeFormat);
const QString release_id = windows_registry.value(QStringLiteral("ReleaseId")).toString();
if (release_id == QStringLiteral("2009")) {
const u32 current_build = windows_registry.value(QStringLiteral("CurrentBuild")).toUInt();
const QString display_version =
windows_registry.value(QStringLiteral("DisplayVersion")).toString();
const u32 ubr = windows_registry.value(QStringLiteral("UBR")).toUInt();
const u32 version = current_build >= 22000 ? 11 : 10;
return QStringLiteral("Windows %1 Version %2 (Build %3.%4)")
.arg(QString::number(version), display_version, QString::number(current_build),
QString::number(ubr));
}
#endif
return QSysInfo::prettyProductName();
}
GMainWindow::GMainWindow(Core::System& system_)
: ui{std::make_unique<Ui::MainWindow>()}, system{system_}, movie{system.Movie()},
config{std::make_unique<Config>()}, emu_thread{nullptr} {
Common::Log::Initialize();
Common::Log::Start();
Debugger::ToggleConsole();
#ifdef __unix__
SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue());
#endif
// register types to use in slots and signals
qRegisterMetaType<std::size_t>("std::size_t");
qRegisterMetaType<Service::AM::InstallStatus>("Service::AM::InstallStatus");
// Register CameraFactory
qt_cameras = std::make_shared<Camera::QtMultimediaCameraHandlerFactory>();
Camera::RegisterFactory("image", std::make_unique<Camera::StillImageCameraFactory>());
Camera::RegisterFactory("qt", std::make_unique<Camera::QtMultimediaCameraFactory>(qt_cameras));
LoadTranslation();
Pica::g_debug_context = Pica::DebugContext::Construct();
setAcceptDrops(true);
ui->setupUi(this);
statusBar()->hide();
default_theme_paths = QIcon::themeSearchPaths();
UpdateUITheme();
SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue());
discord_rpc->Update();
Network::Init();
movie.SetPlaybackCompletionCallback([this] {
QMetaObject::invokeMethod(this, "OnMoviePlaybackCompleted", Qt::BlockingQueuedConnection);
});
InitializeWidgets();
InitializeDebugWidgets();
InitializeRecentFileMenuActions();
InitializeSaveStateMenuActions();
InitializeHotkeys();
ShowUpdatePopup();
#if ENABLE_QT_UPDATER
ShowUpdaterWidgets();
#else
ui->action_Check_For_Updates->setVisible(false);
ui->action_Open_Maintenance_Tool->setVisible(false);
#endif
SetDefaultUIGeometry();
RestoreUIState();
ConnectAppEvents();
ConnectMenuEvents();
ConnectWidgetEvents();
LOG_INFO(Frontend, "Lime3DS Version: {} | {}-{}", Common::g_build_fullname,
Common::g_scm_branch, Common::g_scm_desc);
#if CITRA_ARCH(x86_64)
const auto& caps = Common::GetCPUCaps();
std::string cpu_string = caps.cpu_string;
if (caps.avx || caps.avx2 || caps.avx512) {
cpu_string += " | AVX";
if (caps.avx512) {
cpu_string += "512";
} else if (caps.avx2) {
cpu_string += '2';
}
if (caps.fma || caps.fma4) {
cpu_string += " | FMA";
}
}
LOG_INFO(Frontend, "Host CPU: {}", cpu_string);
#endif
LOG_INFO(Frontend, "Host OS: {}", PrettyProductName().toStdString());
const auto& mem_info = Common::GetMemInfo();
using namespace Common::Literals;
LOG_INFO(Frontend, "Host RAM: {:.2f} GiB", mem_info.total_physical_memory / f64{1_GiB});
LOG_INFO(Frontend, "Host Swap: {:.2f} GiB", mem_info.total_swap_memory / f64{1_GiB});
UpdateWindowTitle();
show();
game_list->LoadCompatibilityList();
game_list->PopulateAsync(UISettings::values.game_dirs);
mouse_hide_timer.setInterval(default_mouse_timeout);
connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor);
connect(ui->menubar, &QMenuBar::hovered, this, &GMainWindow::OnMouseActivity);
#ifdef ENABLE_OPENGL
gl_renderer = GetOpenGLRenderer();
#if defined(_WIN32)
if (gl_renderer.startsWith(QStringLiteral("D3D12"))) {
// OpenGLOn12 supports but does not yet advertise OpenGL 4.0+
// We can override the version here to allow Citra to work.
// TODO: Remove this when OpenGL 4.0+ is advertised.
qputenv("MESA_GL_VERSION_OVERRIDE", "4.6");
}
#endif
#endif
#ifdef ENABLE_VULKAN
physical_devices = GetVulkanPhysicalDevices();
if (physical_devices.empty()) {
QMessageBox::warning(this, tr("No Suitable Vulkan Devices Detected"),
tr("Vulkan initialization failed during boot.<br/>"
"Your GPU may not support Vulkan 1.1, or you do not "
"have the latest graphics driver."));
}
#endif
#if ENABLE_QT_UPDATER
if (UISettings::values.check_for_update_on_start) {
CheckForUpdates();
}
#endif
QStringList args = QApplication::arguments();
if (args.size() < 2) {
return;
}
QString game_path;
for (int i = 1; i < args.size(); ++i) {
// Preserves drag/drop functionality
if (args.size() == 2 && !args[1].startsWith(QChar::fromLatin1('-'))) {
game_path = args[1];
break;
}
// Launch game in fullscreen mode
if (args[i] == QStringLiteral("-f")) {
ui->action_Fullscreen->setChecked(true);
continue;
}
// Launch game in windowed mode
if (args[i] == QStringLiteral("-w")) {
ui->action_Fullscreen->setChecked(false);
continue;
}
// Launch game at path
if (args[i] == QStringLiteral("-g")) {
if (i >= args.size() - 1) {
continue;
}
if (args[i + 1].startsWith(QChar::fromLatin1('-'))) {
continue;
}
game_path = args[++i];
}
}
if (!game_path.isEmpty()) {
BootGame(game_path);
}
}
GMainWindow::~GMainWindow() {
// Will get automatically deleted otherwise
if (!render_window->parent()) {
delete render_window;
}
Pica::g_debug_context.reset();
Network::Shutdown();
}
void GMainWindow::InitializeWidgets() {
#ifdef CITRA_ENABLE_COMPATIBILITY_REPORTING
ui->action_Report_Compatibility->setVisible(true);
#endif
render_window = new GRenderWindow(this, emu_thread.get(), system, false);
secondary_window = new GRenderWindow(this, emu_thread.get(), system, true);
render_window->hide();
secondary_window->hide();
secondary_window->setParent(nullptr);
game_list = new GameList(this);
ui->horizontalLayout->addWidget(game_list);
game_list_placeholder = new GameListPlaceholder(this);
ui->horizontalLayout->addWidget(game_list_placeholder);
game_list_placeholder->setVisible(false);
loading_screen = new LoadingScreen(this);
loading_screen->hide();
ui->horizontalLayout->addWidget(loading_screen);
connect(loading_screen, &LoadingScreen::Hidden, this, [&] {
loading_screen->Clear();
if (emulation_running) {
render_window->show();
render_window->setFocus();
render_window->activateWindow();
}
});
InputCommon::Init();
multiplayer_state = new MultiplayerState(system, this, game_list->GetModel(),
ui->action_Leave_Room, ui->action_Show_Room);
multiplayer_state->setVisible(false);
#if ENABLE_QT_UPDATER
// Setup updater
updater = new Updater(this);
UISettings::values.updater_found = updater->HasUpdater();
#endif
UpdateBootHomeMenuState();
// Create status bar
message_label = new QLabel();
// Configured separately for left alignment
message_label->setFrameStyle(QFrame::NoFrame);
message_label->setContentsMargins(4, 0, 4, 0);
message_label->setAlignment(Qt::AlignLeft);
statusBar()->addPermanentWidget(message_label, 1);
progress_bar = new QProgressBar();
progress_bar->hide();
statusBar()->addPermanentWidget(progress_bar);
emu_speed_label = new QLabel();
emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% "
"indicate emulation is running faster or slower than a 3DS."));
game_fps_label = new QLabel();
game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. "
"This will vary from game to game and scene to scene."));
emu_frametime_label = new QLabel();
emu_frametime_label->setToolTip(
tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For "
"full-speed emulation this should be at most 16.67 ms."));
for (auto& label : {emu_speed_label, game_fps_label, emu_frametime_label}) {
label->setVisible(false);
label->setFrameStyle(QFrame::NoFrame);
label->setContentsMargins(4, 0, 4, 0);
statusBar()->addPermanentWidget(label);
}
// Setup Graphics API button
graphics_api_button = new QPushButton();
graphics_api_button->setObjectName(QStringLiteral("GraphicsAPIStatusBarButton"));
graphics_api_button->setFocusPolicy(Qt::NoFocus);
UpdateAPIIndicator();
connect(graphics_api_button, &QPushButton::clicked, this, [this] { UpdateAPIIndicator(true); });
statusBar()->insertPermanentWidget(0, graphics_api_button);
volume_popup = new QWidget(this);
volume_popup->setWindowFlags(Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint | Qt::Popup);
volume_popup->setLayout(new QVBoxLayout());
volume_popup->setMinimumWidth(200);
volume_slider = new QSlider(Qt::Horizontal);
volume_slider->setObjectName(QStringLiteral("volume_slider"));
volume_slider->setMaximum(100);
volume_slider->setPageStep(5);
connect(volume_slider, &QSlider::valueChanged, this, [this](int percentage) {
Settings::values.audio_muted = false;
const auto value = static_cast<float>(percentage) / volume_slider->maximum();
Settings::values.volume.SetValue(value);
UpdateVolumeUI();
});
volume_popup->layout()->addWidget(volume_slider);
volume_button = new QPushButton();
volume_button->setObjectName(QStringLiteral("TogglableStatusBarButton"));
volume_button->setFocusPolicy(Qt::NoFocus);
volume_button->setCheckable(true);
UpdateVolumeUI();
connect(volume_button, &QPushButton::clicked, this, [&] {
UpdateVolumeUI();
volume_popup->setVisible(!volume_popup->isVisible());
QRect rect = volume_button->geometry();
QPoint bottomLeft = statusBar()->mapToGlobal(rect.topLeft());
bottomLeft.setY(bottomLeft.y() - volume_popup->geometry().height());
volume_popup->setGeometry(QRect(bottomLeft, QSize(rect.width(), rect.height())));
});
statusBar()->insertPermanentWidget(1, volume_button);
statusBar()->addPermanentWidget(multiplayer_state->GetStatusText());
statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon());
statusBar()->setVisible(true);
// Removes an ugly inner border from the status bar widgets under Linux
setStyleSheet(QStringLiteral("QStatusBar::item{border: none;}"));
QActionGroup* actionGroup_ScreenLayouts = new QActionGroup(this);
actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Default);
actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Single_Screen);
actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Large_Screen);
actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Hybrid_Screen);
actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Side_by_Side);
actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Separate_Windows);
}
void GMainWindow::InitializeDebugWidgets() {
connect(ui->action_Create_Pica_Surface_Viewer, &QAction::triggered, this,
&GMainWindow::OnCreateGraphicsSurfaceViewer);
QMenu* debug_menu = ui->menu_View_Debugging;
#if MICROPROFILE_ENABLED
microProfileDialog = new MicroProfileDialog(this);
microProfileDialog->hide();
debug_menu->addAction(microProfileDialog->toggleViewAction());
#endif
registersWidget = new RegistersWidget(system, this);
addDockWidget(Qt::RightDockWidgetArea, registersWidget);
registersWidget->hide();
debug_menu->addAction(registersWidget->toggleViewAction());
connect(this, &GMainWindow::EmulationStarting, registersWidget,
&RegistersWidget::OnEmulationStarting);
connect(this, &GMainWindow::EmulationStopping, registersWidget,
&RegistersWidget::OnEmulationStopping);
graphicsWidget = new GPUCommandStreamWidget(system, this);
addDockWidget(Qt::RightDockWidgetArea, graphicsWidget);
graphicsWidget->hide();
debug_menu->addAction(graphicsWidget->toggleViewAction());
graphicsCommandsWidget = new GPUCommandListWidget(system, this);
addDockWidget(Qt::RightDockWidgetArea, graphicsCommandsWidget);
graphicsCommandsWidget->hide();
debug_menu->addAction(graphicsCommandsWidget->toggleViewAction());
graphicsBreakpointsWidget = new GraphicsBreakPointsWidget(Pica::g_debug_context, this);
addDockWidget(Qt::RightDockWidgetArea, graphicsBreakpointsWidget);
graphicsBreakpointsWidget->hide();
debug_menu->addAction(graphicsBreakpointsWidget->toggleViewAction());
graphicsVertexShaderWidget =
new GraphicsVertexShaderWidget(system, Pica::g_debug_context, this);
addDockWidget(Qt::RightDockWidgetArea, graphicsVertexShaderWidget);
graphicsVertexShaderWidget->hide();
debug_menu->addAction(graphicsVertexShaderWidget->toggleViewAction());
graphicsTracingWidget = new GraphicsTracingWidget(system, Pica::g_debug_context, this);
addDockWidget(Qt::RightDockWidgetArea, graphicsTracingWidget);
graphicsTracingWidget->hide();
debug_menu->addAction(graphicsTracingWidget->toggleViewAction());
connect(this, &GMainWindow::EmulationStarting, graphicsTracingWidget,
&GraphicsTracingWidget::OnEmulationStarting);
connect(this, &GMainWindow::EmulationStopping, graphicsTracingWidget,
&GraphicsTracingWidget::OnEmulationStopping);
waitTreeWidget = new WaitTreeWidget(system, this);
addDockWidget(Qt::LeftDockWidgetArea, waitTreeWidget);
waitTreeWidget->hide();
debug_menu->addAction(waitTreeWidget->toggleViewAction());
connect(this, &GMainWindow::EmulationStarting, waitTreeWidget,
&WaitTreeWidget::OnEmulationStarting);
connect(this, &GMainWindow::EmulationStopping, waitTreeWidget,
&WaitTreeWidget::OnEmulationStopping);
lleServiceModulesWidget = new LLEServiceModulesWidget(this);
addDockWidget(Qt::RightDockWidgetArea, lleServiceModulesWidget);
lleServiceModulesWidget->hide();
debug_menu->addAction(lleServiceModulesWidget->toggleViewAction());
connect(this, &GMainWindow::EmulationStarting,
[this] { lleServiceModulesWidget->setDisabled(true); });
connect(this, &GMainWindow::EmulationStopping, waitTreeWidget,
[this] { lleServiceModulesWidget->setDisabled(false); });
ipcRecorderWidget = new IPCRecorderWidget(system, this);
addDockWidget(Qt::RightDockWidgetArea, ipcRecorderWidget);
ipcRecorderWidget->hide();
debug_menu->addAction(ipcRecorderWidget->toggleViewAction());
connect(this, &GMainWindow::EmulationStarting, ipcRecorderWidget,
&IPCRecorderWidget::OnEmulationStarting);
}
void GMainWindow::InitializeRecentFileMenuActions() {
for (int i = 0; i < max_recent_files_item; ++i) {
actions_recent_files[i] = new QAction(this);
actions_recent_files[i]->setVisible(false);
connect(actions_recent_files[i], &QAction::triggered, this, &GMainWindow::OnMenuRecentFile);
ui->menu_recent_files->addAction(actions_recent_files[i]);
}
ui->menu_recent_files->addSeparator();
QAction* action_clear_recent_files = new QAction(this);
action_clear_recent_files->setText(tr("Clear Recent Files"));
connect(action_clear_recent_files, &QAction::triggered, this, [this] {
UISettings::values.recent_files.clear();
UpdateRecentFiles();
});
ui->menu_recent_files->addAction(action_clear_recent_files);
UpdateRecentFiles();
}
void GMainWindow::InitializeSaveStateMenuActions() {
for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) {
actions_load_state[i] = new QAction(this);
actions_load_state[i]->setData(i + 1);
connect(actions_load_state[i], &QAction::triggered, this, &GMainWindow::OnLoadState);
ui->menu_Load_State->addAction(actions_load_state[i]);
actions_save_state[i] = new QAction(this);
actions_save_state[i]->setData(i + 1);
connect(actions_save_state[i], &QAction::triggered, this, &GMainWindow::OnSaveState);
ui->menu_Save_State->addAction(actions_save_state[i]);
}
connect(ui->action_Load_from_Newest_Slot, &QAction::triggered, this, [this] {
UpdateSaveStates();
if (newest_slot != 0) {
actions_load_state[newest_slot - 1]->trigger();
}
});
connect(ui->action_Save_to_Oldest_Slot, &QAction::triggered, this, [this] {
UpdateSaveStates();
actions_save_state[oldest_slot - 1]->trigger();
});
connect(ui->menu_Load_State->menuAction(), &QAction::hovered, this,
&GMainWindow::UpdateSaveStates);
connect(ui->menu_Save_State->menuAction(), &QAction::hovered, this,
&GMainWindow::UpdateSaveStates);
UpdateSaveStates();
}
void GMainWindow::InitializeHotkeys() {
hotkey_registry.LoadHotkeys();
const QString main_window = QStringLiteral("Main Window");
const QString fullscreen = QStringLiteral("Fullscreen");
const QString toggle_screen_layout = QStringLiteral("Toggle Screen Layout");
const QString swap_screens = QStringLiteral("Swap Screens");
const QString rotate_screens = QStringLiteral("Rotate Screens Upright");
const auto link_action_shortcut = [&](QAction* action, const QString& action_name) {
static const QString main_window = QStringLiteral("Main Window");
action->setShortcut(hotkey_registry.GetKeySequence(main_window, action_name));
action->setShortcutContext(hotkey_registry.GetShortcutContext(main_window, action_name));
action->setAutoRepeat(false);
this->addAction(action);
};
link_action_shortcut(ui->action_Load_File, QStringLiteral("Load File"));
link_action_shortcut(ui->action_Load_Amiibo, QStringLiteral("Load Amiibo"));
link_action_shortcut(ui->action_Remove_Amiibo, QStringLiteral("Remove Amiibo"));
link_action_shortcut(ui->action_Exit, QStringLiteral("Exit Lime3DS"));
link_action_shortcut(ui->action_Restart, QStringLiteral("Restart Emulation"));
link_action_shortcut(ui->action_Pause, QStringLiteral("Continue/Pause Emulation"));
link_action_shortcut(ui->action_Stop, QStringLiteral("Stop Emulation"));
link_action_shortcut(ui->action_Show_Filter_Bar, QStringLiteral("Toggle Filter Bar"));
link_action_shortcut(ui->action_Show_Status_Bar, QStringLiteral("Toggle Status Bar"));
link_action_shortcut(ui->action_Fullscreen, fullscreen);
link_action_shortcut(ui->action_Capture_Screenshot, QStringLiteral("Capture Screenshot"));
link_action_shortcut(ui->action_Screen_Layout_Swap_Screens, swap_screens);
link_action_shortcut(ui->action_Screen_Layout_Upright_Screens, rotate_screens);
link_action_shortcut(ui->action_Enable_Frame_Advancing,
QStringLiteral("Toggle Frame Advancing"));
link_action_shortcut(ui->action_Advance_Frame, QStringLiteral("Advance Frame"));
link_action_shortcut(ui->action_Load_from_Newest_Slot, QStringLiteral("Load from Newest Slot"));
link_action_shortcut(ui->action_Save_to_Oldest_Slot, QStringLiteral("Save to Oldest Slot"));
link_action_shortcut(ui->action_View_Lobby,
QStringLiteral("Multiplayer Browse Public Game Lobby"));
link_action_shortcut(ui->action_Start_Room, QStringLiteral("Multiplayer Create Room"));
link_action_shortcut(ui->action_Connect_To_Room,
QStringLiteral("Multiplayer Direct Connect to Room"));
link_action_shortcut(ui->action_Show_Room, QStringLiteral("Multiplayer Show Current Room"));
link_action_shortcut(ui->action_Leave_Room, QStringLiteral("Multiplayer Leave Room"));
const auto add_secondary_window_hotkey = [this](QKeySequence hotkey, const char* slot) {
// This action will fire specifically when secondary_window is in focus
QAction* secondary_window_action = new QAction(secondary_window);
secondary_window_action->setShortcut(hotkey);
connect(secondary_window_action, SIGNAL(triggered()), this, slot);
secondary_window->addAction(secondary_window_action);
};
// Use the same fullscreen hotkey as the main window
const auto fullscreen_hotkey = hotkey_registry.GetKeySequence(main_window, fullscreen);
add_secondary_window_hotkey(fullscreen_hotkey, SLOT(ToggleSecondaryFullscreen()));
const auto toggle_screen_hotkey =
hotkey_registry.GetKeySequence(main_window, toggle_screen_layout);
add_secondary_window_hotkey(toggle_screen_hotkey, SLOT(ToggleScreenLayout()));
const auto swap_screen_hotkey = hotkey_registry.GetKeySequence(main_window, swap_screens);
add_secondary_window_hotkey(swap_screen_hotkey, SLOT(TriggerSwapScreens()));
const auto rotate_screen_hotkey = hotkey_registry.GetKeySequence(main_window, rotate_screens);
add_secondary_window_hotkey(rotate_screen_hotkey, SLOT(TriggerRotateScreens()));
const auto connect_shortcut = [&](const QString& action_name, const auto& function) {
const auto* hotkey = hotkey_registry.GetHotkey(main_window, action_name, this);
connect(hotkey, &QShortcut::activated, this, function);
};
connect(hotkey_registry.GetHotkey(main_window, toggle_screen_layout, render_window),
&QShortcut::activated, this, &GMainWindow::ToggleScreenLayout);
connect_shortcut(QStringLiteral("Exit Fullscreen"), [&] {
if (emulation_running) {
ui->action_Fullscreen->setChecked(false);
ToggleFullscreen();
}
});
connect_shortcut(QStringLiteral("Toggle Per-Game Speed"), [&] {
Settings::values.frame_limit.SetGlobal(!Settings::values.frame_limit.UsingGlobal());
UpdateStatusBar();
});
connect_shortcut(QStringLiteral("Toggle Texture Dumping"),
[&] { Settings::values.dump_textures = !Settings::values.dump_textures; });
connect_shortcut(QStringLiteral("Toggle Custom Textures"),
[&] { Settings::values.custom_textures = !Settings::values.custom_textures; });
// We use "static" here in order to avoid capturing by lambda due to a MSVC bug, which makes
// the variable hold a garbage value after this function exits
static constexpr u16 SPEED_LIMIT_STEP = 5;
connect_shortcut(QStringLiteral("Increase Speed Limit"), [&] {
if (Settings::values.frame_limit.GetValue() == 0) {
return;
}
if (Settings::values.frame_limit.GetValue() < 995 - SPEED_LIMIT_STEP) {
Settings::values.frame_limit.SetValue(Settings::values.frame_limit.GetValue() +
SPEED_LIMIT_STEP);
} else {
Settings::values.frame_limit = 0;
}
UpdateStatusBar();
});
connect_shortcut(QStringLiteral("Decrease Speed Limit"), [&] {
if (Settings::values.frame_limit.GetValue() == 0) {
Settings::values.frame_limit = 995;
} else if (Settings::values.frame_limit.GetValue() > SPEED_LIMIT_STEP) {
Settings::values.frame_limit.SetValue(Settings::values.frame_limit.GetValue() -
SPEED_LIMIT_STEP);
UpdateStatusBar();
}
UpdateStatusBar();
});
connect_shortcut(QStringLiteral("Audio Mute/Unmute"), &GMainWindow::OnMute);
connect_shortcut(QStringLiteral("Audio Volume Down"), &GMainWindow::OnDecreaseVolume);
connect_shortcut(QStringLiteral("Audio Volume Up"), &GMainWindow::OnIncreaseVolume);
// We use "static" here in order to avoid capturing by lambda due to a MSVC bug, which makes the
// variable hold a garbage value after this function exits
static constexpr u16 FACTOR_3D_STEP = 5;
connect_shortcut(QStringLiteral("Decrease 3D Factor"), [this] {
const auto factor_3d = Settings::values.factor_3d.GetValue();
if (factor_3d > 0) {
if (factor_3d % FACTOR_3D_STEP != 0) {
Settings::values.factor_3d = factor_3d - (factor_3d % FACTOR_3D_STEP);
} else {
Settings::values.factor_3d = factor_3d - FACTOR_3D_STEP;
}
UpdateStatusBar();
}
});
connect_shortcut(QStringLiteral("Increase 3D Factor"), [this] {
const auto factor_3d = Settings::values.factor_3d.GetValue();
if (factor_3d < 100) {
if (factor_3d % FACTOR_3D_STEP != 0) {
Settings::values.factor_3d =
factor_3d + FACTOR_3D_STEP - (factor_3d % FACTOR_3D_STEP);
} else {
Settings::values.factor_3d = factor_3d + FACTOR_3D_STEP;
}
UpdateStatusBar();
}
});
}
void GMainWindow::SetDefaultUIGeometry() {
// geometry: 55% of the window contents are in the upper screen half, 45% in the lower half
const QRect screenRect = screen()->geometry();
const int w = screenRect.width() * 2 / 3;
const int h = screenRect.height() / 2;
const int x = (screenRect.x() + screenRect.width()) / 2 - w / 2;
const int y = (screenRect.y() + screenRect.height()) / 2 - h * 55 / 100;
setGeometry(x, y, w, h);
}
void GMainWindow::RestoreUIState() {
restoreGeometry(UISettings::values.geometry);
restoreState(UISettings::values.state);
render_window->restoreGeometry(UISettings::values.renderwindow_geometry);
#if MICROPROFILE_ENABLED
microProfileDialog->restoreGeometry(UISettings::values.microprofile_geometry);
microProfileDialog->setVisible(UISettings::values.microprofile_visible.GetValue());
#endif
game_list->LoadInterfaceLayout();
ui->action_Single_Window_Mode->setChecked(UISettings::values.single_window_mode.GetValue());
ToggleWindowMode();
ui->action_Fullscreen->setChecked(UISettings::values.fullscreen.GetValue());
SyncMenuUISettings();
ui->action_Display_Dock_Widget_Headers->setChecked(
UISettings::values.display_titlebar.GetValue());
OnDisplayTitleBars(ui->action_Display_Dock_Widget_Headers->isChecked());
ui->action_Show_Filter_Bar->setChecked(UISettings::values.show_filter_bar.GetValue());
game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked());
ui->action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar.GetValue());
statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked());
}
void GMainWindow::OnAppFocusStateChanged(Qt::ApplicationState state) {
if (state != Qt::ApplicationHidden && state != Qt::ApplicationInactive &&
state != Qt::ApplicationActive) {
LOG_DEBUG(Frontend, "ApplicationState unusual flag: {} ", state);
}
if (!emulation_running) {
return;
}
if (UISettings::values.pause_when_in_background) {
if (emu_thread->IsRunning() &&
(state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) {
auto_paused = true;
OnPauseGame();
} else if (!emu_thread->IsRunning() && auto_paused && state == Qt::ApplicationActive) {
auto_paused = false;
OnStartGame();
}
}
if (UISettings::values.mute_when_in_background) {
if (!Settings::values.audio_muted &&
(state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) {
Settings::values.audio_muted = true;
auto_muted = true;
} else if (auto_muted && state == Qt::ApplicationActive) {
Settings::values.audio_muted = false;
auto_muted = false;
}
UpdateVolumeUI();
}
}
bool GApplicationEventFilter::eventFilter(QObject* object, QEvent* event) {
if (event->type() == QEvent::FileOpen) {
emit FileOpen(static_cast<QFileOpenEvent*>(event));
return true;
}
return false;
}
void GMainWindow::ConnectAppEvents() {
const auto filter = new GApplicationEventFilter();
QGuiApplication::instance()->installEventFilter(filter);
connect(filter, &GApplicationEventFilter::FileOpen, this, &GMainWindow::OnFileOpen);
}
void GMainWindow::ConnectWidgetEvents() {
connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile);
connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory);
connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder);
connect(game_list, &GameList::NavigateToGamedbEntryRequested, this,
&GMainWindow::OnGameListNavigateToGamedbEntry);
connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS);
connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory);
connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this,
&GMainWindow::OnGameListAddDirectory);
connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList);
connect(game_list, &GameList::PopulatingCompleted, this,
[this] { multiplayer_state->UpdateGameList(game_list->GetModel()); });
connect(game_list, &GameList::OpenPerGameGeneralRequested, this,
&GMainWindow::OnGameListOpenPerGameProperties);
connect(this, &GMainWindow::EmulationStarting, render_window,
&GRenderWindow::OnEmulationStarting);
connect(this, &GMainWindow::EmulationStopping, render_window,
&GRenderWindow::OnEmulationStopping);
connect(this, &GMainWindow::EmulationStarting, secondary_window,
&GRenderWindow::OnEmulationStarting);
connect(this, &GMainWindow::EmulationStopping, secondary_window,
&GRenderWindow::OnEmulationStopping);
connect(&status_bar_update_timer, &QTimer::timeout, this, &GMainWindow::UpdateStatusBar);
connect(this, &GMainWindow::UpdateProgress, this, &GMainWindow::OnUpdateProgress);
connect(this, &GMainWindow::CIAInstallReport, this, &GMainWindow::OnCIAInstallReport);
connect(this, &GMainWindow::CIAInstallFinished, this, &GMainWindow::OnCIAInstallFinished);
connect(this, &GMainWindow::UpdateThemedIcons, multiplayer_state,
&MultiplayerState::UpdateThemedIcons);
}
void GMainWindow::ConnectMenuEvents() {
const auto connect_menu = [&](QAction* action, const auto& event_fn) {
connect(action, &QAction::triggered, this, event_fn);
// Add actions to this window so that hiding menus in fullscreen won't disable them
addAction(action);
// Add actions to the render window so that they work outside of single window mode
render_window->addAction(action);
};
// File
connect_menu(ui->action_Load_File, &GMainWindow::OnMenuLoadFile);
connect_menu(ui->action_Install_CIA, &GMainWindow::OnMenuInstallCIA);
for (u32 region = 0; region < Core::NUM_SYSTEM_TITLE_REGIONS; region++) {
connect_menu(ui->menu_Boot_Home_Menu->actions().at(region),
[this, region] { OnMenuBootHomeMenu(region); });
}
connect_menu(ui->action_Exit, &QMainWindow::close);
connect_menu(ui->action_Load_Amiibo, &GMainWindow::OnLoadAmiibo);
connect_menu(ui->action_Remove_Amiibo, &GMainWindow::OnRemoveAmiibo);
// Emulation
connect_menu(ui->action_Pause, &GMainWindow::OnPauseContinueGame);
connect_menu(ui->action_Stop, &GMainWindow::OnStopGame);
connect_menu(ui->action_Restart, [this] { BootGame(QString(game_path)); });
connect_menu(ui->action_Report_Compatibility, &GMainWindow::OnMenuReportCompatibility);
connect_menu(ui->action_Configure, &GMainWindow::OnConfigure);
connect_menu(ui->action_Configure_Current_Game, &GMainWindow::OnConfigurePerGame);
// View
connect_menu(ui->action_Single_Window_Mode, &GMainWindow::ToggleWindowMode);
connect_menu(ui->action_Display_Dock_Widget_Headers, &GMainWindow::OnDisplayTitleBars);
connect_menu(ui->action_Show_Filter_Bar, &GMainWindow::OnToggleFilterBar);
connect(ui->action_Show_Status_Bar, &QAction::triggered, statusBar(), &QStatusBar::setVisible);
// Multiplayer
connect(ui->action_View_Lobby, &QAction::triggered, multiplayer_state,
&MultiplayerState::OnViewLobby);
connect(ui->action_Start_Room, &QAction::triggered, multiplayer_state,
&MultiplayerState::OnCreateRoom);
connect(ui->action_Leave_Room, &QAction::triggered, multiplayer_state,
&MultiplayerState::OnCloseRoom);
connect(ui->action_Connect_To_Room, &QAction::triggered, multiplayer_state,
&MultiplayerState::OnDirectConnectToRoom);
connect(ui->action_Show_Room, &QAction::triggered, multiplayer_state,
&MultiplayerState::OnOpenNetworkRoom);
connect_menu(ui->action_Fullscreen, &GMainWindow::ToggleFullscreen);
connect_menu(ui->action_Screen_Layout_Default, &GMainWindow::ChangeScreenLayout);
connect_menu(ui->action_Screen_Layout_Single_Screen, &GMainWindow::ChangeScreenLayout);
connect_menu(ui->action_Screen_Layout_Large_Screen, &GMainWindow::ChangeScreenLayout);
connect_menu(ui->action_Screen_Layout_Hybrid_Screen, &GMainWindow::ChangeScreenLayout);
connect_menu(ui->action_Screen_Layout_Side_by_Side, &GMainWindow::ChangeScreenLayout);
connect_menu(ui->action_Screen_Layout_Separate_Windows, &GMainWindow::ChangeScreenLayout);
connect_menu(ui->action_Screen_Layout_Swap_Screens, &GMainWindow::OnSwapScreens);
connect_menu(ui->action_Screen_Layout_Upright_Screens, &GMainWindow::OnRotateScreens);
// Movie
connect_menu(ui->action_Record_Movie, &GMainWindow::OnRecordMovie);
connect_menu(ui->action_Play_Movie, &GMainWindow::OnPlayMovie);
connect_menu(ui->action_Close_Movie, &GMainWindow::OnCloseMovie);
connect_menu(ui->action_Save_Movie, &GMainWindow::OnSaveMovie);
connect_menu(ui->action_Movie_Read_Only_Mode,
[this](bool checked) { movie.SetReadOnly(checked); });
connect_menu(ui->action_Enable_Frame_Advancing, [this] {
if (emulation_running) {
system.frame_limiter.SetFrameAdvancing(ui->action_Enable_Frame_Advancing->isChecked());
ui->action_Advance_Frame->setEnabled(ui->action_Enable_Frame_Advancing->isChecked());
}
});
connect_menu(ui->action_Advance_Frame, [this] {
if (emulation_running && system.frame_limiter.IsFrameAdvancing()) {
ui->action_Enable_Frame_Advancing->setChecked(true);
ui->action_Advance_Frame->setEnabled(true);
system.frame_limiter.AdvanceFrame();
}
});
connect_menu(ui->action_Capture_Screenshot, &GMainWindow::OnCaptureScreenshot);
connect_menu(ui->action_Dump_Video, &GMainWindow::OnDumpVideo);
// Help
connect_menu(ui->action_Open_Citra_Folder, &GMainWindow::OnOpenCitraFolder);
connect_menu(ui->action_FAQ, []() {
QDesktopServices::openUrl(QUrl(QStringLiteral("https://discord.com/invite/4ZjMpAp3M6")));
});
connect_menu(ui->action_About, &GMainWindow::OnMenuAboutCitra);
#if ENABLE_QT_UPDATER
connect_menu(ui->action_Check_For_Updates, &GMainWindow::OnCheckForUpdates);
connect_menu(ui->action_Open_Maintenance_Tool, &GMainWindow::OnOpenUpdater);
#endif
}
void GMainWindow::UpdateMenuState() {
const bool is_paused = !emu_thread || !emu_thread->IsRunning();
const std::array running_actions{
ui->action_Stop,
ui->action_Restart,
ui->action_Configure_Current_Game,
ui->action_Report_Compatibility,
ui->action_Load_Amiibo,
ui->action_Remove_Amiibo,
ui->action_Pause,
ui->action_Advance_Frame,
};
for (QAction* action : running_actions) {
action->setEnabled(emulation_running);
}
ui->action_Capture_Screenshot->setEnabled(emulation_running && !is_paused);
if (emulation_running && is_paused) {
ui->action_Pause->setText(tr("&Continue"));
} else {
ui->action_Pause->setText(tr("&Pause"));
}
}
void GMainWindow::OnDisplayTitleBars(bool show) {
QList<QDockWidget*> widgets = findChildren<QDockWidget*>();
if (show) {
for (QDockWidget* widget : widgets) {
QWidget* old = widget->titleBarWidget();
widget->setTitleBarWidget(nullptr);
if (old) {
delete old;
}
}
} else {
for (QDockWidget* widget : widgets) {
QWidget* old = widget->titleBarWidget();
widget->setTitleBarWidget(new QWidget());
if (old) {
delete old;
}
}
}
}
#if ENABLE_QT_UPDATER
void GMainWindow::OnCheckForUpdates() {
explicit_update_check = true;
CheckForUpdates();
}
void GMainWindow::CheckForUpdates() {
if (updater->CheckForUpdates()) {
LOG_INFO(Frontend, "Update check started");
} else {
LOG_WARNING(Frontend, "Unable to start check for updates");
}
}
void GMainWindow::OnUpdateFound(bool found, bool error) {
if (error) {
LOG_WARNING(Frontend, "Update check failed");
return;
}
if (!found) {
LOG_INFO(Frontend, "No updates found");
// If the user explicitly clicked the "Check for Updates" button, we are
// going to want to show them a prompt anyway.
if (explicit_update_check) {
explicit_update_check = false;
ShowNoUpdatePrompt();
}
return;
}
if (emulation_running && !explicit_update_check) {
LOG_INFO(Frontend, "Update found, deferring as game is running");
defer_update_prompt = true;
return;
}
LOG_INFO(Frontend, "Update found!");
explicit_update_check = false;
ShowUpdatePrompt();
}
void GMainWindow::ShowUpdatePrompt() {
defer_update_prompt = false;
auto result =
QMessageBox::question(this, tr("Update Available"),
tr("An update is available. Would you like to install it now?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
if (result == QMessageBox::Yes) {
updater->LaunchUIOnExit();
close();
}
}
void GMainWindow::ShowNoUpdatePrompt() {
QMessageBox::information(this, tr("No Update Found"), tr("No update is found."),
QMessageBox::Ok, QMessageBox::Ok);
}
void GMainWindow::OnOpenUpdater() {
updater->LaunchUI();
}
void GMainWindow::ShowUpdaterWidgets() {
ui->action_Check_For_Updates->setVisible(UISettings::values.updater_found);
ui->action_Open_Maintenance_Tool->setVisible(UISettings::values.updater_found);
connect(updater, &Updater::CheckUpdatesDone, this, &GMainWindow::OnUpdateFound);
}
#endif
void GMainWindow::ShowUpdatePopup() {
QNetworkAccessManager* manager = new QNetworkAccessManager();
QObject::connect(manager, &QNetworkAccessManager::finished, this, [&](QNetworkReply *reply) {
if (reply->error()) {
QByteArray ba = reply->errorString().toLocal8Bit();
QMessageBox::information(
this, tr("New Lime3DS Version"),
tr( ba.data()
));
manager->clearAccessCache();
return;
} else {
QJsonObject jsonObject = QJsonDocument::fromJson(reply->readAll()).object();
QString jsonTag = jsonObject[QStringLiteral("tag_name")].toString();
if (jsonTag != QString::fromUtf8(Common::g_build_fullname)) {
QMessageBox::information(
this, tr("New Lime3DS Version"),
tr( " "
"A New Version of Lime3DS is Available!"
"Download <a href='https://github.com/Lime3DS/Lime3DS/releases'>here</a>"
));
}
}
});
QUrl url(QStringLiteral("https://api.github.com/repos/Lime3DS/Lime3DS/releases/latest"));
QNetworkRequest networkRequest(url);
manager->get(networkRequest);
}
#if defined(HAVE_SDL2) && defined(__unix__) && !defined(__APPLE__)
static std::optional<QDBusObjectPath> HoldWakeLockLinux(u32 window_id = 0) {
if (!QDBusConnection::sessionBus().isConnected()) {
return {};
}
// reference: https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.Inhibit
QDBusInterface xdp(QStringLiteral("org.freedesktop.portal.Desktop"),
QStringLiteral("/org/freedesktop/portal/desktop"),
QStringLiteral("org.freedesktop.portal.Inhibit"));
if (!xdp.isValid()) {
LOG_WARNING(Frontend, "Couldn't connect to XDP D-Bus endpoint");
return {};
}
QVariantMap options = {};
//: TRANSLATORS: This string is shown to the user to explain why Citra needs to prevent the
//: computer from sleeping
options.insert(QString::fromLatin1("reason"),
QCoreApplication::translate("GMainWindow", "Citra is running a game"));
// 0x4: Suspend lock; 0x8: Idle lock
QDBusReply<QDBusObjectPath> reply =
xdp.call(QString::fromLatin1("Inhibit"),
QString::fromLatin1("x11:") + QString::number(window_id, 16), 12U, options);
if (reply.isValid()) {
return reply.value();
}
LOG_WARNING(Frontend, "Couldn't read Inhibit reply from XDP: {}",
reply.error().message().toStdString());
return {};
}
static void ReleaseWakeLockLinux(const QDBusObjectPath& lock) {
if (!QDBusConnection::sessionBus().isConnected()) {
return;
}
QDBusInterface unlocker(QString::fromLatin1("org.freedesktop.portal.Desktop"), lock.path(),
QString::fromLatin1("org.freedesktop.portal.Request"));
unlocker.call(QString::fromLatin1("Close"));
}
#endif // __unix__
void GMainWindow::PreventOSSleep() {
#ifdef _WIN32
SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED);
#elif defined(HAVE_SDL2)
SDL_DisableScreenSaver();
#if defined(__unix__) && !defined(__APPLE__)
auto reply = HoldWakeLockLinux(winId());
if (reply) {
wake_lock = std::move(reply.value());
}
#endif // defined(__unix__) && !defined(__APPLE__)
#endif // _WIN32
}
void GMainWindow::AllowOSSleep() {
#ifdef _WIN32
SetThreadExecutionState(ES_CONTINUOUS);
#elif defined(HAVE_SDL2)
SDL_EnableScreenSaver();
#if defined(__unix__) && !defined(__APPLE__)
if (!wake_lock.path().isEmpty()) {
ReleaseWakeLockLinux(wake_lock);
}
#endif // defined(__unix__) && !defined(__APPLE__)
#endif // _WIN32
}
bool GMainWindow::LoadROM(const QString& filename) {
// Shutdown previous session if the emu thread is still active...
if (emu_thread) {
ShutdownGame();
}
if (!render_window->InitRenderTarget() || !secondary_window->InitRenderTarget()) {
LOG_CRITICAL(Frontend, "Failed to initialize render targets!");
return false;
}
const auto scope = render_window->Acquire();
const Core::System::ResultStatus result{
system.Load(*render_window, filename.toStdString(), secondary_window)};
if (result != Core::System::ResultStatus::Success) {
switch (result) {
case Core::System::ResultStatus::ErrorGetLoader:
LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filename.toStdString());
QMessageBox::critical(
this, tr("Invalid ROM Format"),
tr("Your ROM format is not supported.<br/>Please follow the guides to redump your "
"<a href='https://citra-emu.org/wiki/dumping-game-cartridges/'>game "
"cartridges</a> or "
"<a href='https://citra-emu.org/wiki/dumping-installed-titles/'>installed "
"titles</a>."));
break;
case Core::System::ResultStatus::ErrorSystemMode:
LOG_CRITICAL(Frontend, "Failed to load ROM!");
QMessageBox::critical(
this, tr("ROM Corrupted"),
tr("Your ROM is corrupted. <br/>Please follow the guides to redump your "
"<a href='https://citra-emu.org/wiki/dumping-game-cartridges/'>game "
"cartridges</a> or "
"<a href='https://citra-emu.org/wiki/dumping-installed-titles/'>installed "
"titles</a>."));
break;
case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: {
QMessageBox::critical(
this, tr("ROM Encrypted"),
tr("Your ROM is encrypted. <br/>Please follow the guides to redump your "
"<a href='https://citra-emu.org/wiki/dumping-game-cartridges/'>game "
"cartridges</a> or "
"<a href='https://citra-emu.org/wiki/dumping-installed-titles/'>installed "
"titles</a>."));
break;
}
case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat:
QMessageBox::critical(
this, tr("Invalid ROM Format"),
tr("Your ROM format is not supported.<br/>Please follow the guides to redump your "
"<a href='https://citra-emu.org/wiki/dumping-game-cartridges/'>game "
"cartridges</a> or "
"<a href='https://citra-emu.org/wiki/dumping-installed-titles/'>installed "
"titles</a>."));
break;
case Core::System::ResultStatus::ErrorLoader_ErrorGbaTitle:
QMessageBox::critical(this, tr("Unsupported ROM"),
tr("GBA Virtual Console ROMs are not supported by Citra."));
break;
default:
QMessageBox::critical(
this, tr("Error while loading ROM!"),
tr("An unknown error occurred. Please see the log for more details."));
break;
}
return false;
}
std::string title;
std::string title_long;
system.GetAppLoader().ReadTitle(title);
system.GetAppLoader().ReadTitleLong(title_long);
game_title = QString::fromStdString(title);
game_title_long = QString::fromStdString(title_long);
UpdateWindowTitle();
game_path = filename;
return true;
}
void GMainWindow::BootGame(const QString& filename) {
if (filename.endsWith(QStringLiteral(".cia"))) {
const auto answer = QMessageBox::question(
this, tr("CIA must be installed before usage"),
tr("Before using this CIA, you must install it. Do you want to install it now?"),
QMessageBox::Yes | QMessageBox::No);
if (answer == QMessageBox::Yes)
InstallCIA(QStringList(filename));
return;
}
LOG_INFO(Frontend, "Lime3DS starting...");
StoreRecentFile(filename); // Put the filename on top of the list
if (movie_record_on_start) {
movie.PrepareForRecording();
}
if (movie_playback_on_start) {
movie.PrepareForPlayback(movie_playback_path.toStdString());
}
const std::string path = filename.toStdString();
const auto loader = Loader::GetLoader(path);
u64 title_id{0};
loader->ReadProgramId(title_id);
// Load per game settings
const std::string name{FileUtil::GetFilename(filename.toStdString())};
const std::string config_file_name = title_id == 0 ? name : fmt::format("{:016X}", title_id);
LOG_INFO(Frontend, "Loading per game config file for title {}", config_file_name);
Config per_game_config(config_file_name, Config::ConfigType::PerGameConfig);
system.ApplySettings();
Settings::LogSettings();
// Save configurations
UpdateUISettings();
game_list->SaveInterfaceLayout();
config->Save();
if (!LoadROM(filename))
return;
// Set everything up
if (movie_record_on_start) {
movie.StartRecording(movie_record_path.toStdString(), movie_record_author.toStdString());
movie_record_on_start = false;
movie_record_path.clear();
movie_record_author.clear();
}
if (movie_playback_on_start) {
movie.StartPlayback(movie_playback_path.toStdString());
movie_playback_on_start = false;
movie_playback_path.clear();
}
if (ui->action_Enable_Frame_Advancing->isChecked()) {
ui->action_Advance_Frame->setEnabled(true);
system.frame_limiter.SetFrameAdvancing(true);
} else {
ui->action_Advance_Frame->setEnabled(false);
}
if (video_dumping_on_start) {
StartVideoDumping(video_dumping_path);
video_dumping_on_start = false;
video_dumping_path.clear();
}
// Register debug widgets
if (graphicsWidget->isVisible()) {
graphicsWidget->Register();
}
// Create and start the emulation thread
emu_thread = std::make_unique<EmuThread>(system, *render_window);
emit EmulationStarting(emu_thread.get());
emu_thread->start();
connect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame);
connect(render_window, &GRenderWindow::MouseActivity, this, &GMainWindow::OnMouseActivity);
connect(secondary_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame);
connect(secondary_window, &GRenderWindow::MouseActivity, this, &GMainWindow::OnMouseActivity);
// BlockingQueuedConnection is important here, it makes sure we've finished refreshing our views
// before the CPU continues
connect(emu_thread.get(), &EmuThread::DebugModeEntered, registersWidget,
&RegistersWidget::OnDebugModeEntered, Qt::BlockingQueuedConnection);
connect(emu_thread.get(), &EmuThread::DebugModeEntered, waitTreeWidget,
&WaitTreeWidget::OnDebugModeEntered, Qt::BlockingQueuedConnection);
connect(emu_thread.get(), &EmuThread::DebugModeLeft, registersWidget,
&RegistersWidget::OnDebugModeLeft, Qt::BlockingQueuedConnection);
connect(emu_thread.get(), &EmuThread::DebugModeLeft, waitTreeWidget,
&WaitTreeWidget::OnDebugModeLeft, Qt::BlockingQueuedConnection);
connect(emu_thread.get(), &EmuThread::LoadProgress, loading_screen,
&LoadingScreen::OnLoadProgress, Qt::QueuedConnection);
connect(emu_thread.get(), &EmuThread::HideLoadingScreen, loading_screen,
&LoadingScreen::OnLoadComplete);
// Update the GUI
registersWidget->OnDebugModeEntered();
if (ui->action_Single_Window_Mode->isChecked()) {
game_list->hide();
game_list_placeholder->hide();
}
status_bar_update_timer.start(1000);
if (UISettings::values.hide_mouse) {
mouse_hide_timer.start();
setMouseTracking(true);
}
loading_screen->Prepare(system.GetAppLoader());
loading_screen->show();
emulation_running = true;
if (ui->action_Fullscreen->isChecked()) {
ShowFullscreen();
}
OnStartGame();
}
void GMainWindow::ShutdownGame() {
if (!emulation_running) {
return;
}
if (ui->action_Fullscreen->isChecked()) {
HideFullscreen();
}
auto video_dumper = system.GetVideoDumper();
if (video_dumper && video_dumper->IsDumping()) {
game_shutdown_delayed = true;
OnStopVideoDumping();
return;
}
AllowOSSleep();
discord_rpc->Pause();
emu_thread->RequestStop();
// Release emu threads from any breakpoints
// This belongs after RequestStop() and before wait() because if emulation stops on a GPU
// breakpoint after (or before) RequestStop() is called, the emulation would never be able
// to continue out to the main loop and terminate. Thus wait() would hang forever.
// TODO(bunnei): This function is not thread safe, but it's being used as if it were
Pica::g_debug_context->ClearBreakpoints();
// Unregister debug widgets
if (graphicsWidget->isVisible()) {
graphicsWidget->Unregister();
}
// Frame advancing must be cancelled in order to release the emu thread from waiting
system.frame_limiter.SetFrameAdvancing(false);
emit EmulationStopping();
// Wait for emulation thread to complete and delete it
emu_thread->wait();
emu_thread = nullptr;
OnCloseMovie();
discord_rpc->Update();
#ifdef __unix__
Common::Linux::StopGamemode();
#endif
// The emulation is stopped, so closing the window or not does not matter anymore
disconnect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame);
disconnect(secondary_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame);
render_window->hide();
secondary_window->hide();
loading_screen->hide();
loading_screen->Clear();
if (game_list->IsEmpty()) {
game_list_placeholder->show();
} else {
game_list->show();
}
game_list->SetFilterFocus();
setMouseTracking(false);
// Disable status bar updates
status_bar_update_timer.stop();
message_label_used_for_movie = false;
emu_speed_label->setVisible(false);
game_fps_label->setVisible(false);
emu_frametime_label->setVisible(false);
UpdateSaveStates();
emulation_running = false;
#if ENABLE_QT_UDPATER
if (defer_update_prompt) {
ShowUpdatePrompt();
}
#endif
game_title.clear();
game_title_long.clear();
UpdateWindowTitle();
game_path.clear();
// Update the GUI
UpdateMenuState();
// When closing the game, destroy the GLWindow to clear the context after the game is closed
render_window->ReleaseRenderTarget();
secondary_window->ReleaseRenderTarget();
}
void GMainWindow::StoreRecentFile(const QString& filename) {
UISettings::values.recent_files.prepend(filename);
UISettings::values.recent_files.removeDuplicates();
while (UISettings::values.recent_files.size() > max_recent_files_item) {
UISettings::values.recent_files.removeLast();
}
UpdateRecentFiles();
}
void GMainWindow::UpdateRecentFiles() {
const int num_recent_files =
std::min(static_cast<int>(UISettings::values.recent_files.size()), max_recent_files_item);
for (int i = 0; i < num_recent_files; i++) {
const QString text = QStringLiteral("&%1. %2").arg(i + 1).arg(
QFileInfo(UISettings::values.recent_files[i]).fileName());
actions_recent_files[i]->setText(text);
actions_recent_files[i]->setData(UISettings::values.recent_files[i]);
actions_recent_files[i]->setToolTip(UISettings::values.recent_files[i]);
actions_recent_files[i]->setVisible(true);
}
for (int j = num_recent_files; j < max_recent_files_item; ++j) {
actions_recent_files[j]->setVisible(false);
}
// Enable the recent files menu if the list isn't empty
ui->menu_recent_files->setEnabled(num_recent_files != 0);
}
void GMainWindow::UpdateSaveStates() {
if (!system.IsPoweredOn()) {
ui->menu_Load_State->setEnabled(false);
ui->menu_Save_State->setEnabled(false);
return;
}
ui->menu_Load_State->setEnabled(true);
ui->menu_Save_State->setEnabled(true);
ui->action_Load_from_Newest_Slot->setEnabled(false);
oldest_slot = newest_slot = 0;
oldest_slot_time = std::numeric_limits<u64>::max();
newest_slot_time = 0;
u64 title_id;
if (system.GetAppLoader().ReadProgramId(title_id) != Loader::ResultStatus::Success) {
return;
}
auto savestates = Core::ListSaveStates(title_id, movie.GetCurrentMovieID());
for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) {
actions_load_state[i]->setEnabled(false);
actions_load_state[i]->setText(tr("Slot %1").arg(i + 1));
actions_save_state[i]->setText(tr("Slot %1").arg(i + 1));
}
for (const auto& savestate : savestates) {
const bool display_name =
savestate.status == Core::SaveStateInfo::ValidationStatus::RevisionDismatch &&
!savestate.build_name.empty();
const auto text =
tr("Slot %1 - %2 %3")
.arg(savestate.slot)
.arg(QDateTime::fromSecsSinceEpoch(savestate.time)
.toString(QStringLiteral("yyyy-MM-dd hh:mm:ss")))
.arg(display_name ? QString::fromStdString(savestate.build_name) : QLatin1String())
.trimmed();
actions_load_state[savestate.slot - 1]->setEnabled(true);
actions_load_state[savestate.slot - 1]->setText(text);
actions_save_state[savestate.slot - 1]->setText(text);
ui->action_Load_from_Newest_Slot->setEnabled(true);
if (savestate.time > newest_slot_time) {
newest_slot = savestate.slot;
newest_slot_time = savestate.time;
}
if (savestate.time < oldest_slot_time) {
oldest_slot = savestate.slot;
oldest_slot_time = savestate.time;
}
}
for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) {
if (!actions_load_state[i]->isEnabled()) {
// Prefer empty slot
oldest_slot = i + 1;
oldest_slot_time = 0;
break;
}
}
}
void GMainWindow::OnGameListLoadFile(QString game_path) {
if (ConfirmChangeGame()) {
BootGame(game_path);
}
}
void GMainWindow::OnGameListOpenFolder(u64 data_id, GameListOpenTarget target) {
std::string path;
std::string open_target;
switch (target) {
case GameListOpenTarget::SAVE_DATA: {
open_target = "Save Data";
std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir);
path = FileSys::ArchiveSource_SDSaveData::GetSaveDataPathFor(sdmc_dir, data_id);
break;
}
case GameListOpenTarget::EXT_DATA: {
open_target = "Extra Data";
std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir);
path = FileSys::GetExtDataPathFromId(sdmc_dir, data_id);
break;
}
case GameListOpenTarget::APPLICATION: {
open_target = "Application";
auto media_type = Service::AM::GetTitleMediaType(data_id);
path = Service::AM::GetTitlePath(media_type, data_id) + "content/";
break;
}
case GameListOpenTarget::UPDATE_DATA: {
open_target = "Update Data";
path = Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, data_id + 0xe00000000) +
"content/";
break;
}
case GameListOpenTarget::TEXTURE_DUMP: {
open_target = "Dumped Textures";
path = fmt::format("{}textures/{:016X}/",
FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), data_id);
break;
}
case GameListOpenTarget::TEXTURE_LOAD: {
open_target = "Custom Textures";
path = fmt::format("{}textures/{:016X}/",
FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), data_id);
break;
}
case GameListOpenTarget::MODS: {
open_target = "Mods";
path = fmt::format("{}mods/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir),
data_id);
break;
}
case GameListOpenTarget::DLC_DATA: {
open_target = "DLC Data";
path = fmt::format("{}Nintendo 3DS/00000000000000000000000000000000/"
"00000000000000000000000000000000/title/0004008c/{:08x}/content/",
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), data_id);
break;
}
case GameListOpenTarget::SHADER_CACHE: {
open_target = "Shader Cache";
path = FileUtil::GetUserPath(FileUtil::UserPath::ShaderDir);
break;
}
default:
LOG_ERROR(Frontend, "Unexpected target {}", static_cast<int>(target));
return;
}
QString qpath = QString::fromStdString(path);
QDir dir(qpath);
if (!dir.exists()) {
QMessageBox::critical(
this, tr("Error Opening %1 Folder").arg(QString::fromStdString(open_target)),
tr("Folder does not exist!"));
return;
}
LOG_INFO(Frontend, "Opening {} path for data_id={:016x}", open_target, data_id);
QDesktopServices::openUrl(QUrl::fromLocalFile(qpath));
}
void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id,
const CompatibilityList& compatibility_list) {
auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
QString directory;
if (it != compatibility_list.end())
directory = it->second.second;
QDesktopServices::openUrl(QUrl(QStringLiteral("https://citra-emu.org/game/") + directory));
}
void GMainWindow::OnGameListDumpRomFS(QString game_path, u64 program_id) {
auto* dialog = new QProgressDialog(tr("Dumping..."), tr("Cancel"), 0, 0, this);
dialog->setWindowModality(Qt::WindowModal);
dialog->setWindowFlags(dialog->windowFlags() &
~(Qt::WindowCloseButtonHint | Qt::WindowContextHelpButtonHint));
dialog->setCancelButton(nullptr);
dialog->setMinimumDuration(0);
dialog->setValue(0);
const auto base_path = fmt::format(
"{}romfs/{:016X}", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), program_id);
const auto update_path =
fmt::format("{}romfs/{:016X}", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir),
program_id | 0x0004000e00000000);
using FutureWatcher = QFutureWatcher<std::pair<Loader::ResultStatus, Loader::ResultStatus>>;
auto* future_watcher = new FutureWatcher(this);
connect(future_watcher, &FutureWatcher::finished, this,
[this, dialog, base_path, update_path, future_watcher] {
dialog->hide();
const auto& [base, update] = future_watcher->result();
if (base != Loader::ResultStatus::Success) {
QMessageBox::critical(
this, tr("Lime3DS"),
tr("Could not dump base RomFS.\nRefer to the log for details."));
return;
}
QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(base_path)));
if (update == Loader::ResultStatus::Success) {
QDesktopServices::openUrl(
QUrl::fromLocalFile(QString::fromStdString(update_path)));
}
});
auto future = QtConcurrent::run([game_path, base_path, update_path] {
std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(game_path.toStdString());
return std::make_pair(loader->DumpRomFS(base_path), loader->DumpUpdateRomFS(update_path));
});
future_watcher->setFuture(future);
}
void GMainWindow::OnGameListOpenDirectory(const QString& directory) {
QString path;
if (directory == QStringLiteral("INSTALLED")) {
path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) +
"Nintendo "
"3DS/00000000000000000000000000000000/"
"00000000000000000000000000000000/title/00040000");
} else if (directory == QStringLiteral("SYSTEM")) {
path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) +
"00000000000000000000000000000000/title/00040010");
} else {
path = directory;
}
if (!QFileInfo::exists(path)) {
QMessageBox::critical(this, tr("Error Opening %1").arg(path), tr("Folder does not exist!"));
return;
}
QDesktopServices::openUrl(QUrl::fromLocalFile(path));
}
void GMainWindow::OnGameListAddDirectory() {
const QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory"));
if (dir_path.isEmpty())
return;
UISettings::GameDir game_dir{dir_path, false, true};
if (!UISettings::values.game_dirs.contains(game_dir)) {
UISettings::values.game_dirs.append(game_dir);
game_list->PopulateAsync(UISettings::values.game_dirs);
} else {
LOG_WARNING(Frontend, "Selected directory is already in the game list");
}
}
void GMainWindow::OnGameListShowList(bool show) {
if (emulation_running && ui->action_Single_Window_Mode->isChecked())
return;
game_list->setVisible(show);
game_list_placeholder->setVisible(!show);
};
void GMainWindow::OnGameListOpenPerGameProperties(const QString& file) {
const auto loader = Loader::GetLoader(file.toStdString());
u64 title_id{};
if (!loader || loader->ReadProgramId(title_id) != Loader::ResultStatus::Success) {
QMessageBox::information(this, tr("Properties"),
tr("The game properties could not be loaded."));
return;
}
OpenPerGameConfiguration(title_id, file);
}
void GMainWindow::OnMenuLoadFile() {
const QString extensions = QStringLiteral("*.").append(
GameList::supported_file_extensions.join(QStringLiteral(" *.")));
const QString file_filter = tr("3DS Executable (%1);;All Files (*.*)",
"%1 is an identifier for the 3DS executable file extensions.")
.arg(extensions);
const QString filename = QFileDialog::getOpenFileName(
this, tr("Load File"), UISettings::values.roms_path, file_filter);
if (filename.isEmpty()) {
return;
}
UISettings::values.roms_path = QFileInfo(filename).path();
BootGame(filename);
}
void GMainWindow::OnMenuInstallCIA() {
QStringList filepaths = QFileDialog::getOpenFileNames(
this, tr("Load Files"), UISettings::values.roms_path,
tr("3DS Installation File (*.CIA*)") + QStringLiteral(";;") + tr("All Files (*.*)"));
if (filepaths.isEmpty()) {
return;
}
UISettings::values.roms_path = QFileInfo(filepaths[0]).path();
InstallCIA(filepaths);
}
void GMainWindow::OnMenuBootHomeMenu(u32 region) {
BootGame(QString::fromStdString(Core::GetHomeMenuNcchPath(region)));
}
void GMainWindow::InstallCIA(QStringList filepaths) {
ui->action_Install_CIA->setEnabled(false);
game_list->SetDirectoryWatcherEnabled(false);
progress_bar->show();
progress_bar->setMaximum(INT_MAX);
(void)QtConcurrent::run([&, filepaths] {
Service::AM::InstallStatus status;
const auto cia_progress = [&](std::size_t written, std::size_t total) {
emit UpdateProgress(written, total);
};
for (const auto& current_path : filepaths) {
status = Service::AM::InstallCIA(current_path.toStdString(), cia_progress);
emit CIAInstallReport(status, current_path);
}
emit CIAInstallFinished();
});
}
void GMainWindow::OnUpdateProgress(std::size_t written, std::size_t total) {
progress_bar->setValue(
static_cast<int>(INT_MAX * (static_cast<double>(written) / static_cast<double>(total))));
}
void GMainWindow::OnCIAInstallReport(Service::AM::InstallStatus status, QString filepath) {
QString filename = QFileInfo(filepath).fileName();
switch (status) {
case Service::AM::InstallStatus::Success:
this->statusBar()->showMessage(tr("%1 has been installed successfully.").arg(filename));
break;
case Service::AM::InstallStatus::ErrorFailedToOpenFile:
QMessageBox::critical(this, tr("Unable to open File"),
tr("Could not open %1").arg(filename));
break;
case Service::AM::InstallStatus::ErrorAborted:
QMessageBox::critical(
this, tr("Installation aborted"),
tr("The installation of %1 was aborted. Please see the log for more details")
.arg(filename));
break;
case Service::AM::InstallStatus::ErrorInvalid:
QMessageBox::critical(this, tr("Invalid File"), tr("%1 is not a valid CIA").arg(filename));
break;
case Service::AM::InstallStatus::ErrorEncrypted:
QMessageBox::critical(this, tr("Encrypted File"),
tr("%1 must be decrypted "
"before being used with Lime. A real 3DS is required.")
.arg(filename));
break;
case Service::AM::InstallStatus::ErrorFileNotFound:
QMessageBox::critical(this, tr("Unable to find File"),
tr("Could not find %1").arg(filename));
break;
}
}
void GMainWindow::OnCIAInstallFinished() {
progress_bar->hide();
progress_bar->setValue(0);
game_list->SetDirectoryWatcherEnabled(true);
ui->action_Install_CIA->setEnabled(true);
game_list->PopulateAsync(UISettings::values.game_dirs);
}
void GMainWindow::UninstallTitles(
const std::vector<std::tuple<Service::FS::MediaType, u64, QString>>& titles) {
if (titles.empty()) {
return;
}
// Select the first title in the list as representative.
const auto first_name = std::get<QString>(titles[0]);
QProgressDialog progress(tr("Uninstalling '%1'...").arg(first_name), tr("Cancel"), 0,
static_cast<int>(titles.size()), this);
progress.setWindowModality(Qt::WindowModal);
QFutureWatcher<void> future_watcher;
QObject::connect(&future_watcher, &QFutureWatcher<void>::finished, &progress,
&QProgressDialog::reset);
QObject::connect(&progress, &QProgressDialog::canceled, &future_watcher,
&QFutureWatcher<void>::cancel);
QObject::connect(&future_watcher, &QFutureWatcher<void>::progressValueChanged, &progress,
&QProgressDialog::setValue);
auto failed = false;
QString failed_name;
const auto uninstall_title = [&future_watcher, &failed, &failed_name](const auto& title) {
const auto name = std::get<QString>(title);
const auto media_type = std::get<Service::FS::MediaType>(title);
const auto program_id = std::get<u64>(title);
const auto result = Service::AM::UninstallProgram(media_type, program_id);
if (result.IsError()) {
LOG_ERROR(Frontend, "Failed to uninstall '{}': 0x{:08X}", name.toStdString(),
result.raw);
failed = true;
failed_name = name;
future_watcher.cancel();
}
};
future_watcher.setFuture(QtConcurrent::map(titles, uninstall_title));
progress.exec();
future_watcher.waitForFinished();
if (failed) {
QMessageBox::critical(this, tr("Lime3DS"),
tr("Failed to uninstall '%1'.").arg(failed_name));
} else if (!future_watcher.isCanceled()) {
QMessageBox::information(this, tr("Lime3DS"),
tr("Successfully uninstalled '%1'.").arg(first_name));
}
}
void GMainWindow::OnMenuRecentFile() {
QAction* action = qobject_cast<QAction*>(sender());
ASSERT(action);
const QString filename = action->data().toString();
if (QFileInfo::exists(filename)) {
BootGame(filename);
} else {
// Display an error message and remove the file from the list.
QMessageBox::information(this, tr("File not found"),
tr("File \"%1\" not found").arg(filename));
UISettings::values.recent_files.removeOne(filename);
UpdateRecentFiles();
}
}
void GMainWindow::OnStartGame() {
qt_cameras->ResumeCameras();
PreventOSSleep();
emu_thread->SetRunning(true);
graphics_api_button->setEnabled(false);
qRegisterMetaType<Core::System::ResultStatus>("Core::System::ResultStatus");
qRegisterMetaType<std::string>("std::string");
connect(emu_thread.get(), &EmuThread::ErrorThrown, this, &GMainWindow::OnCoreError);
UpdateMenuState();
discord_rpc->Update();
#ifdef __unix__
Common::Linux::StartGamemode();
#endif
UpdateSaveStates();
UpdateStatusButtons();
}
void GMainWindow::OnRestartGame() {
if (!system.IsPoweredOn()) {
return;
}
// Make a copy since BootGame edits game_path
BootGame(QString(game_path));
}
void GMainWindow::OnPauseGame() {
emu_thread->SetRunning(false);
qt_cameras->PauseCameras();
UpdateMenuState();
AllowOSSleep();
#ifdef __unix__
Common::Linux::StopGamemode();
#endif
}
void GMainWindow::OnPauseContinueGame() {
if (emulation_running) {
if (emu_thread->IsRunning()) {
OnPauseGame();
} else {
OnStartGame();
}
}
}
void GMainWindow::OnStopGame() {
ShutdownGame();
graphics_api_button->setEnabled(true);
Settings::RestoreGlobalState(false);
UpdateStatusButtons();
}
void GMainWindow::OnLoadComplete() {
loading_screen->OnLoadComplete();
UpdateSecondaryWindowVisibility();
}
void GMainWindow::OnMenuReportCompatibility() {
if (!NetSettings::values.citra_token.empty() && !NetSettings::values.citra_username.empty()) {
CompatDB compatdb{this};
compatdb.exec();
} else {
QMessageBox::critical(this, tr("Missing Citra Account"),
tr("You must link your Citra account to submit test cases."
"<br/>Go to Emulation &gt; Configure... &gt; Web to do so."));
}
}
void GMainWindow::ToggleFullscreen() {
if (!emulation_running) {
return;
}
if (ui->action_Fullscreen->isChecked()) {
ShowFullscreen();
} else {
HideFullscreen();
}
}
void GMainWindow::ToggleSecondaryFullscreen() {
if (!emulation_running) {
return;
}
if (secondary_window->isFullScreen()) {
secondary_window->showNormal();
} else {
secondary_window->showFullScreen();
}
}
void GMainWindow::ShowFullscreen() {
if (ui->action_Single_Window_Mode->isChecked()) {
UISettings::values.geometry = saveGeometry();
ui->menubar->hide();
statusBar()->hide();
showFullScreen();
} else {
UISettings::values.renderwindow_geometry = render_window->saveGeometry();
render_window->showFullScreen();
}
}
void GMainWindow::HideFullscreen() {
if (ui->action_Single_Window_Mode->isChecked()) {
statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked());
ui->menubar->show();
showNormal();
restoreGeometry(UISettings::values.geometry);
} else {
render_window->showNormal();
render_window->restoreGeometry(UISettings::values.renderwindow_geometry);
}
}
void GMainWindow::ToggleWindowMode() {
if (ui->action_Single_Window_Mode->isChecked()) {
// Render in the main window...
render_window->BackupGeometry();
ui->horizontalLayout->addWidget(render_window);
render_window->setFocusPolicy(Qt::StrongFocus);
if (emulation_running) {
render_window->setVisible(true);
render_window->setFocus();
game_list->hide();
}
} else {
// Render in a separate window...
ui->horizontalLayout->removeWidget(render_window);
render_window->setParent(nullptr);
render_window->setFocusPolicy(Qt::NoFocus);
if (emulation_running) {
render_window->setVisible(true);
render_window->RestoreGeometry();
game_list->show();
}
}
}
void GMainWindow::UpdateSecondaryWindowVisibility() {
if (!emulation_running) {
return;
}
if (Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows) {
secondary_window->RestoreGeometry();
secondary_window->show();
} else {
secondary_window->BackupGeometry();
secondary_window->hide();
}
}
void GMainWindow::ChangeScreenLayout() {
Settings::LayoutOption new_layout = Settings::LayoutOption::Default;
if (ui->action_Screen_Layout_Default->isChecked()) {
new_layout = Settings::LayoutOption::Default;
} else if (ui->action_Screen_Layout_Single_Screen->isChecked()) {
new_layout = Settings::LayoutOption::SingleScreen;
} else if (ui->action_Screen_Layout_Large_Screen->isChecked()) {
new_layout = Settings::LayoutOption::LargeScreen;
} else if (ui->action_Screen_Layout_Hybrid_Screen->isChecked()) {
new_layout = Settings::LayoutOption::HybridScreen;
} else if (ui->action_Screen_Layout_Side_by_Side->isChecked()) {
new_layout = Settings::LayoutOption::SideScreen;
} else if (ui->action_Screen_Layout_Separate_Windows->isChecked()) {
new_layout = Settings::LayoutOption::SeparateWindows;
}
Settings::values.layout_option = new_layout;
system.ApplySettings();
UpdateSecondaryWindowVisibility();
}
void GMainWindow::ToggleScreenLayout() {
const Settings::LayoutOption new_layout = []() {
switch (Settings::values.layout_option.GetValue()) {
case Settings::LayoutOption::Default:
return Settings::LayoutOption::SingleScreen;
case Settings::LayoutOption::SingleScreen:
return Settings::LayoutOption::LargeScreen;
case Settings::LayoutOption::LargeScreen:
return Settings::LayoutOption::HybridScreen;
case Settings::LayoutOption::HybridScreen:
return Settings::LayoutOption::SideScreen;
case Settings::LayoutOption::SideScreen:
return Settings::LayoutOption::SeparateWindows;
case Settings::LayoutOption::SeparateWindows:
return Settings::LayoutOption::Default;
default:
LOG_ERROR(Frontend, "Unknown layout option {}",
Settings::values.layout_option.GetValue());
return Settings::LayoutOption::Default;
}
}();
Settings::values.layout_option = new_layout;
SyncMenuUISettings();
system.ApplySettings();
UpdateSecondaryWindowVisibility();
}
void GMainWindow::OnSwapScreens() {
Settings::values.swap_screen = ui->action_Screen_Layout_Swap_Screens->isChecked();
system.ApplySettings();
}
void GMainWindow::OnRotateScreens() {
Settings::values.upright_screen = ui->action_Screen_Layout_Upright_Screens->isChecked();
system.ApplySettings();
}
void GMainWindow::TriggerSwapScreens() {
ui->action_Screen_Layout_Swap_Screens->trigger();
}
void GMainWindow::TriggerRotateScreens() {
ui->action_Screen_Layout_Upright_Screens->trigger();
}
void GMainWindow::OnSaveState() {
QAction* action = qobject_cast<QAction*>(sender());
ASSERT(action);
system.SendSignal(Core::System::Signal::Save, action->data().toUInt());
system.frame_limiter.AdvanceFrame();
newest_slot = action->data().toUInt();
}
void GMainWindow::OnLoadState() {
QAction* action = qobject_cast<QAction*>(sender());
ASSERT(action);
if (UISettings::values.save_state_warning) {
QMessageBox::warning(this, tr("Savestates"),
tr("Warning: Savestates are NOT a replacement for in-game saves, "
"and are not meant to be reliable.\n\nUse at your own risk!"));
UISettings::values.save_state_warning = false;
config->Save();
}
system.SendSignal(Core::System::Signal::Load, action->data().toUInt());
system.frame_limiter.AdvanceFrame();
}
void GMainWindow::OnConfigure() {
game_list->SetDirectoryWatcherEnabled(false);
Settings::SetConfiguringGlobal(true);
ConfigureDialog configureDialog(this, hotkey_registry, system, gl_renderer, physical_devices,
!multiplayer_state->IsHostingPublicRoom());
connect(&configureDialog, &ConfigureDialog::LanguageChanged, this,
&GMainWindow::OnLanguageChanged);
auto old_theme = UISettings::values.theme;
const int old_input_profile_index = Settings::values.current_input_profile_index;
const auto old_input_profiles = Settings::values.input_profiles;
const auto old_touch_from_button_maps = Settings::values.touch_from_button_maps;
const bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue();
#ifdef __unix__
const bool old_gamemode = Settings::values.enable_gamemode.GetValue();
#endif
auto result = configureDialog.exec();
game_list->SetDirectoryWatcherEnabled(true);
if (result == QDialog::Accepted) {
configureDialog.ApplyConfiguration();
InitializeHotkeys();
if (UISettings::values.theme != old_theme) {
UpdateUITheme();
}
if (UISettings::values.enable_discord_presence.GetValue() != old_discord_presence) {
SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue());
}
#ifdef __unix__
if (Settings::values.enable_gamemode.GetValue() != old_gamemode) {
SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue());
}
#endif
if (!multiplayer_state->IsHostingPublicRoom())
multiplayer_state->UpdateCredentials();
emit UpdateThemedIcons();
SyncMenuUISettings();
game_list->RefreshGameDirectory();
config->Save();
if (UISettings::values.hide_mouse && emulation_running) {
setMouseTracking(true);
mouse_hide_timer.start();
} else {
setMouseTracking(false);
}
UpdateSecondaryWindowVisibility();
UpdateBootHomeMenuState();
UpdateStatusButtons();
} else {
Settings::values.input_profiles = old_input_profiles;
Settings::values.touch_from_button_maps = old_touch_from_button_maps;
Settings::LoadProfile(old_input_profile_index);
}
}
void GMainWindow::OnLoadAmiibo() {
if (!emu_thread || !emu_thread->IsRunning()) [[unlikely]] {
return;
}
Service::SM::ServiceManager& sm = system.ServiceManager();
auto nfc = sm.GetService<Service::NFC::Module::Interface>("nfc:u");
if (nfc == nullptr) {
return;
}
std::scoped_lock lock{system.Kernel().GetHLELock()};
if (nfc->IsTagActive()) {
QMessageBox::warning(this, tr("Error opening amiibo data file"),
tr("A tag is already in use."));
return;
}
if (!nfc->IsSearchingForAmiibos()) {
QMessageBox::warning(this, tr("Error opening amiibo data file"),
tr("Game is not looking for amiibos."));
return;
}
const QString extensions{QStringLiteral("*.bin")};
const QString file_filter = tr("Amiibo File (%1);; All Files (*.*)").arg(extensions);
const QString filename = QFileDialog::getOpenFileName(this, tr("Load Amiibo"), {}, file_filter);
if (filename.isEmpty()) {
return;
}
LoadAmiibo(filename);
}
void GMainWindow::LoadAmiibo(const QString& filename) {
Service::SM::ServiceManager& sm = system.ServiceManager();
auto nfc = sm.GetService<Service::NFC::Module::Interface>("nfc:u");
if (!nfc) [[unlikely]] {
return;
}
std::scoped_lock lock{system.Kernel().GetHLELock()};
if (!nfc->LoadAmiibo(filename.toStdString())) {
QMessageBox::warning(this, tr("Error opening amiibo data file"),
tr("Unable to open amiibo file \"%1\" for reading.").arg(filename));
return;
}
ui->action_Remove_Amiibo->setEnabled(true);
}
void GMainWindow::OnRemoveAmiibo() {
Service::SM::ServiceManager& sm = system.ServiceManager();
auto nfc = sm.GetService<Service::NFC::Module::Interface>("nfc:u");
if (!nfc) [[unlikely]] {
return;
}
std::scoped_lock lock{system.Kernel().GetHLELock()};
nfc->RemoveAmiibo();
ui->action_Remove_Amiibo->setEnabled(false);
}
void GMainWindow::OnOpenCitraFolder() {
QDesktopServices::openUrl(QUrl::fromLocalFile(
QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::UserDir))));
}
void GMainWindow::OnToggleFilterBar() {
game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked());
if (ui->action_Show_Filter_Bar->isChecked()) {
game_list->SetFilterFocus();
} else {
game_list->ClearFilter();
}
}
void GMainWindow::OnCreateGraphicsSurfaceViewer() {
auto graphicsSurfaceViewerWidget =
new GraphicsSurfaceWidget(system, Pica::g_debug_context, this);
addDockWidget(Qt::RightDockWidgetArea, graphicsSurfaceViewerWidget);
// TODO: Maybe graphicsSurfaceViewerWidget->setFloating(true);
graphicsSurfaceViewerWidget->show();
}
void GMainWindow::OnRecordMovie() {
MovieRecordDialog dialog(this, system);
if (dialog.exec() != QDialog::Accepted) {
return;
}
movie_record_on_start = true;
movie_record_path = dialog.GetPath();
movie_record_author = dialog.GetAuthor();
if (emulation_running) { // Restart game
BootGame(QString(game_path));
}
ui->action_Close_Movie->setEnabled(true);
ui->action_Save_Movie->setEnabled(true);
}
void GMainWindow::OnPlayMovie() {
MoviePlayDialog dialog(this, game_list, system);
if (dialog.exec() != QDialog::Accepted) {
return;
}
movie_playback_on_start = true;
movie_playback_path = dialog.GetMoviePath();
BootGame(dialog.GetGamePath());
ui->action_Close_Movie->setEnabled(true);
ui->action_Save_Movie->setEnabled(false);
}
void GMainWindow::OnCloseMovie() {
if (movie_record_on_start) {
QMessageBox::information(this, tr("Record Movie"), tr("Movie recording cancelled."));
movie_record_on_start = false;
movie_record_path.clear();
movie_record_author.clear();
} else {
const bool was_running = emu_thread && emu_thread->IsRunning();
if (was_running) {
OnPauseGame();
}
const bool was_recording = movie.GetPlayMode() == Core::Movie::PlayMode::Recording;
movie.Shutdown();
if (was_recording) {
QMessageBox::information(this, tr("Movie Saved"),
tr("The movie is successfully saved."));
}
if (was_running) {
OnStartGame();
}
}
ui->action_Close_Movie->setEnabled(false);
ui->action_Save_Movie->setEnabled(false);
}
void GMainWindow::OnSaveMovie() {
const bool was_running = emu_thread && emu_thread->IsRunning();
if (was_running) {
OnPauseGame();
}
if (movie.GetPlayMode() == Core::Movie::PlayMode::Recording) {
movie.SaveMovie();
QMessageBox::information(this, tr("Movie Saved"), tr("The movie is successfully saved."));
} else {
LOG_ERROR(Frontend, "Tried to save movie while movie is not being recorded");
}
if (was_running) {
OnStartGame();
}
}
void GMainWindow::OnCaptureScreenshot() {
if (!emu_thread || !emu_thread->IsRunning()) [[unlikely]] {
return;
}
OnPauseGame();
std::string path = UISettings::values.screenshot_path.GetValue();
if (!FileUtil::IsDirectory(path)) {
if (!FileUtil::CreateFullPath(path)) {
QMessageBox::information(this, tr("Invalid Screenshot Directory"),
tr("Cannot create specified screenshot directory. Screenshot "
"path is set back to its default value."));
path = FileUtil::GetUserPath(FileUtil::UserPath::UserDir);
path.append("screenshots/");
UISettings::values.screenshot_path = path;
};
}
static QRegularExpression expr(QStringLiteral("[\\/:?\"<>|]"));
const std::string filename = game_title.remove(expr).toStdString();
const std::string timestamp =
QDateTime::currentDateTime().toString(QStringLiteral("dd.MM.yy_hh.mm.ss.z")).toStdString();
path.append(fmt::format("/{}_{}.png", filename, timestamp));
auto* const screenshot_window = secondary_window->HasFocus() ? secondary_window : render_window;
screenshot_window->CaptureScreenshot(UISettings::values.screenshot_resolution_factor.GetValue(),
QString::fromStdString(path));
OnStartGame();
}
void GMainWindow::OnDumpVideo() {
if (DynamicLibrary::FFmpeg::LoadFFmpeg()) {
if (ui->action_Dump_Video->isChecked()) {
OnStartVideoDumping();
} else {
OnStopVideoDumping();
}
} else {
ui->action_Dump_Video->setChecked(false);
QMessageBox message_box;
message_box.setWindowTitle(tr("Could not load video dumper"));
message_box.setText(
tr("FFmpeg could not be loaded. Make sure you have a compatible version installed."
#ifdef _WIN32
"\n\nTo install FFmpeg to Lime, press Open and select your FFmpeg directory."
#endif
"\n\nTo view a guide on how to install FFmpeg, press Help."));
message_box.setStandardButtons(QMessageBox::Ok | QMessageBox::Help
#ifdef _WIN32
| QMessageBox::Open
#endif
);
auto result = message_box.exec();
if (result == QMessageBox::Help) {
QDesktopServices::openUrl(QUrl(QStringLiteral(
"https://citra-emu.org/wiki/installing-ffmpeg-for-the-video-dumper/")));
#ifdef _WIN32
} else if (result == QMessageBox::Open) {
OnOpenFFmpeg();
#endif
}
}
}
#ifdef _WIN32
void GMainWindow::OnOpenFFmpeg() {
auto filename =
QFileDialog::getExistingDirectory(this, tr("Select FFmpeg Directory")).toStdString();
if (filename.empty()) {
return;
}
// Check for a bin directory if they chose the FFmpeg root directory.
auto bin_dir = filename + DIR_SEP + "bin";
if (!FileUtil::Exists(bin_dir)) {
// Otherwise, assume the user directly selected the directory containing the DLLs.
bin_dir = filename;
}
static const std::array library_names = {
Common::DynamicLibrary::GetLibraryName("avcodec", LIBAVCODEC_VERSION_MAJOR),
Common::DynamicLibrary::GetLibraryName("avfilter", LIBAVFILTER_VERSION_MAJOR),
Common::DynamicLibrary::GetLibraryName("avformat", LIBAVFORMAT_VERSION_MAJOR),
Common::DynamicLibrary::GetLibraryName("avutil", LIBAVUTIL_VERSION_MAJOR),
Common::DynamicLibrary::GetLibraryName("swresample", LIBSWRESAMPLE_VERSION_MAJOR),
};
for (auto& library_name : library_names) {
if (!FileUtil::Exists(bin_dir + DIR_SEP + library_name)) {
QMessageBox::critical(this, tr("Lime3DS"),
tr("The provided FFmpeg directory is missing %1. Please make "
"sure the correct directory was selected.")
.arg(QString::fromStdString(library_name)));
return;
}
}
std::atomic<bool> success(true);
auto process_file = [&success](u64* num_entries_out, const std::string& directory,
const std::string& virtual_name) -> bool {
auto file_path = directory + DIR_SEP + virtual_name;
if (file_path.ends_with(".dll")) {
auto destination_path = FileUtil::GetExeDirectory() + DIR_SEP + virtual_name;
if (!FileUtil::Copy(file_path, destination_path)) {
success.store(false);
return false;
}
}
return true;
};
FileUtil::ForeachDirectoryEntry(nullptr, bin_dir, process_file);
if (success.load()) {
QMessageBox::information(this, tr("Lime3DS"), tr("FFmpeg has been sucessfully installed."));
} else {
QMessageBox::critical(this, tr("Lime3DS"),
tr("Installation of FFmpeg failed. Check the log file for details."));
}
}
#endif
void GMainWindow::OnStartVideoDumping() {
DumpingDialog dialog(this, system);
if (dialog.exec() != QDialog::DialogCode::Accepted) {
ui->action_Dump_Video->setChecked(false);
return;
}
const auto path = dialog.GetFilePath();
if (emulation_running) {
StartVideoDumping(path);
} else {
video_dumping_on_start = true;
video_dumping_path = path;
}
}
void GMainWindow::StartVideoDumping(const QString& path) {
auto& renderer = system.GPU().Renderer();
const auto layout{Layout::FrameLayoutFromResolutionScale(renderer.GetResolutionScaleFactor())};
auto dumper = std::make_shared<VideoDumper::FFmpegBackend>(renderer);
if (dumper->StartDumping(path.toStdString(), layout)) {
system.RegisterVideoDumper(dumper);
} else {
QMessageBox::critical(
this, tr("Lime3DS"),
tr("Could not start video dumping.<br>Refer to the log for details."));
ui->action_Dump_Video->setChecked(false);
}
}
void GMainWindow::OnStopVideoDumping() {
ui->action_Dump_Video->setChecked(false);
if (video_dumping_on_start) {
video_dumping_on_start = false;
video_dumping_path.clear();
} else {
auto dumper = system.GetVideoDumper();
if (!dumper || !dumper->IsDumping()) {
return;
}
game_paused_for_dumping = emu_thread->IsRunning();
OnPauseGame();
auto future = QtConcurrent::run([dumper] { dumper->StopDumping(); });
auto* future_watcher = new QFutureWatcher<void>(this);
connect(future_watcher, &QFutureWatcher<void>::finished, this, [this] {
if (game_shutdown_delayed) {
game_shutdown_delayed = false;
ShutdownGame();
} else if (game_paused_for_dumping) {
game_paused_for_dumping = false;
OnStartGame();
}
});
future_watcher->setFuture(future);
}
}
void GMainWindow::UpdateStatusBar() {
if (!emu_thread) [[unlikely]] {
status_bar_update_timer.stop();
return;
}
// Update movie status
const u64 current = movie.GetCurrentInputIndex();
const u64 total = movie.GetTotalInputCount();
const auto play_mode = movie.GetPlayMode();
if (play_mode == Core::Movie::PlayMode::Recording) {
message_label->setText(tr("Recording %1").arg(current));
message_label_used_for_movie = true;
ui->action_Save_Movie->setEnabled(true);
} else if (play_mode == Core::Movie::PlayMode::Playing) {
message_label->setText(tr("Playing %1 / %2").arg(current).arg(total));
message_label_used_for_movie = true;
ui->action_Save_Movie->setEnabled(false);
} else if (play_mode == Core::Movie::PlayMode::MovieFinished) {
message_label->setText(tr("Movie Finished"));
message_label_used_for_movie = true;
ui->action_Save_Movie->setEnabled(false);
} else if (message_label_used_for_movie) { // Clear the label if movie was just closed
message_label->setText(QString{});
message_label_used_for_movie = false;
ui->action_Save_Movie->setEnabled(false);
}
auto results = system.GetAndResetPerfStats();
if (Settings::values.frame_limit.GetValue() == 0) {
emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0));
} else {
emu_speed_label->setText(tr("Speed: %1% / %2%")
.arg(results.emulation_speed * 100.0, 0, 'f', 0)
.arg(Settings::values.frame_limit.GetValue()));
}
game_fps_label->setText(tr("Game: %1 FPS").arg(results.game_fps, 0, 'f', 0));
emu_frametime_label->setText(tr("Frame: %1 ms").arg(results.frametime * 1000.0, 0, 'f', 2));
emu_speed_label->setVisible(true);
game_fps_label->setVisible(true);
emu_frametime_label->setVisible(true);
}
void GMainWindow::UpdateBootHomeMenuState() {
const auto current_region = Settings::values.region_value.GetValue();
for (u32 region = 0; region < Core::NUM_SYSTEM_TITLE_REGIONS; region++) {
const auto path = Core::GetHomeMenuNcchPath(region);
ui->menu_Boot_Home_Menu->actions().at(region)->setEnabled(
(current_region == Settings::REGION_VALUE_AUTO_SELECT ||
current_region == static_cast<int>(region)) &&
!path.empty() && FileUtil::Exists(path));
}
}
void GMainWindow::HideMouseCursor() {
if (!emu_thread || !UISettings::values.hide_mouse.GetValue()) {
mouse_hide_timer.stop();
ShowMouseCursor();
return;
}
render_window->setCursor(QCursor(Qt::BlankCursor));
secondary_window->setCursor(QCursor(Qt::BlankCursor));
if (UISettings::values.single_window_mode.GetValue()) {
setCursor(QCursor(Qt::BlankCursor));
}
}
void GMainWindow::ShowMouseCursor() {
unsetCursor();
render_window->unsetCursor();
secondary_window->unsetCursor();
if (emu_thread && UISettings::values.hide_mouse) {
mouse_hide_timer.start();
}
}
void GMainWindow::OnMute() {
Settings::values.audio_muted = !Settings::values.audio_muted;
UpdateVolumeUI();
}
void GMainWindow::OnDecreaseVolume() {
Settings::values.audio_muted = false;
const auto current_volume =
static_cast<s32>(Settings::values.volume.GetValue() * volume_slider->maximum());
int step = 5;
if (current_volume <= 30) {
step = 2;
}
if (current_volume <= 6) {
step = 1;
}
const auto value =
static_cast<float>(std::max(current_volume - step, 0)) / volume_slider->maximum();
Settings::values.volume.SetValue(value);
UpdateVolumeUI();
}
void GMainWindow::OnIncreaseVolume() {
Settings::values.audio_muted = false;
const auto current_volume =
static_cast<s32>(Settings::values.volume.GetValue() * volume_slider->maximum());
int step = 5;
if (current_volume < 30) {
step = 2;
}
if (current_volume < 6) {
step = 1;
}
const auto value = static_cast<float>(current_volume + step) / volume_slider->maximum();
Settings::values.volume.SetValue(value);
UpdateVolumeUI();
}
void GMainWindow::UpdateVolumeUI() {
const auto volume_value =
static_cast<int>(Settings::values.volume.GetValue() * volume_slider->maximum());
volume_slider->setValue(volume_value);
if (Settings::values.audio_muted) {
volume_button->setChecked(false);
volume_button->setText(tr("VOLUME: MUTE"));
} else {
volume_button->setChecked(true);
volume_button->setText(tr("VOLUME: %1%", "Volume percentage (e.g. 50%)").arg(volume_value));
}
}
void GMainWindow::UpdateAPIIndicator(bool update) {
static std::array graphics_apis = {QStringLiteral("SOFTWARE"), QStringLiteral("OPENGL"),
QStringLiteral("VULKAN")};
static std::array graphics_api_colors = {QStringLiteral("#3ae400"), QStringLiteral("#00ccdd"),
QStringLiteral("#91242a")};
u32 api_index = static_cast<u32>(Settings::values.graphics_api.GetValue());
if (update) {
api_index = (api_index + 1) % graphics_apis.size();
// Skip past any disabled renderers.
#ifndef ENABLE_SOFTWARE_RENDERER
if (api_index == static_cast<u32>(Settings::GraphicsAPI::Software)) {
api_index = (api_index + 1) % graphics_apis.size();
}
#endif
#ifndef ENABLE_OPENGL
if (api_index == static_cast<u32>(Settings::GraphicsAPI::OpenGL)) {
api_index = (api_index + 1) % graphics_apis.size();
}
#endif
#ifndef ENABLE_VULKAN
if (api_index == static_cast<u32>(Settings::GraphicsAPI::Vulkan)) {
api_index = (api_index + 1) % graphics_apis.size();
}
#endif
Settings::values.graphics_api = static_cast<Settings::GraphicsAPI>(api_index);
}
const QString style_sheet = QStringLiteral("QPushButton { font-weight: bold; color: %0; }")
.arg(graphics_api_colors[api_index]);
graphics_api_button->setText(graphics_apis[api_index]);
graphics_api_button->setStyleSheet(style_sheet);
}
void GMainWindow::UpdateStatusButtons() {
UpdateAPIIndicator();
UpdateVolumeUI();
}
void GMainWindow::OnMouseActivity() {
ShowMouseCursor();
}
void GMainWindow::mouseMoveEvent([[maybe_unused]] QMouseEvent* event) {
OnMouseActivity();
}
void GMainWindow::mousePressEvent([[maybe_unused]] QMouseEvent* event) {
OnMouseActivity();
}
void GMainWindow::mouseReleaseEvent([[maybe_unused]] QMouseEvent* event) {
OnMouseActivity();
}
void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string details) {
QString status_message;
QString title, message;
QMessageBox::Icon error_severity_icon;
if (result == Core::System::ResultStatus::ErrorSystemFiles) {
const QString common_message =
tr("%1 is missing. Please <a "
"href='https://citra-emu.org/wiki/"
"dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your "
"system archives</a>.<br/>Continuing emulation may result in crashes and bugs.");
if (!details.empty()) {
message = common_message.arg(QString::fromStdString(details));
} else {
message = common_message.arg(tr("A system archive"));
}
title = tr("System Archive Not Found");
status_message = tr("System Archive Missing");
error_severity_icon = QMessageBox::Icon::Critical;
} else if (result == Core::System::ResultStatus::ErrorSavestate) {
title = tr("Save/load Error");
message = QString::fromStdString(details);
error_severity_icon = QMessageBox::Icon::Warning;
} else {
title = tr("Fatal Error");
message =
tr("A fatal error occurred. "
"<a href='https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check "
"the log</a> for details."
"<br/>Continuing emulation may result in crashes and bugs.");
status_message = tr("Fatal Error encountered");
error_severity_icon = QMessageBox::Icon::Critical;
}
QMessageBox message_box;
message_box.setWindowTitle(title);
message_box.setText(message);
message_box.setIcon(error_severity_icon);
if (error_severity_icon == QMessageBox::Icon::Critical) {
message_box.addButton(tr("Continue"), QMessageBox::RejectRole);
QPushButton* abort_button = message_box.addButton(tr("Quit Game"), QMessageBox::AcceptRole);
if (result != Core::System::ResultStatus::ShutdownRequested)
message_box.exec();
if (result == Core::System::ResultStatus::ShutdownRequested ||
message_box.clickedButton() == abort_button) {
if (emu_thread) {
ShutdownGame();
return;
}
}
} else {
// This block should run when the error isn't too big of a deal
// e.g. when a save state can't be saved or loaded
message_box.addButton(tr("OK"), QMessageBox::RejectRole);
message_box.exec();
}
// Only show the message if the game is still running.
if (emu_thread) {
emu_thread->SetRunning(true);
message_label->setText(status_message);
message_label_used_for_movie = false;
}
}
void GMainWindow::OnMenuAboutCitra() {
AboutDialog about{this};
about.exec();
}
bool GMainWindow::ConfirmClose() {
if (!emu_thread || !UISettings::values.confirm_before_closing) {
return true;
}
QMessageBox::StandardButton answer =
QMessageBox::question(this, tr("Lime3DS"), tr("Would you like to exit now?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
return answer != QMessageBox::No;
}
void GMainWindow::closeEvent(QCloseEvent* event) {
if (!ConfirmClose()) {
event->ignore();
return;
}
UpdateUISettings();
game_list->SaveInterfaceLayout();
hotkey_registry.SaveHotkeys();
// Shutdown session if the emu thread is active...
if (emu_thread) {
ShutdownGame();
}
render_window->close();
secondary_window->close();
multiplayer_state->Close();
InputCommon::Shutdown();
QWidget::closeEvent(event);
}
static bool IsSingleFileDropEvent(const QMimeData* mime) {
return mime->hasUrls() && mime->urls().length() == 1;
}
static const std::array<std::string, 8> AcceptedExtensions = {"cci", "3ds", "cxi", "bin",
"3dsx", "app", "elf", "axf"};
static bool IsCorrectFileExtension(const QMimeData* mime) {
const QString& filename = mime->urls().at(0).toLocalFile();
return std::find(AcceptedExtensions.begin(), AcceptedExtensions.end(),
QFileInfo(filename).suffix().toStdString()) != AcceptedExtensions.end();
}
static bool IsAcceptableDropEvent(QDropEvent* event) {
return IsSingleFileDropEvent(event->mimeData()) && IsCorrectFileExtension(event->mimeData());
}
void GMainWindow::AcceptDropEvent(QDropEvent* event) {
if (IsAcceptableDropEvent(event)) {
event->setDropAction(Qt::DropAction::LinkAction);
event->accept();
}
}
bool GMainWindow::DropAction(QDropEvent* event) {
if (!IsAcceptableDropEvent(event)) {
return false;
}
const QMimeData* mime_data = event->mimeData();
const QString& filename = mime_data->urls().at(0).toLocalFile();
if (emulation_running && QFileInfo(filename).suffix() == QStringLiteral("bin")) {
// Amiibo
LoadAmiibo(filename);
} else {
// Game
if (ConfirmChangeGame()) {
BootGame(filename);
}
}
return true;
}
void GMainWindow::OnFileOpen(const QFileOpenEvent* event) {
BootGame(event->file());
}
void GMainWindow::dropEvent(QDropEvent* event) {
DropAction(event);
}
void GMainWindow::dragEnterEvent(QDragEnterEvent* event) {
AcceptDropEvent(event);
}
void GMainWindow::dragMoveEvent(QDragMoveEvent* event) {
AcceptDropEvent(event);
}
bool GMainWindow::ConfirmChangeGame() {
if (!emu_thread) [[unlikely]] {
return true;
}
auto answer = QMessageBox::question(
this, tr("Lime3DS"), tr("The game is still running. Would you like to stop emulation?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
return answer != QMessageBox::No;
}
void GMainWindow::filterBarSetChecked(bool state) {
ui->action_Show_Filter_Bar->setChecked(state);
emit(OnToggleFilterBar());
}
void GMainWindow::UpdateUITheme() {
const QString icons_base_path = QStringLiteral(":/icons/");
const QString default_theme = QStringLiteral("default");
const QString default_theme_path = icons_base_path + default_theme;
const QString& current_theme = UISettings::values.theme;
const bool is_default_theme = current_theme == QString::fromUtf8(UISettings::themes[0].second);
QStringList theme_paths(default_theme_paths);
if (is_default_theme || current_theme.isEmpty()) {
const QString theme_uri(QStringLiteral(":default/style.qss"));
QFile f(theme_uri);
if (f.open(QFile::ReadOnly | QFile::Text)) {
QTextStream ts(&f);
qApp->setStyleSheet(ts.readAll());
setStyleSheet(ts.readAll());
} else {
LOG_ERROR(Frontend,
"Unable to open default stylesheet, falling back to empty stylesheet");
qApp->setStyleSheet({});
setStyleSheet({});
}
theme_paths.append(default_theme_path);
QIcon::setThemeName(default_theme);
} else {
const QString theme_uri(QLatin1Char{':'} + current_theme + QStringLiteral("/style.qss"));
QFile f(theme_uri);
if (f.open(QFile::ReadOnly | QFile::Text)) {
QTextStream ts(&f);
qApp->setStyleSheet(ts.readAll());
setStyleSheet(ts.readAll());
} else {
LOG_ERROR(Frontend, "Unable to set style, stylesheet file not found");
}
const QString current_theme_path = icons_base_path + current_theme;
theme_paths.append({default_theme_path, current_theme_path});
QIcon::setThemeName(current_theme);
}
QIcon::setThemeSearchPaths(theme_paths);
}
void GMainWindow::LoadTranslation() {
// If the selected language is English, no need to install any translation
if (UISettings::values.language == QStringLiteral("en")) {
return;
}
bool loaded;
if (UISettings::values.language.isEmpty()) {
// If the selected language is empty, use system locale
loaded = translator.load(QLocale(), {}, {}, QStringLiteral(":/languages/"));
} else {
// Otherwise load from the specified file
loaded = translator.load(UISettings::values.language, QStringLiteral(":/languages/"));
}
if (loaded) {
qApp->installTranslator(&translator);
} else {
UISettings::values.language = QStringLiteral("en");
}
}
void GMainWindow::OnLanguageChanged(const QString& locale) {
if (UISettings::values.language != QStringLiteral("en")) {
qApp->removeTranslator(&translator);
}
UISettings::values.language = locale;
LoadTranslation();
ui->retranslateUi(this);
RetranslateStatusBar();
UpdateWindowTitle();
}
void GMainWindow::OnConfigurePerGame() {
u64 title_id{};
system.GetAppLoader().ReadProgramId(title_id);
OpenPerGameConfiguration(title_id, game_path);
}
void GMainWindow::OpenPerGameConfiguration(u64 title_id, const QString& file_name) {
Settings::SetConfiguringGlobal(false);
ConfigurePerGame dialog(this, title_id, file_name, gl_renderer, physical_devices, system);
const auto result = dialog.exec();
if (result != QDialog::Accepted) {
Settings::RestoreGlobalState(system.IsPoweredOn());
return;
} else if (result == QDialog::Accepted) {
dialog.ApplyConfiguration();
}
// Do not cause the global config to write local settings into the config file
const bool is_powered_on = system.IsPoweredOn();
Settings::RestoreGlobalState(system.IsPoweredOn());
if (!is_powered_on) {
config->Save();
}
UpdateStatusButtons();
}
void GMainWindow::OnMoviePlaybackCompleted() {
OnPauseGame();
QMessageBox::information(this, tr("Playback Completed"), tr("Movie playback completed."));
}
void GMainWindow::UpdateWindowTitle() {
const QString full_name = QString::fromUtf8(Common::g_build_fullname);
if (game_title_long.isEmpty()) {
setWindowTitle(QStringLiteral("Lime3DS %1").arg(full_name));
} else {
setWindowTitle(QStringLiteral("Lime3DS %1 | %2").arg(full_name, game_title_long));
render_window->setWindowTitle(QStringLiteral("Lime3DS %1 | %2 | %3")
.arg(full_name, game_title_long, tr("Primary Window")));
secondary_window->setWindowTitle(
QStringLiteral("Lime3DS %1 | %2 | %3")
.arg(full_name, game_title_long, tr("Secondary Window")));
}
}
void GMainWindow::UpdateUISettings() {
if (!ui->action_Fullscreen->isChecked()) {
UISettings::values.geometry = saveGeometry();
UISettings::values.renderwindow_geometry = render_window->saveGeometry();
}
UISettings::values.state = saveState();
#if MICROPROFILE_ENABLED
UISettings::values.microprofile_geometry = microProfileDialog->saveGeometry();
UISettings::values.microprofile_visible = microProfileDialog->isVisible();
#endif
UISettings::values.single_window_mode = ui->action_Single_Window_Mode->isChecked();
UISettings::values.fullscreen = ui->action_Fullscreen->isChecked();
UISettings::values.display_titlebar = ui->action_Display_Dock_Widget_Headers->isChecked();
UISettings::values.show_filter_bar = ui->action_Show_Filter_Bar->isChecked();
UISettings::values.show_status_bar = ui->action_Show_Status_Bar->isChecked();
UISettings::values.first_start = false;
}
void GMainWindow::SyncMenuUISettings() {
ui->action_Screen_Layout_Default->setChecked(Settings::values.layout_option.GetValue() ==
Settings::LayoutOption::Default);
ui->action_Screen_Layout_Single_Screen->setChecked(Settings::values.layout_option.GetValue() ==
Settings::LayoutOption::SingleScreen);
ui->action_Screen_Layout_Large_Screen->setChecked(Settings::values.layout_option.GetValue() ==
Settings::LayoutOption::LargeScreen);
ui->action_Screen_Layout_Hybrid_Screen->setChecked(Settings::values.layout_option.GetValue() ==
Settings::LayoutOption::HybridScreen);
ui->action_Screen_Layout_Side_by_Side->setChecked(Settings::values.layout_option.GetValue() ==
Settings::LayoutOption::SideScreen);
ui->action_Screen_Layout_Separate_Windows->setChecked(
Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows);
ui->action_Screen_Layout_Swap_Screens->setChecked(Settings::values.swap_screen.GetValue());
ui->action_Screen_Layout_Upright_Screens->setChecked(
Settings::values.upright_screen.GetValue());
}
void GMainWindow::RetranslateStatusBar() {
if (emu_thread)
UpdateStatusBar();
emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% "
"indicate emulation is running faster or slower than a 3DS."));
game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. "
"This will vary from game to game and scene to scene."));
emu_frametime_label->setToolTip(
tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For "
"full-speed emulation this should be at most 16.67 ms."));
multiplayer_state->retranslateUi();
}
void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) {
#ifdef USE_DISCORD_PRESENCE
if (state) {
discord_rpc = std::make_unique<DiscordRPC::DiscordImpl>(system);
} else {
discord_rpc = std::make_unique<DiscordRPC::NullImpl>();
}
#else
discord_rpc = std::make_unique<DiscordRPC::NullImpl>();
#endif
discord_rpc->Update();
}
#ifdef __unix__
void GMainWindow::SetGamemodeEnabled(bool state) {
if (emulation_running) {
Common::Linux::SetGamemodeState(state);
}
}
#endif
#ifdef main
#undef main
#endif
static Qt::HighDpiScaleFactorRoundingPolicy GetHighDpiRoundingPolicy() {
#ifdef _WIN32
// For Windows, we want to avoid scaling artifacts on fractional scaling ratios.
// This is done by setting the optimal scaling policy for the primary screen.
// Create a temporary QApplication.
int temp_argc = 0;
char** temp_argv = nullptr;
QApplication temp{temp_argc, temp_argv};
// Get the current screen geometry.
const QScreen* primary_screen = QGuiApplication::primaryScreen();
if (!primary_screen) {
return Qt::HighDpiScaleFactorRoundingPolicy::PassThrough;
}
const QRect screen_rect = primary_screen->geometry();
const qreal real_ratio = primary_screen->devicePixelRatio();
const qreal real_width = std::trunc(screen_rect.width() * real_ratio);
const qreal real_height = std::trunc(screen_rect.height() * real_ratio);
// Recommended minimum width and height for proper window fit.
// Any screen with a lower resolution than this will still have a scale of 1.
constexpr qreal minimum_width = 1350.0;
constexpr qreal minimum_height = 900.0;
const qreal width_ratio = std::max(1.0, real_width / minimum_width);
const qreal height_ratio = std::max(1.0, real_height / minimum_height);
// Get the lower of the 2 ratios and truncate, this is the maximum integer scale.
const qreal max_ratio = std::trunc(std::min(width_ratio, height_ratio));
if (max_ratio > real_ratio) {
return Qt::HighDpiScaleFactorRoundingPolicy::Round;
} else {
return Qt::HighDpiScaleFactorRoundingPolicy::Floor;
}
#else
// Other OSes should be better than Windows at fractional scaling.
return Qt::HighDpiScaleFactorRoundingPolicy::PassThrough;
#endif
}
int main(int argc, char* argv[]) {
Common::DetachedTasks detached_tasks;
MicroProfileOnThreadCreate("Frontend");
SCOPE_EXIT({ MicroProfileShutdown(); });
// Init settings params
QCoreApplication::setOrganizationName(QStringLiteral("Lime3DS team"));
QCoreApplication::setApplicationName(QStringLiteral("Lime3DS"));
auto rounding_policy = GetHighDpiRoundingPolicy();
QApplication::setHighDpiScaleFactorRoundingPolicy(rounding_policy);
#ifdef __APPLE__
auto bundle_dir = FileUtil::GetBundleDirectory();
if (bundle_dir) {
FileUtil::SetCurrentDir(bundle_dir.value() + "..");
}
#endif
#ifdef ENABLE_OPENGL
QCoreApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity);
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
#endif
QApplication app(argc, argv);
// Qt changes the locale and causes issues in float conversion using std::to_string() when
// generating shaders
setlocale(LC_ALL, "C");
auto& system{Core::System::GetInstance()};
// Register Qt image interface
system.RegisterImageInterface(std::make_shared<QtImageInterface>());
GMainWindow main_window(system);
// Register frontend applets
Frontend::RegisterDefaultApplets(system);
system.RegisterMiiSelector(std::make_shared<QtMiiSelector>(main_window));
system.RegisterSoftwareKeyboard(std::make_shared<QtKeyboard>(main_window));
#ifdef __APPLE__
// Register microphone permission check.
system.RegisterMicPermissionCheck(&AppleAuthorization::CheckAuthorizationForMicrophone);
#endif
main_window.show();
QObject::connect(&app, &QGuiApplication::applicationStateChanged, &main_window,
&GMainWindow::OnAppFocusStateChanged);
int result = app.exec();
detached_tasks.WaitForAllTasks();
return result;
}