citra/src/citra_qt/main.cpp

3030 lines
113 KiB
C++
Raw Normal View History

// Copyright 2014 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <clocale>
#include <memory>
2014-10-28 08:36:00 +01:00
#include <thread>
2014-04-01 04:26:50 +02:00
#include <QFileDialog>
2017-12-02 22:35:20 +01:00
#include <QFutureWatcher>
2020-08-17 23:17:42 +02:00
#include <QLabel>
#include <QMessageBox>
2019-09-14 12:28:39 +02:00
#include <QSysInfo>
#include <QtConcurrent/QtConcurrentMap>
2017-12-02 22:35:20 +01:00
#include <QtConcurrent/QtConcurrentRun>
#include <QtGui>
#include <QtWidgets>
#include <fmt/format.h>
2023-08-01 02:57:38 +02:00
#include "core/telemetry_session.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>
#endif
#include "citra_qt/aboutdialog.h"
#include "citra_qt/applets/mii_selector.h"
#include "citra_qt/applets/swkbd.h"
2015-09-11 06:23:00 +02:00
#include "citra_qt/bootmanager.h"
citra_qt: camera integration (#3566) * Implement camera feature * Make OpenCVCamera optional * Fix styling problems * CI configuration * Fix CI * Hopefully final fix * Hopefully final fix * Fix all the problems * Oops.. * Add Qt Multimedia Camera * Another oops * Try to fix broken linux CI * Try to fix broken linux CI * Fix problems * Improve UI * Fix problems * camera: Add support for Qt <5.10 and fix preview error * CI: try to fix linux-frozen travis build * camera: fix problems and add multiple handlers support * fix CI * remove most ServiceFramework changes * Fix format * Remove last ServiceFramework change * camera: remove unused interfaces; revert submodule change * camera: fix CI error * ci: use ccache for opencv build * citra_qt: fix configuration error; CI: add mediaservice plugin * citra_qt: fix clang-format * citra_qt: fix documentation error * citra_qt: fix configuration page; camera: fix pausing crash * citra_qt: fix preview not stopping * camera: extend handlers length * camera: fix camera resume error * camera: fix clang-format * camera: remove all OpenCV; citra_qt: rewrite configuration * camera: remove all OpenCV; citra_qt: rewrite configuration * camera: remove all OpenCV; citra_qt: rewrite configuration * CI: fix linux ci * camera: check settings update; citra_qt: fix configuration error * service_cam: use a better way to apply configuration * Service_CAM: rewrite camera reload * cam: fix clang format * citra_qt: fix argument load issue; camera: base of system camera selection * citra_qt: Add system camera selection * camera: fix image upside down, Implement SetFrameRate in Qt Multimedia Camera * camera: Add missing <array> include, update SetFrameRate and add settings in Qt Multimedia Camera header * camera: move started in Qt Multimedia Camera header * QtMultimediaCamera: Move frame rates to SetFrameRate; Set minimum and maximum frame rate * Update appveyor.yml
2018-05-11 19:42:23 +02:00
#include "citra_qt/camera/qt_multimedia_camera.h"
#include "citra_qt/camera/still_image_camera.h"
2018-01-17 20:57:42 +01:00
#include "citra_qt/compatdb.h"
#include "citra_qt/compatibility_list.h"
#include "citra_qt/configuration/config.h"
#include "citra_qt/configuration/configure_dialog.h"
#include "citra_qt/configuration/configure_per_game.h"
#include "citra_qt/debugger/console.h"
#include "citra_qt/debugger/graphics/graphics.h"
#include "citra_qt/debugger/graphics/graphics_breakpoints.h"
#include "citra_qt/debugger/graphics/graphics_cmdlists.h"
#include "citra_qt/debugger/graphics/graphics_surface.h"
#include "citra_qt/debugger/graphics/graphics_tracing.h"
#include "citra_qt/debugger/graphics/graphics_vertex_shader.h"
#include "citra_qt/debugger/ipc/recorder.h"
#include "citra_qt/debugger/lle_service_modules.h"
2015-09-11 06:23:00 +02:00
#include "citra_qt/debugger/profiler.h"
#include "citra_qt/debugger/registers.h"
2016-04-08 18:28:54 +02:00
#include "citra_qt/debugger/wait_tree.h"
#include "citra_qt/discord.h"
#include "citra_qt/dumping/dumping_dialog.h"
#include "citra_qt/game_list.h"
#include "citra_qt/hotkeys.h"
#include "citra_qt/loading_screen.h"
#include "citra_qt/main.h"
#include "citra_qt/movie/movie_play_dialog.h"
#include "citra_qt/movie/movie_record_dialog.h"
#include "citra_qt/multiplayer/state.h"
#include "citra_qt/qt_image_interface.h"
#include "citra_qt/uisettings.h"
#include "citra_qt/updater/updater.h"
2018-01-19 14:42:21 +01:00
#include "citra_qt/util/clickable_label.h"
Add vulkan backend (#6512) * code: Prepare frontend for vulkan support * citra_qt: Add vulkan options to the GUI * vk_instance: Collect tooling info * renderer_vulkan: Add vulkan backend * qt: Fix fullscreen and resize issues on macOS. (#47) * qt: Fix bugged macOS full screen transition. * renderer/vulkan: Fix swapchain recreation destroying in-use semaphore. * renderer/vulkan: Make gl_Position invariant. (#48) This fixes an issue with black artifacts in Pokemon games on Apple GPUs. If the vertex calculations differ slightly between render passes, it can cause parts of model faces to fail depth test. * vk_renderpass_cache: Bump pixel format count * android: Custom driver code * vk_instance: Set moltenvk configuration * rasterizer_cache: Proper surface unregister * citra_qt: Fix invalid characters * vk_rasterizer: Correct special unbind * android: Allow async presentation toggle * vk_graphics_pipeline: Fix async shader compilation * We were actually waiting for the pipelines regardless of the setting, oops * vk_rasterizer: More robust attribute loading * android: Move PollEvents to OpenGL window * Vulkan does not need this and it causes problems * vk_instance: Enable robust buffer access * Improves stability on mali devices * vk_renderpass_cache: Bring back renderpass flushing * externals: Update vulkan-headers * gl_rasterizer: Separable shaders for everyone * vk_blit_helper: Corect depth to color convertion * renderer_vulkan: Implement reinterpretation with copy * Allows reinterpreteration with simply copy on AMD * vk_graphics_pipeline: Only fast compile if no shaders are pending * With this shaders weren't being compiled in parallel * vk_swapchain: Ensure vsync doesn't lock framerate * vk_present_window: Match guest swapchain size to vulkan image count * Less latency and fixes crashes that were caused by images being deleted before free * vk_instance: Blacklist VK_EXT_pipeline_creation_cache_control with nvidia gpus * Resolves crashes when async shader compilation is enabled * vk_rasterizer: Bump async threshold to 6 * Many games have fullscreen quads with 6 vertices. Fixes pokemon textures missing with async shaders * android: More robust surface recreation * renderer_vulkan: Fix dynamic state being lost * vk_pipeline_cache: Skip cache save when no pipeline cache exists * This is the cache when loading a save state * sdl: Fix surface initialization on macOS. (#49) * sdl: Fix surface initialization on macOS. * sdl: Fix render window events not being handled under Vulkan. * renderer/vulkan: Fix binding/unbinding of shadow rendering buffer. * vk_stream_buffer: Respect non coherent access alignment * Required by nvidia GPUs on MacOS * renderer/vulkan: Support VK_EXT_fragment_shader_interlock for shadow rendering. (#51) * renderer_vulkan: Port some recent shader fixes * vk_pipeline_cache: Improve shadow detection * vk_swapchain: Add missing check * renderer_vulkan: Fix hybrid screen * Revert "gl_rasterizer: Separable shaders for everyone" Causes crashes on mali GPUs, will need separate PR This reverts commit d22d556d30ff641b62dfece85738c96b7fbf7061. * renderer_vulkan: Fix flipped screenshot --------- Co-authored-by: Steveice10 <1269164+Steveice10@users.noreply.github.com>
2023-09-13 00:28:50 +02:00
#include "citra_qt/util/vk_device_info.h"
#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"
2022-11-15 11:16:45 +01:00
#include "common/literals.h"
#include "common/logging/backend.h"
#include "common/logging/log.h"
#include "common/memory_detect.h"
#include "common/microprofile.h"
2015-06-21 15:58:59 +02:00
#include "common/scm_rev.h"
2014-10-28 08:36:00 +01:00
#include "common/scope_exit.h"
#if CITRA_ARCH(x86_64)
2019-09-14 12:28:39 +02:00
#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"
2023-05-03 17:24:10 +02:00
#include "core/hle/service/am/am.h"
#include "core/hle/service/fs/archive.h"
#include "core/hle/service/nfc/nfc.h"
2015-09-11 06:23:00 +02:00
#include "core/loader/loader.h"
2018-07-08 07:55:32 +02:00
#include "core/movie.h"
#include "core/savestate.h"
#include "core/system_titles.h"
#include "input_common/main.h"
2020-08-20 09:46:35 +02:00
#include "network/network_settings.h"
#include "ui_main.h"
#include "video_core/renderer_base.h"
#include "video_core/video_core.h"
2015-05-19 06:21:33 +02:00
#ifdef __APPLE__
#include "common/apple_authorization.h"
#endif
#ifdef USE_DISCORD_PRESENCE
#include "citra_qt/discord_impl.h"
#endif
2016-08-01 23:13:35 +02:00
#ifdef QT_STATICPLUGIN
Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin);
#endif
#ifdef _WIN32
extern "C" {
2018-03-16 22:06:33 +01:00
// 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.
*/
enum class CalloutFlag : uint32_t {
Telemetry = 0x1,
};
void GMainWindow::ShowTelemetryCallout() {
if (UISettings::values.callout_flags.GetValue() &
static_cast<uint32_t>(CalloutFlag::Telemetry)) {
return;
}
UISettings::values.callout_flags =
UISettings::values.callout_flags.GetValue() | static_cast<uint32_t>(CalloutFlag::Telemetry);
2018-09-17 17:16:01 +02:00
const QString telemetry_message =
tr("<a href='https://citra-emu.org/entry/telemetry-and-why-thats-a-good-thing/'>Anonymous "
"data is collected</a> to help improve Citra. "
"<br/><br/>Would you like to share your usage data with us?");
if (QMessageBox::question(this, tr("Telemetry"), telemetry_message) == QMessageBox::Yes) {
NetSettings::values.enable_telemetry = true;
2023-06-30 12:39:38 +02:00
system.ApplySettings();
}
}
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_)
2023-08-01 02:57:38 +02:00
: ui{std::make_unique<Ui::MainWindow>()}, system{system_}, movie{system.Movie()},
config{std::make_unique<Config>()}, emu_thread{nullptr} {
Common::Log::Initialize();
2023-07-03 01:17:03 +02:00
Common::Log::Start();
Debugger::ToggleConsole();
2018-02-14 01:16:26 +01:00
// register types to use in slots and signals
qRegisterMetaType<std::size_t>("std::size_t");
2018-02-14 01:16:26 +01:00
qRegisterMetaType<Service::AM::InstallStatus>("Service::AM::InstallStatus");
2017-12-02 22:35:20 +01:00
2023-05-05 12:10:34 +02:00
// 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();
2017-02-16 04:23:30 +01:00
setAcceptDrops(true);
ui->setupUi(this);
2014-04-01 04:26:50 +02:00
statusBar()->hide();
2018-03-30 17:20:21 +02:00
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();
#if ENABLE_QT_UPDATER
ShowUpdaterWidgets();
#else
ui->action_Check_For_Updates->setVisible(false);
ui->action_Open_Maintenance_Tool->setVisible(false);
#endif
SetDefaultUIGeometry();
RestoreUIState();
ConnectMenuEvents();
ConnectWidgetEvents();
2018-06-29 15:56:12 +02:00
LOG_INFO(Frontend, "Citra 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);
2019-09-14 12:28:39 +02:00
#endif
LOG_INFO(Frontend, "Host OS: {}", PrettyProductName().toStdString());
2022-11-15 11:16:45 +01:00
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();
2018-04-16 00:42:58 +02:00
game_list->LoadCompatibilityList();
game_list->PopulateAsync(UISettings::values.game_dirs);
// Show one-time "callout" messages to the user
ShowTelemetryCallout();
mouse_hide_timer.setInterval(default_mouse_timeout);
connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor);
connect(ui->menubar, &QMenuBar::hovered, this, &GMainWindow::OnMouseActivity);
Add vulkan backend (#6512) * code: Prepare frontend for vulkan support * citra_qt: Add vulkan options to the GUI * vk_instance: Collect tooling info * renderer_vulkan: Add vulkan backend * qt: Fix fullscreen and resize issues on macOS. (#47) * qt: Fix bugged macOS full screen transition. * renderer/vulkan: Fix swapchain recreation destroying in-use semaphore. * renderer/vulkan: Make gl_Position invariant. (#48) This fixes an issue with black artifacts in Pokemon games on Apple GPUs. If the vertex calculations differ slightly between render passes, it can cause parts of model faces to fail depth test. * vk_renderpass_cache: Bump pixel format count * android: Custom driver code * vk_instance: Set moltenvk configuration * rasterizer_cache: Proper surface unregister * citra_qt: Fix invalid characters * vk_rasterizer: Correct special unbind * android: Allow async presentation toggle * vk_graphics_pipeline: Fix async shader compilation * We were actually waiting for the pipelines regardless of the setting, oops * vk_rasterizer: More robust attribute loading * android: Move PollEvents to OpenGL window * Vulkan does not need this and it causes problems * vk_instance: Enable robust buffer access * Improves stability on mali devices * vk_renderpass_cache: Bring back renderpass flushing * externals: Update vulkan-headers * gl_rasterizer: Separable shaders for everyone * vk_blit_helper: Corect depth to color convertion * renderer_vulkan: Implement reinterpretation with copy * Allows reinterpreteration with simply copy on AMD * vk_graphics_pipeline: Only fast compile if no shaders are pending * With this shaders weren't being compiled in parallel * vk_swapchain: Ensure vsync doesn't lock framerate * vk_present_window: Match guest swapchain size to vulkan image count * Less latency and fixes crashes that were caused by images being deleted before free * vk_instance: Blacklist VK_EXT_pipeline_creation_cache_control with nvidia gpus * Resolves crashes when async shader compilation is enabled * vk_rasterizer: Bump async threshold to 6 * Many games have fullscreen quads with 6 vertices. Fixes pokemon textures missing with async shaders * android: More robust surface recreation * renderer_vulkan: Fix dynamic state being lost * vk_pipeline_cache: Skip cache save when no pipeline cache exists * This is the cache when loading a save state * sdl: Fix surface initialization on macOS. (#49) * sdl: Fix surface initialization on macOS. * sdl: Fix render window events not being handled under Vulkan. * renderer/vulkan: Fix binding/unbinding of shadow rendering buffer. * vk_stream_buffer: Respect non coherent access alignment * Required by nvidia GPUs on MacOS * renderer/vulkan: Support VK_EXT_fragment_shader_interlock for shadow rendering. (#51) * renderer_vulkan: Port some recent shader fixes * vk_pipeline_cache: Improve shadow detection * vk_swapchain: Add missing check * renderer_vulkan: Fix hybrid screen * Revert "gl_rasterizer: Separable shaders for everyone" Causes crashes on mali GPUs, will need separate PR This reverts commit d22d556d30ff641b62dfece85738c96b7fbf7061. * renderer_vulkan: Fix flipped screenshot --------- Co-authored-by: Steveice10 <1269164+Steveice10@users.noreply.github.com>
2023-09-13 00:28:50 +02:00
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."));
}
#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();
2018-01-19 14:42:21 +01:00
Network::Shutdown();
}
void GMainWindow::InitializeWidgets() {
#ifdef CITRA_ENABLE_COMPATIBILITY_REPORTING
ui->action_Report_Compatibility->setVisible(true);
#endif
2023-06-24 00:59:18 +02:00
render_window = new GRenderWindow(this, emu_thread.get(), system, false);
secondary_window = new GRenderWindow(this, emu_thread.get(), system, true);
2014-04-22 05:15:17 +02:00
render_window->hide();
secondary_window->hide();
secondary_window->setParent(nullptr);
2014-04-01 04:26:50 +02:00
game_list = new GameList(this);
ui->horizontalLayout->addWidget(game_list);
2017-02-18 21:09:14 +01:00
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();
2017-02-18 21:09:14 +01:00
// 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);
2017-12-02 22:35:20 +01:00
progress_bar = new QProgressBar();
progress_bar->hide();
statusBar()->addPermanentWidget(progress_bar);
2017-02-18 21:09:14 +01:00
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."));
2017-02-18 21:09:14 +01:00
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."));
2017-02-18 21:09:14 +01:00
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."));
2017-02-18 21:09:14 +01:00
for (auto& label : {emu_speed_label, game_fps_label, emu_frametime_label}) {
label->setVisible(false);
2017-02-21 01:53:40 +01:00
label->setFrameStyle(QFrame::NoFrame);
label->setContentsMargins(4, 0, 4, 0);
statusBar()->addPermanentWidget(label);
2017-02-18 21:09:14 +01:00
}
// 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);
statusBar()->addPermanentWidget(multiplayer_state->GetStatusText());
statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon());
2017-02-18 21:09:14 +01:00
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;
2015-09-01 06:35:33 +02:00
#if MICROPROFILE_ENABLED
microProfileDialog = new MicroProfileDialog(this);
microProfileDialog->hide();
debug_menu->addAction(microProfileDialog->toggleViewAction());
#endif
registersWidget = new RegistersWidget(system, this);
2014-04-19 00:30:53 +02:00
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);
2014-04-19 00:30:53 +02:00
graphicsWidget = new GPUCommandStreamWidget(this);
addDockWidget(Qt::RightDockWidgetArea, graphicsWidget);
graphicsWidget->hide();
debug_menu->addAction(graphicsWidget->toggleViewAction());
graphicsCommandsWidget = new GPUCommandListWidget(system, this);
2014-05-18 17:52:22 +02:00
addDockWidget(Qt::RightDockWidgetArea, graphicsCommandsWidget);
graphicsCommandsWidget->hide();
debug_menu->addAction(graphicsCommandsWidget->toggleViewAction());
2014-05-18 17:52:22 +02:00
graphicsBreakpointsWidget = new GraphicsBreakPointsWidget(Pica::g_debug_context, this);
addDockWidget(Qt::RightDockWidgetArea, graphicsBreakpointsWidget);
graphicsBreakpointsWidget->hide();
debug_menu->addAction(graphicsBreakpointsWidget->toggleViewAction());
graphicsVertexShaderWidget = new GraphicsVertexShaderWidget(Pica::g_debug_context, this);
addDockWidget(Qt::RightDockWidgetArea, graphicsVertexShaderWidget);
graphicsVertexShaderWidget->hide();
debug_menu->addAction(graphicsVertexShaderWidget->toggleViewAction());
graphicsTracingWidget = new GraphicsTracingWidget(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);
2016-04-08 18:28:54 +02:00
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 Citra"));
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"));
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; });
Custom textures rewrite (#6452) * common: Add thread pool from yuzu * Is really useful for asynchronous operations like shader compilation and custom textures, will be used in following PRs * core: Improve ImageInterface * Provide a default implementation so frontends don't have to duplicate code registering the lodepng version * Add a dds version too which we will use in the next commit * rasterizer_cache: Rewrite custom textures * There's just too much to talk about here, look at the PR description for more details * rasterizer_cache: Implement basic pack configuration file * custom_tex_manager: Flip dumped textures * custom_tex_manager: Optimize custom texture hashing * If no convertions are needed then we can hash the decoded data directly removing the needed for duplicate decode * custom_tex_manager: Implement asynchronous texture loading * The file loading and decoding is offloaded into worker threads, while the upload itself still occurs in the main thread to avoid having to manage shared contexts * Address review comments * custom_tex_manager: Introduce custom material support * video_core: Move custom textures to separate directory * Also split the files to make the code cleaner * gl_texture_runtime: Generate mipmaps for material * custom_tex_manager: Prevent memory overflow when preloading * externals: Add dds-ktx as submodule * string_util: Return vector from SplitString * No code benefits from passing it as an argument * custom_textures: Use json config file * gl_rasterizer: Only bind material for unit 0 * Address review comments
2023-04-27 06:38:28 +02:00
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("Mute Audio"),
[] { Settings::values.audio_muted = !Settings::values.audio_muted; });
// 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();
}
});
}
2014-04-01 04:26:50 +02:00
void GMainWindow::SetDefaultUIGeometry() {
2014-04-01 04:26:50 +02:00
// geometry: 55% of the window contents are in the upper screen half, 45% in the lower half
2023-05-05 12:10:34 +02:00
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;
2014-04-01 04:26:50 +02:00
setGeometry(x, y, w, h);
}
2014-04-01 04:26:50 +02:00
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();
2015-09-02 14:56:38 +02:00
ui->action_Single_Window_Mode->setChecked(UISettings::values.single_window_mode.GetValue());
2014-04-22 05:15:17 +02:00
ToggleWindowMode();
2014-04-01 04:26:50 +02:00
ui->action_Fullscreen->setChecked(UISettings::values.fullscreen.GetValue());
SyncMenuUISettings();
2017-10-11 20:21:09 +02:00
ui->action_Display_Dock_Widget_Headers->setChecked(
UISettings::values.display_titlebar.GetValue());
OnDisplayTitleBars(ui->action_Display_Dock_Widget_Headers->isChecked());
2017-02-18 21:09:14 +01:00
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());
}
2019-09-14 23:14:23 +02:00
void GMainWindow::OnAppFocusStateChanged(Qt::ApplicationState state) {
if (!UISettings::values.pause_when_in_background) {
return;
}
if (state != Qt::ApplicationHidden && state != Qt::ApplicationInactive &&
state != Qt::ApplicationActive) {
LOG_DEBUG(Frontend, "ApplicationState unusual flag: {} ", state);
}
if (ui->action_Pause->isEnabled() &&
(state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) {
auto_paused = true;
OnPauseGame();
} else if (emulation_running && !emu_thread->IsRunning() && auto_paused &&
state == Qt::ApplicationActive) {
auto_paused = false;
OnStartGame();
}
}
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()); });
2014-04-01 04:26:50 +02:00
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);
2017-12-02 22:35:20 +01:00
connect(this, &GMainWindow::UpdateProgress, this, &GMainWindow::OnUpdateProgress);
2018-02-14 01:16:26 +01:00
connect(this, &GMainWindow::CIAInstallReport, this, &GMainWindow::OnCIAInstallReport);
connect(this, &GMainWindow::CIAInstallFinished, this, &GMainWindow::OnCIAInstallFinished);
connect(this, &GMainWindow::UpdateThemedIcons, multiplayer_state,
&MultiplayerState::UpdateThemedIcons);
2014-04-01 04:26:50 +02:00
}
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);
2018-01-19 14:42:21 +01:00
// 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);
2018-01-19 14:42:21 +01:00
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);
2018-07-08 07:55:32 +02:00
// 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://citra-emu.org/wiki/faq/")));
});
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()) {
2018-06-29 13:18:07 +02:00
LOG_INFO(Frontend, "Update check started");
} else {
2018-06-29 13:18:07 +02:00
LOG_WARNING(Frontend, "Unable to start check for updates");
}
}
void GMainWindow::OnUpdateFound(bool found, bool error) {
if (error) {
2018-06-29 13:18:07 +02:00
LOG_WARNING(Frontend, "Update check failed");
return;
}
if (!found) {
2018-06-29 13:18:07 +02:00
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) {
2018-06-29 13:18:07 +02:00
LOG_INFO(Frontend, "Update found, deferring as game is running");
defer_update_prompt = true;
return;
}
2018-06-29 13:18:07 +02:00
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
#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;
}
Prepare frontend for multiple graphics APIs (#6347) * externals: Update dynarmic * settings: Introduce GraphicsAPI enum * For now it's OpenGL only but will be expanded upon later * citra_qt: Introduce backend agnostic context management * Mostly a direct port from yuzu * core: Simplify context acquire * settings: Add option to create debug contexts * renderer_opengl: Abstract initialization to Driver * This commit also updates glad and adds some useful extensions which we will use in part 2 * Rasterizer construction is moved to the specific renderer instead of RendererBase. Software rendering has been disable to achieve this but will be brought back in the next commit. * video_core: Remove Init/Shutdown methods from renderer * The constructor and destructor can do the same job * In addition move opengl function loading to Qt since SDL already does this. Also remove ErrorVideoCore which is never reached * citra_qt: Decouple software renderer from opengl part 1 * citra: Decouple software renderer from opengl part 2 * android: Decouple software renderer from opengl part 3 * swrasterizer: Decouple software renderer from opengl part 4 * This commit simply enforces the renderer naming conventions in the software renderer * video_core: Move RendererBase to VideoCore * video_core: De-globalize screenshot state * video_core: Pass system to the renderers * video_core: Commonize shader uniform data * video_core: Abstract backend agnostic rasterizer operations * bootmanager: Remove references to OpenGL for macOS OpenGL macOS headers definitions clash heavily with each other * citra_qt: Proper title for api settings * video_core: Reduce boost usage * bootmanager: Fix hide mouse option Remove event handlers from RenderWidget for events that are already handled by the parent GRenderWindow. Also enable mouse tracking on the RenderWidget. * android: Remove software from graphics api list * code: Address review comments * citra: Port per-game settings read * Having to update the default value for all backends is a pain so lets centralize it * android: Rename to OpenGLES --------- Co-authored-by: MerryMage <MerryMage@users.noreply.github.com> Co-authored-by: Vitor Kiguchi <vitor-kiguchi@hotmail.com>
2023-03-27 13:29:17 +02:00
const auto scope = render_window->Acquire();
const Core::System::ResultStatus result{
system.Load(*render_window, filename.toStdString(), secondary_window)};
2014-04-04 03:24:07 +02:00
if (result != Core::System::ResultStatus::Success) {
switch (result) {
case Core::System::ResultStatus::ErrorGetLoader:
2018-06-29 13:18:07 +02:00
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:
2018-06-29 13:18:07 +02:00
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;
2016-12-17 07:20:47 +01:00
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;
2014-04-04 03:24:07 +02:00
}
std::string title;
system.GetAppLoader().ReadTitle(title);
game_title = QString::fromStdString(title);
UpdateWindowTitle();
game_path = filename;
system.TelemetrySession().AddField(Common::Telemetry::FieldType::App, "Frontend", "Qt");
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;
}
2018-06-29 13:18:07 +02:00
LOG_INFO(Frontend, "Citra 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();
2023-07-03 01:17:03 +02:00
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();
}
// Create and start the emulation thread
emu_thread = std::make_unique<EmuThread>(system, *render_window);
emit EmulationStarting(emu_thread.get());
emu_thread->start();
2014-04-22 05:15:17 +02:00
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()) {
2015-09-01 06:35:33 +02:00
game_list->hide();
game_list_placeholder->hide();
2015-09-01 06:35:33 +02:00
}
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();
2014-04-01 04:26:50 +02:00
}
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();
// 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();
// 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
2018-06-02 05:27:09 +02:00
game_title.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 =
2023-05-05 12:10:34 +02:00
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;
}
2023-08-01 02:57:38 +02:00
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;
}
}
}
2015-09-01 06:35:33 +02:00
void GMainWindow::OnGameListLoadFile(QString game_path) {
if (ConfirmChangeGame()) {
BootGame(game_path);
}
2015-09-01 06:35:33 +02:00
}
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: {
2019-08-14 07:04:50 +02:00
open_target = "Dumped Textures";
path = fmt::format("{}textures/{:016X}/",
FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), data_id);
break;
}
case GameListOpenTarget::TEXTURE_LOAD: {
2019-08-14 07:04:50 +02:00
open_target = "Custom Textures";
path = fmt::format("{}textures/{:016X}/",
FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), data_id);
break;
}
case GameListOpenTarget::MODS: {
2020-02-07 07:44:34 +01:00
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:
2018-06-29 13:18:07 +02:00
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) {
2018-09-01 01:42:07 +02:00
auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
2018-09-01 01:42:07 +02:00
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("Citra"),
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 {
2018-06-29 13:18:07 +02:00
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);
2014-04-01 04:26:50 +02:00
}
2017-12-02 22:35:20 +01:00
void GMainWindow::OnMenuInstallCIA() {
2018-02-14 01:16:26 +01:00
QStringList filepaths = QFileDialog::getOpenFileNames(
this, tr("Load Files"), UISettings::values.roms_path,
tr("3DS Installation File (*.CIA*)") + QStringLiteral(";;") + tr("All Files (*.*)"));
if (filepaths.isEmpty()) {
2017-12-02 22:35:20 +01:00
return;
}
2017-12-02 22:35:20 +01:00
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);
2017-12-02 22:35:20 +01:00
progress_bar->show();
2018-07-03 00:13:45 +02:00
progress_bar->setMaximum(INT_MAX);
2018-02-14 01:16:26 +01:00
2023-05-05 12:10:34 +02:00
(void)QtConcurrent::run([&, filepaths] {
2018-02-14 01:16:26 +01:00
Service::AM::InstallStatus status;
const auto cia_progress = [&](std::size_t written, std::size_t total) {
2018-02-16 21:53:38 +01:00
emit UpdateProgress(written, total);
};
for (const auto& current_path : filepaths) {
2018-02-14 01:16:26 +01:00
status = Service::AM::InstallCIA(current_path.toStdString(), cia_progress);
emit CIAInstallReport(status, current_path);
}
emit CIAInstallFinished();
2017-12-02 22:35:20 +01:00
});
}
void GMainWindow::OnUpdateProgress(std::size_t written, std::size_t total) {
2018-07-03 00:13:45 +02:00
progress_bar->setValue(
static_cast<int>(INT_MAX * (static_cast<double>(written) / static_cast<double>(total))));
2017-12-02 22:35:20 +01:00
}
2018-02-14 01:16:26 +01:00
void GMainWindow::OnCIAInstallReport(Service::AM::InstallStatus status, QString filepath) {
QString filename = QFileInfo(filepath).fileName();
switch (status) {
2017-12-02 22:35:20 +01:00
case Service::AM::InstallStatus::Success:
2018-02-14 01:16:26 +01:00
this->statusBar()->showMessage(tr("%1 has been installed successfully.").arg(filename));
2017-12-02 22:35:20 +01:00
break;
case Service::AM::InstallStatus::ErrorFailedToOpenFile:
QMessageBox::critical(this, tr("Unable to open File"),
2018-02-14 01:16:26 +01:00
tr("Could not open %1").arg(filename));
2017-12-02 22:35:20 +01:00
break;
case Service::AM::InstallStatus::ErrorAborted:
QMessageBox::critical(
this, tr("Installation aborted"),
2018-02-14 01:16:26 +01:00
tr("The installation of %1 was aborted. Please see the log for more details")
.arg(filename));
2017-12-02 22:35:20 +01:00
break;
case Service::AM::InstallStatus::ErrorInvalid:
2018-02-14 01:16:26 +01:00
QMessageBox::critical(this, tr("Invalid File"), tr("%1 is not a valid CIA").arg(filename));
2017-12-02 22:35:20 +01:00
break;
case Service::AM::InstallStatus::ErrorEncrypted:
QMessageBox::critical(this, tr("Encrypted File"),
2018-02-14 01:16:26 +01:00
tr("%1 must be decrypted "
"before being used with Citra. A real 3DS is required.")
.arg(filename));
2017-12-02 22:35:20 +01:00
break;
case Service::AM::InstallStatus::ErrorFileNotFound:
QMessageBox::critical(this, tr("Unable to find File"),
tr("Could not find %1").arg(filename));
break;
2017-12-02 22:35:20 +01:00
}
2018-02-14 01:16:26 +01:00
}
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);
2017-12-02 22:35:20 +01:00
}
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("Citra"), tr("Failed to uninstall '%1'.").arg(failed_name));
} else if (!future_watcher.isCanceled()) {
QMessageBox::information(this, tr("Citra"),
tr("Successfully uninstalled '%1'.").arg(first_name));
}
}
void GMainWindow::OnMenuRecentFile() {
QAction* action = qobject_cast<QAction*>(sender());
2020-03-28 18:08:27 +01:00
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() {
2023-05-05 12:10:34 +02:00
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);
2014-04-04 03:24:07 +02:00
UpdateMenuState();
discord_rpc->Update();
UpdateSaveStates();
UpdateAPIIndicator();
2014-04-01 04:26:50 +02:00
}
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);
2023-05-05 12:10:34 +02:00
qt_cameras->PauseCameras();
UpdateMenuState();
AllowOSSleep();
2014-04-01 04:26:50 +02:00
}
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);
UpdateAPIIndicator();
2014-04-01 04:26:50 +02:00
}
void GMainWindow::OnLoadComplete() {
loading_screen->OnLoadComplete();
UpdateSecondaryWindowVisibility();
}
void GMainWindow::OnMenuReportCompatibility() {
2020-08-20 09:46:35 +02:00
if (!NetSettings::values.citra_token.empty() && !NetSettings::values.citra_username.empty()) {
CompatDB compatdb{system.TelemetrySession(), 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."));
}
}
2017-10-11 20:21:09 +02:00
void GMainWindow::ToggleFullscreen() {
if (!emulation_running) {
return;
}
if (ui->action_Fullscreen->isChecked()) {
ShowFullscreen();
2017-10-11 20:21:09 +02:00
} 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);
2017-10-11 20:21:09 +02:00
}
}
void GMainWindow::ToggleWindowMode() {
if (ui->action_Single_Window_Mode->isChecked()) {
// Render in the main window...
2014-04-22 05:15:17 +02:00
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();
2015-09-01 06:35:33 +02:00
game_list->show();
}
2014-04-01 04:26:50 +02:00
}
}
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;
2023-06-30 12:39:38 +02:00
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();
2023-06-30 12:39:38 +02:00
system.ApplySettings();
UpdateSecondaryWindowVisibility();
}
void GMainWindow::OnSwapScreens() {
Settings::values.swap_screen = ui->action_Screen_Layout_Swap_Screens->isChecked();
2023-06-30 12:39:38 +02:00
system.ApplySettings();
}
void GMainWindow::OnRotateScreens() {
Settings::values.upright_screen = ui->action_Screen_Layout_Upright_Screens->isChecked();
2023-06-30 12:39:38 +02:00
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();
2020-04-06 23:47:52 +02:00
newest_slot = action->data().toUInt();
2020-01-03 18:19:59 +01:00
}
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();
2020-01-03 18:19:59 +01:00
}
void GMainWindow::OnConfigure() {
game_list->SetDirectoryWatcherEnabled(false);
Settings::SetConfiguringGlobal(true);
Add vulkan backend (#6512) * code: Prepare frontend for vulkan support * citra_qt: Add vulkan options to the GUI * vk_instance: Collect tooling info * renderer_vulkan: Add vulkan backend * qt: Fix fullscreen and resize issues on macOS. (#47) * qt: Fix bugged macOS full screen transition. * renderer/vulkan: Fix swapchain recreation destroying in-use semaphore. * renderer/vulkan: Make gl_Position invariant. (#48) This fixes an issue with black artifacts in Pokemon games on Apple GPUs. If the vertex calculations differ slightly between render passes, it can cause parts of model faces to fail depth test. * vk_renderpass_cache: Bump pixel format count * android: Custom driver code * vk_instance: Set moltenvk configuration * rasterizer_cache: Proper surface unregister * citra_qt: Fix invalid characters * vk_rasterizer: Correct special unbind * android: Allow async presentation toggle * vk_graphics_pipeline: Fix async shader compilation * We were actually waiting for the pipelines regardless of the setting, oops * vk_rasterizer: More robust attribute loading * android: Move PollEvents to OpenGL window * Vulkan does not need this and it causes problems * vk_instance: Enable robust buffer access * Improves stability on mali devices * vk_renderpass_cache: Bring back renderpass flushing * externals: Update vulkan-headers * gl_rasterizer: Separable shaders for everyone * vk_blit_helper: Corect depth to color convertion * renderer_vulkan: Implement reinterpretation with copy * Allows reinterpreteration with simply copy on AMD * vk_graphics_pipeline: Only fast compile if no shaders are pending * With this shaders weren't being compiled in parallel * vk_swapchain: Ensure vsync doesn't lock framerate * vk_present_window: Match guest swapchain size to vulkan image count * Less latency and fixes crashes that were caused by images being deleted before free * vk_instance: Blacklist VK_EXT_pipeline_creation_cache_control with nvidia gpus * Resolves crashes when async shader compilation is enabled * vk_rasterizer: Bump async threshold to 6 * Many games have fullscreen quads with 6 vertices. Fixes pokemon textures missing with async shaders * android: More robust surface recreation * renderer_vulkan: Fix dynamic state being lost * vk_pipeline_cache: Skip cache save when no pipeline cache exists * This is the cache when loading a save state * sdl: Fix surface initialization on macOS. (#49) * sdl: Fix surface initialization on macOS. * sdl: Fix render window events not being handled under Vulkan. * renderer/vulkan: Fix binding/unbinding of shadow rendering buffer. * vk_stream_buffer: Respect non coherent access alignment * Required by nvidia GPUs on MacOS * renderer/vulkan: Support VK_EXT_fragment_shader_interlock for shadow rendering. (#51) * renderer_vulkan: Port some recent shader fixes * vk_pipeline_cache: Improve shadow detection * vk_swapchain: Add missing check * renderer_vulkan: Fix hybrid screen * Revert "gl_rasterizer: Separable shaders for everyone" Causes crashes on mali GPUs, will need separate PR This reverts commit d22d556d30ff641b62dfece85738c96b7fbf7061. * renderer_vulkan: Fix flipped screenshot --------- Co-authored-by: Steveice10 <1269164+Steveice10@users.noreply.github.com>
2023-09-13 00:28:50 +02:00
ConfigureDialog configureDialog(this, hotkey_registry, system, physical_devices,
!multiplayer_state->IsHostingPublicRoom());
connect(&configureDialog, &ConfigureDialog::LanguageChanged, this,
&GMainWindow::OnLanguageChanged);
2018-07-02 23:53:34 +02:00
auto old_theme = UISettings::values.theme;
2018-12-29 15:19:45 +01:00
const int old_input_profile_index = Settings::values.current_input_profile_index;
2018-12-29 03:13:57 +01:00
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();
2016-01-24 18:34:05 +01:00
auto result = configureDialog.exec();
game_list->SetDirectoryWatcherEnabled(true);
if (result == QDialog::Accepted) {
configureDialog.ApplyConfiguration();
InitializeHotkeys();
2018-07-02 23:53:34 +02:00
if (UISettings::values.theme != old_theme)
UpdateUITheme();
if (UISettings::values.enable_discord_presence.GetValue() != old_discord_presence)
SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue());
if (!multiplayer_state->IsHostingPublicRoom())
multiplayer_state->UpdateCredentials();
emit UpdateThemedIcons();
SyncMenuUISettings();
game_list->RefreshGameDirectory();
2016-01-24 21:54:04 +01:00
config->Save();
if (UISettings::values.hide_mouse && emulation_running) {
setMouseTracking(true);
mouse_hide_timer.start();
} else {
setMouseTracking(false);
}
UpdateSecondaryWindowVisibility();
UpdateBootHomeMenuState();
UpdateAPIIndicator();
} else {
2018-12-29 03:13:57 +01:00
Settings::values.input_profiles = old_input_profiles;
Settings::values.touch_from_button_maps = old_touch_from_button_maps;
2018-12-29 15:19:45 +01:00
Settings::LoadProfile(old_input_profile_index);
2016-01-24 18:34:05 +01:00
}
2014-04-01 04:26:50 +02:00
}
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;
}
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;
}
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;
}
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.Memory(), Pica::g_debug_context, this);
addDockWidget(Qt::RightDockWidgetArea, graphicsSurfaceViewerWidget);
// TODO: Maybe graphicsSurfaceViewerWidget->setFloating(true);
graphicsSurfaceViewerWidget->show();
}
2018-07-08 07:55:32 +02:00
void GMainWindow::OnRecordMovie() {
2023-08-01 02:57:38 +02:00
MovieRecordDialog dialog(this, system);
if (dialog.exec() != QDialog::Accepted) {
2018-07-08 07:55:32 +02:00
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));
2018-07-08 07:55:32 +02:00
}
ui->action_Close_Movie->setEnabled(true);
ui->action_Save_Movie->setEnabled(true);
}
void GMainWindow::OnPlayMovie() {
2023-08-01 02:57:38 +02:00
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);
2018-07-08 07:55:32 +02:00
}
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();
}
2018-07-08 07:55:32 +02:00
}
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();
2018-07-08 07:55:32 +02:00
}
}
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 Citra, 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("Citra"),
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("Citra"), tr("FFmpeg has been sucessfully installed."));
} else {
QMessageBox::critical(this, tr("Citra"),
tr("Installation of FFmpeg failed. Check the log file for details."));
}
}
#endif
void GMainWindow::OnStartVideoDumping() {
2023-06-30 12:39:38 +02:00
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) {
Layout::FramebufferLayout layout{
Layout::FrameLayoutFromResolutionScale(VideoCore::g_renderer->GetResolutionScaleFactor())};
auto dumper = std::make_shared<VideoDumper::FFmpegBackend>();
if (dumper->StartDumping(path.toStdString(), layout)) {
system.RegisterVideoDumper(dumper);
} else {
QMessageBox::critical(
this, tr("Citra"),
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%")
2018-02-14 01:16:26 +01:00
.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::UpdateAPIIndicator(bool update) {
Add vulkan backend (#6512) * code: Prepare frontend for vulkan support * citra_qt: Add vulkan options to the GUI * vk_instance: Collect tooling info * renderer_vulkan: Add vulkan backend * qt: Fix fullscreen and resize issues on macOS. (#47) * qt: Fix bugged macOS full screen transition. * renderer/vulkan: Fix swapchain recreation destroying in-use semaphore. * renderer/vulkan: Make gl_Position invariant. (#48) This fixes an issue with black artifacts in Pokemon games on Apple GPUs. If the vertex calculations differ slightly between render passes, it can cause parts of model faces to fail depth test. * vk_renderpass_cache: Bump pixel format count * android: Custom driver code * vk_instance: Set moltenvk configuration * rasterizer_cache: Proper surface unregister * citra_qt: Fix invalid characters * vk_rasterizer: Correct special unbind * android: Allow async presentation toggle * vk_graphics_pipeline: Fix async shader compilation * We were actually waiting for the pipelines regardless of the setting, oops * vk_rasterizer: More robust attribute loading * android: Move PollEvents to OpenGL window * Vulkan does not need this and it causes problems * vk_instance: Enable robust buffer access * Improves stability on mali devices * vk_renderpass_cache: Bring back renderpass flushing * externals: Update vulkan-headers * gl_rasterizer: Separable shaders for everyone * vk_blit_helper: Corect depth to color convertion * renderer_vulkan: Implement reinterpretation with copy * Allows reinterpreteration with simply copy on AMD * vk_graphics_pipeline: Only fast compile if no shaders are pending * With this shaders weren't being compiled in parallel * vk_swapchain: Ensure vsync doesn't lock framerate * vk_present_window: Match guest swapchain size to vulkan image count * Less latency and fixes crashes that were caused by images being deleted before free * vk_instance: Blacklist VK_EXT_pipeline_creation_cache_control with nvidia gpus * Resolves crashes when async shader compilation is enabled * vk_rasterizer: Bump async threshold to 6 * Many games have fullscreen quads with 6 vertices. Fixes pokemon textures missing with async shaders * android: More robust surface recreation * renderer_vulkan: Fix dynamic state being lost * vk_pipeline_cache: Skip cache save when no pipeline cache exists * This is the cache when loading a save state * sdl: Fix surface initialization on macOS. (#49) * sdl: Fix surface initialization on macOS. * sdl: Fix render window events not being handled under Vulkan. * renderer/vulkan: Fix binding/unbinding of shadow rendering buffer. * vk_stream_buffer: Respect non coherent access alignment * Required by nvidia GPUs on MacOS * renderer/vulkan: Support VK_EXT_fragment_shader_interlock for shadow rendering. (#51) * renderer_vulkan: Port some recent shader fixes * vk_pipeline_cache: Improve shadow detection * vk_swapchain: Add missing check * renderer_vulkan: Fix hybrid screen * Revert "gl_rasterizer: Separable shaders for everyone" Causes crashes on mali GPUs, will need separate PR This reverts commit d22d556d30ff641b62dfece85738c96b7fbf7061. * renderer_vulkan: Fix flipped screenshot --------- Co-authored-by: Steveice10 <1269164+Steveice10@users.noreply.github.com>
2023-09-13 00:28:50 +02:00
static std::array graphics_apis = {QStringLiteral("SOFTWARE"), QStringLiteral("OPENGL"),
QStringLiteral("VULKAN")};
Add vulkan backend (#6512) * code: Prepare frontend for vulkan support * citra_qt: Add vulkan options to the GUI * vk_instance: Collect tooling info * renderer_vulkan: Add vulkan backend * qt: Fix fullscreen and resize issues on macOS. (#47) * qt: Fix bugged macOS full screen transition. * renderer/vulkan: Fix swapchain recreation destroying in-use semaphore. * renderer/vulkan: Make gl_Position invariant. (#48) This fixes an issue with black artifacts in Pokemon games on Apple GPUs. If the vertex calculations differ slightly between render passes, it can cause parts of model faces to fail depth test. * vk_renderpass_cache: Bump pixel format count * android: Custom driver code * vk_instance: Set moltenvk configuration * rasterizer_cache: Proper surface unregister * citra_qt: Fix invalid characters * vk_rasterizer: Correct special unbind * android: Allow async presentation toggle * vk_graphics_pipeline: Fix async shader compilation * We were actually waiting for the pipelines regardless of the setting, oops * vk_rasterizer: More robust attribute loading * android: Move PollEvents to OpenGL window * Vulkan does not need this and it causes problems * vk_instance: Enable robust buffer access * Improves stability on mali devices * vk_renderpass_cache: Bring back renderpass flushing * externals: Update vulkan-headers * gl_rasterizer: Separable shaders for everyone * vk_blit_helper: Corect depth to color convertion * renderer_vulkan: Implement reinterpretation with copy * Allows reinterpreteration with simply copy on AMD * vk_graphics_pipeline: Only fast compile if no shaders are pending * With this shaders weren't being compiled in parallel * vk_swapchain: Ensure vsync doesn't lock framerate * vk_present_window: Match guest swapchain size to vulkan image count * Less latency and fixes crashes that were caused by images being deleted before free * vk_instance: Blacklist VK_EXT_pipeline_creation_cache_control with nvidia gpus * Resolves crashes when async shader compilation is enabled * vk_rasterizer: Bump async threshold to 6 * Many games have fullscreen quads with 6 vertices. Fixes pokemon textures missing with async shaders * android: More robust surface recreation * renderer_vulkan: Fix dynamic state being lost * vk_pipeline_cache: Skip cache save when no pipeline cache exists * This is the cache when loading a save state * sdl: Fix surface initialization on macOS. (#49) * sdl: Fix surface initialization on macOS. * sdl: Fix render window events not being handled under Vulkan. * renderer/vulkan: Fix binding/unbinding of shadow rendering buffer. * vk_stream_buffer: Respect non coherent access alignment * Required by nvidia GPUs on MacOS * renderer/vulkan: Support VK_EXT_fragment_shader_interlock for shadow rendering. (#51) * renderer_vulkan: Port some recent shader fixes * vk_pipeline_cache: Improve shadow detection * vk_swapchain: Add missing check * renderer_vulkan: Fix hybrid screen * Revert "gl_rasterizer: Separable shaders for everyone" Causes crashes on mali GPUs, will need separate PR This reverts commit d22d556d30ff641b62dfece85738c96b7fbf7061. * renderer_vulkan: Fix flipped screenshot --------- Co-authored-by: Steveice10 <1269164+Steveice10@users.noreply.github.com>
2023-09-13 00:28:50 +02:00
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();
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::OnMouseActivity() {
ShowMouseCursor();
}
2020-08-15 15:55:54 +02:00
void GMainWindow::mouseMoveEvent([[maybe_unused]] QMouseEvent* event) {
OnMouseActivity();
}
2020-08-15 15:55:54 +02:00
void GMainWindow::mousePressEvent([[maybe_unused]] QMouseEvent* event) {
OnMouseActivity();
}
2020-08-15 15:55:54 +02:00
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;
2020-04-13 00:12:15 +02:00
} 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("Citra"), 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();
2014-04-01 04:26:50 +02:00
// Shutdown session if the emu thread is active...
if (emu_thread) {
ShutdownGame();
}
2014-04-01 04:26:50 +02:00
render_window->close();
secondary_window->close();
multiplayer_state->Close();
InputCommon::Shutdown();
2014-04-01 04:26:50 +02:00
QWidget::closeEvent(event);
}
2019-11-28 18:56:58 +01:00
static bool IsSingleFileDropEvent(const QMimeData* mime) {
return mime->hasUrls() && mime->urls().length() == 1;
2017-02-16 04:23:30 +01:00
}
2019-11-28 18:56:58 +01:00
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);
}
2017-02-16 04:23:30 +01:00
}
2019-11-28 18:56:58 +01:00
return true;
}
void GMainWindow::dropEvent(QDropEvent* event) {
DropAction(event);
2017-02-16 04:23:30 +01:00
}
void GMainWindow::dragEnterEvent(QDragEnterEvent* event) {
2019-11-28 18:56:58 +01:00
AcceptDropEvent(event);
2017-02-16 04:23:30 +01:00
}
void GMainWindow::dragMoveEvent(QDragMoveEvent* event) {
2019-11-28 18:56:58 +01:00
AcceptDropEvent(event);
2017-02-16 04:23:30 +01:00
}
bool GMainWindow::ConfirmChangeGame() {
if (!emu_thread) [[unlikely]] {
2017-02-16 04:23:30 +01:00
return true;
}
2017-02-16 04:23:30 +01:00
auto answer = QMessageBox::question(
this, tr("Citra"), tr("The game is still running. Would you like to stop emulation?"),
2017-02-16 04:23:30 +01:00
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() {
2023-11-02 01:58:02 +01:00
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({});
}
2023-11-02 01:58:02 +01:00
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");
}
2023-11-02 01:58:02 +01:00
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);
2018-07-24 12:57:45 +02:00
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);
Add vulkan backend (#6512) * code: Prepare frontend for vulkan support * citra_qt: Add vulkan options to the GUI * vk_instance: Collect tooling info * renderer_vulkan: Add vulkan backend * qt: Fix fullscreen and resize issues on macOS. (#47) * qt: Fix bugged macOS full screen transition. * renderer/vulkan: Fix swapchain recreation destroying in-use semaphore. * renderer/vulkan: Make gl_Position invariant. (#48) This fixes an issue with black artifacts in Pokemon games on Apple GPUs. If the vertex calculations differ slightly between render passes, it can cause parts of model faces to fail depth test. * vk_renderpass_cache: Bump pixel format count * android: Custom driver code * vk_instance: Set moltenvk configuration * rasterizer_cache: Proper surface unregister * citra_qt: Fix invalid characters * vk_rasterizer: Correct special unbind * android: Allow async presentation toggle * vk_graphics_pipeline: Fix async shader compilation * We were actually waiting for the pipelines regardless of the setting, oops * vk_rasterizer: More robust attribute loading * android: Move PollEvents to OpenGL window * Vulkan does not need this and it causes problems * vk_instance: Enable robust buffer access * Improves stability on mali devices * vk_renderpass_cache: Bring back renderpass flushing * externals: Update vulkan-headers * gl_rasterizer: Separable shaders for everyone * vk_blit_helper: Corect depth to color convertion * renderer_vulkan: Implement reinterpretation with copy * Allows reinterpreteration with simply copy on AMD * vk_graphics_pipeline: Only fast compile if no shaders are pending * With this shaders weren't being compiled in parallel * vk_swapchain: Ensure vsync doesn't lock framerate * vk_present_window: Match guest swapchain size to vulkan image count * Less latency and fixes crashes that were caused by images being deleted before free * vk_instance: Blacklist VK_EXT_pipeline_creation_cache_control with nvidia gpus * Resolves crashes when async shader compilation is enabled * vk_rasterizer: Bump async threshold to 6 * Many games have fullscreen quads with 6 vertices. Fixes pokemon textures missing with async shaders * android: More robust surface recreation * renderer_vulkan: Fix dynamic state being lost * vk_pipeline_cache: Skip cache save when no pipeline cache exists * This is the cache when loading a save state * sdl: Fix surface initialization on macOS. (#49) * sdl: Fix surface initialization on macOS. * sdl: Fix render window events not being handled under Vulkan. * renderer/vulkan: Fix binding/unbinding of shadow rendering buffer. * vk_stream_buffer: Respect non coherent access alignment * Required by nvidia GPUs on MacOS * renderer/vulkan: Support VK_EXT_fragment_shader_interlock for shadow rendering. (#51) * renderer_vulkan: Port some recent shader fixes * vk_pipeline_cache: Improve shadow detection * vk_swapchain: Add missing check * renderer_vulkan: Fix hybrid screen * Revert "gl_rasterizer: Separable shaders for everyone" Causes crashes on mali GPUs, will need separate PR This reverts commit d22d556d30ff641b62dfece85738c96b7fbf7061. * renderer_vulkan: Fix flipped screenshot --------- Co-authored-by: Steveice10 <1269164+Steveice10@users.noreply.github.com>
2023-09-13 00:28:50 +02:00
ConfigurePerGame dialog(this, title_id, file_name, 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();
}
}
2018-07-08 07:55:32 +02:00
void GMainWindow::OnMoviePlaybackCompleted() {
OnPauseGame();
2018-07-08 07:55:32 +02:00
QMessageBox::information(this, tr("Playback Completed"), tr("Movie playback completed."));
}
void GMainWindow::UpdateWindowTitle() {
const QString full_name = QString::fromUtf8(Common::g_build_fullname);
2018-06-02 05:27:09 +02:00
if (game_title.isEmpty()) {
setWindowTitle(QStringLiteral("Citra %1").arg(full_name));
2018-06-02 05:27:09 +02:00
} else {
setWindowTitle(QStringLiteral("Citra %1 | %2").arg(full_name, game_title));
render_window->setWindowTitle(
QStringLiteral("Citra %1 | %2 | %3").arg(full_name, game_title, tr("Primary Window")));
secondary_window->setWindowTitle(QStringLiteral("Citra %1 | %2 | %3")
.arg(full_name, game_title, tr("Secondary Window")));
2018-06-02 05:27:09 +02:00
}
}
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());
}
2018-07-24 12:57:45 +02:00
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) {
2023-08-01 02:57:38 +02:00
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();
}
2014-04-01 04:26:50 +02:00
#ifdef main
#undef main
#endif
2023-05-05 12:10:34 +02:00
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) {
2023-05-05 12:10:34 +02:00
return Qt::HighDpiScaleFactorRoundingPolicy::PassThrough;
}
const QRect screen_rect = primary_screen->geometry();
2023-05-05 12:10:34 +02:00
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.
2023-05-05 12:10:34 +02:00
constexpr qreal minimum_width = 1350.0;
constexpr qreal minimum_height = 900.0;
2023-05-05 12:10:34 +02:00
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.
2023-05-05 12:10:34 +02:00
const qreal max_ratio = std::trunc(std::min(width_ratio, height_ratio));
if (max_ratio > real_ratio) {
2023-05-05 12:10:34 +02:00
return Qt::HighDpiScaleFactorRoundingPolicy::Round;
} else {
2023-05-05 12:10:34 +02:00
return Qt::HighDpiScaleFactorRoundingPolicy::Floor;
}
#else
// Other OSes should be better than Windows at fractional scaling.
2023-05-05 12:10:34 +02:00
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("Citra team"));
QCoreApplication::setApplicationName(QStringLiteral("Citra"));
2023-05-05 12:10:34 +02:00
auto rounding_policy = GetHighDpiRoundingPolicy();
QApplication::setHighDpiScaleFactorRoundingPolicy(rounding_policy);
#ifdef __APPLE__
std::string bin_path = FileUtil::GetBundleDirectory() + DIR_SEP + "..";
chdir(bin_path.c_str());
#endif
2023-05-05 12:10:34 +02:00
QCoreApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity);
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
2014-04-01 04:26:50 +02:00
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
2014-04-01 04:26:50 +02:00
main_window.show();
QObject::connect(&app, &QGuiApplication::applicationStateChanged, &main_window,
2019-09-14 23:14:23 +02:00
&GMainWindow::OnAppFocusStateChanged);
int result = app.exec();
detached_tasks.WaitForAllTasks();
return result;
2014-04-01 04:26:50 +02:00
}