From b1b49d97895f5b4764967740668bdbcccba2f5c3 Mon Sep 17 00:00:00 2001 From: pineappleEA Date: Fri, 10 Nov 2023 18:46:19 +0100 Subject: [PATCH] early-access version 3970 --- README.md | 2 +- src/core/hle/service/hid/irs.cpp | 2 +- .../hid/irsensor/clustering_processor.cpp | 6 +- .../hid/irsensor/image_transfer_processor.cpp | 2 +- .../service/hid/irsensor/moment_processor.cpp | 120 ++++- .../service/hid/irsensor/moment_processor.h | 24 +- src/yuzu/game_list.cpp | 6 +- src/yuzu/main.cpp | 427 ++++++++++-------- src/yuzu/main.h | 21 +- src/yuzu/util/util.cpp | 12 +- src/yuzu/util/util.h | 3 +- src/yuzu/vk_device_info.cpp | 1 + 12 files changed, 406 insertions(+), 220 deletions(-) diff --git a/README.md b/README.md index 452a5453b..ca8e4541f 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ yuzu emulator early access ============= -This is the source code for early-access 3969. +This is the source code for early-access 3970. ## Legal Notice diff --git a/src/core/hle/service/hid/irs.cpp b/src/core/hle/service/hid/irs.cpp index 6c2dc0f3b..f880d27c5 100755 --- a/src/core/hle/service/hid/irs.cpp +++ b/src/core/hle/service/hid/irs.cpp @@ -138,7 +138,7 @@ void IRS::RunMomentProcessor(HLERequestContext& ctx) { if (result.IsSuccess()) { auto& device = GetIrCameraSharedMemoryDeviceEntry(parameters.camera_handle); - MakeProcessor(parameters.camera_handle, device); + MakeProcessorWithCoreContext(parameters.camera_handle, device); auto& image_transfer_processor = GetProcessor(parameters.camera_handle); image_transfer_processor.SetConfig(parameters.processor_config); npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex, diff --git a/src/core/hle/service/hid/irsensor/clustering_processor.cpp b/src/core/hle/service/hid/irsensor/clustering_processor.cpp index 926ced742..2ed4b140c 100755 --- a/src/core/hle/service/hid/irsensor/clustering_processor.cpp +++ b/src/core/hle/service/hid/irsensor/clustering_processor.cpp @@ -48,7 +48,7 @@ void ClusteringProcessor::OnControllerUpdate(Core::HID::ControllerTriggerType ty } next_state = {}; - const auto camera_data = npad_device->GetCamera(); + const auto& camera_data = npad_device->GetCamera(); auto filtered_image = camera_data.data; RemoveLowIntensityData(filtered_image); @@ -202,14 +202,14 @@ ClusteringProcessor::ClusteringData ClusteringProcessor::MergeCluster( } u8 ClusteringProcessor::GetPixel(const std::vector& data, std::size_t x, std::size_t y) const { - if ((y * width) + x > data.size()) { + if ((y * width) + x >= data.size()) { return 0; } return data[(y * width) + x]; } void ClusteringProcessor::SetPixel(std::vector& data, std::size_t x, std::size_t y, u8 value) { - if ((y * width) + x > data.size()) { + if ((y * width) + x >= data.size()) { return; } data[(y * width) + x] = value; diff --git a/src/core/hle/service/hid/irsensor/image_transfer_processor.cpp b/src/core/hle/service/hid/irsensor/image_transfer_processor.cpp index 41e3c8654..44082363e 100755 --- a/src/core/hle/service/hid/irsensor/image_transfer_processor.cpp +++ b/src/core/hle/service/hid/irsensor/image_transfer_processor.cpp @@ -49,7 +49,7 @@ void ImageTransferProcessor::OnControllerUpdate(Core::HID::ControllerTriggerType return; } - const auto camera_data = npad_device->GetCamera(); + const auto& camera_data = npad_device->GetCamera(); // This indicates how much ambient light is present processor_state.ambient_noise_level = Core::IrSensor::CameraAmbientNoiseLevel::Low; diff --git a/src/core/hle/service/hid/irsensor/moment_processor.cpp b/src/core/hle/service/hid/irsensor/moment_processor.cpp index 78b5eef0d..3e2fee501 100755 --- a/src/core/hle/service/hid/irsensor/moment_processor.cpp +++ b/src/core/hle/service/hid/irsensor/moment_processor.cpp @@ -1,24 +1,136 @@ // SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later +#include "core/hid/emulated_controller.h" +#include "core/hid/hid_core.h" #include "core/hle/service/hid/irsensor/moment_processor.h" namespace Service::IRS { -MomentProcessor::MomentProcessor(Core::IrSensor::DeviceFormat& device_format) +static constexpr auto format = Core::IrSensor::ImageTransferProcessorFormat::Size40x30; +static constexpr std::size_t ImageWidth = 40; +static constexpr std::size_t ImageHeight = 30; + +MomentProcessor::MomentProcessor(Core::HID::HIDCore& hid_core_, + Core::IrSensor::DeviceFormat& device_format, + std::size_t npad_index) : device(device_format) { + npad_device = hid_core_.GetEmulatedControllerByIndex(npad_index); + device.mode = Core::IrSensor::IrSensorMode::MomentProcessor; device.camera_status = Core::IrSensor::IrCameraStatus::Unconnected; device.camera_internal_status = Core::IrSensor::IrCameraInternalStatus::Stopped; + + shared_memory = std::construct_at( + reinterpret_cast(&device_format.state.processor_raw_data)); + + Core::HID::ControllerUpdateCallback engine_callback{ + .on_change = [this](Core::HID::ControllerTriggerType type) { OnControllerUpdate(type); }, + .is_npad_service = true, + }; + callback_key = npad_device->SetCallback(engine_callback); } -MomentProcessor::~MomentProcessor() = default; +MomentProcessor::~MomentProcessor() { + npad_device->DeleteCallback(callback_key); +}; -void MomentProcessor::StartProcessor() {} +void MomentProcessor::StartProcessor() { + device.camera_status = Core::IrSensor::IrCameraStatus::Available; + device.camera_internal_status = Core::IrSensor::IrCameraInternalStatus::Ready; +} void MomentProcessor::SuspendProcessor() {} void MomentProcessor::StopProcessor() {} +void MomentProcessor::OnControllerUpdate(Core::HID::ControllerTriggerType type) { + if (type != Core::HID::ControllerTriggerType::IrSensor) { + return; + } + + next_state = {}; + const auto& camera_data = npad_device->GetCamera(); + + const auto window_width = static_cast(current_config.window_of_interest.width); + const auto window_height = static_cast(current_config.window_of_interest.height); + const auto window_start_x = static_cast(current_config.window_of_interest.x); + const auto window_start_y = static_cast(current_config.window_of_interest.y); + + const std::size_t block_width = window_width / Columns; + const std::size_t block_height = window_height / Rows; + + for (std::size_t row = 0; row < Rows; row++) { + for (std::size_t column = 0; column < Columns; column++) { + const size_t x_pos = (column * block_width) + window_start_x; + const size_t y_pos = (row * block_height) + window_start_y; + auto& statistic = next_state.statistic[column + (row * Columns)]; + statistic = GetStatistic(camera_data.data, x_pos, y_pos, block_width, block_height); + } + } + + next_state.sampling_number = camera_data.sample; + next_state.timestamp = next_state.timestamp + 131; + next_state.ambient_noise_level = Core::IrSensor::CameraAmbientNoiseLevel::Low; + shared_memory->moment_lifo.WriteNextEntry(next_state); + + if (!IsProcessorActive()) { + StartProcessor(); + } +} + +u8 MomentProcessor::GetPixel(const std::vector& data, std::size_t x, std::size_t y) const { + if ((y * ImageWidth) + x >= data.size()) { + return 0; + } + return data[(y * ImageWidth) + x]; +} + +MomentProcessor::MomentStatistic MomentProcessor::GetStatistic(const std::vector& data, + std::size_t start_x, + std::size_t start_y, + std::size_t width, + std::size_t height) const { + // The actual implementation is always 320x240 + static constexpr std::size_t RealWidth = 320; + static constexpr std::size_t RealHeight = 240; + static constexpr std::size_t Threshold = 30; + MomentStatistic statistic{}; + std::size_t active_points{}; + + // Sum all data points on the block that meet with the threshold + for (std::size_t y = 0; y < width; y++) { + for (std::size_t x = 0; x < height; x++) { + const size_t x_pos = x + start_x; + const size_t y_pos = y + start_y; + const auto pixel = + GetPixel(data, x_pos * ImageWidth / RealWidth, y_pos * ImageHeight / RealHeight); + + if (pixel < Threshold) { + continue; + } + + statistic.average_intensity += pixel; + + statistic.centroid.x += static_cast(x_pos); + statistic.centroid.y += static_cast(y_pos); + + active_points++; + } + } + + // Return an empty field if no points were available + if (active_points == 0) { + return {}; + } + + // Finally calculate the actual centroid and average intensity + statistic.centroid.x /= static_cast(active_points); + statistic.centroid.y /= static_cast(active_points); + statistic.average_intensity /= static_cast(width * height); + + return statistic; +} + void MomentProcessor::SetConfig(Core::IrSensor::PackedMomentProcessorConfig config) { current_config.camera_config.exposure_time = config.camera_config.exposure_time; current_config.camera_config.gain = config.camera_config.gain; @@ -29,6 +141,8 @@ void MomentProcessor::SetConfig(Core::IrSensor::PackedMomentProcessorConfig conf current_config.preprocess = static_cast(config.preprocess); current_config.preprocess_intensity_threshold = config.preprocess_intensity_threshold; + + npad_device->SetCameraFormat(format); } } // namespace Service::IRS diff --git a/src/core/hle/service/hid/irsensor/moment_processor.h b/src/core/hle/service/hid/irsensor/moment_processor.h index 5d70c340c..bb8fd7479 100755 --- a/src/core/hle/service/hid/irsensor/moment_processor.h +++ b/src/core/hle/service/hid/irsensor/moment_processor.h @@ -6,12 +6,14 @@ #include "common/bit_field.h" #include "common/common_types.h" #include "core/hid/irs_types.h" +#include "core/hle/service/hid/irs_ring_lifo.h" #include "core/hle/service/hid/irsensor/processor_base.h" namespace Service::IRS { class MomentProcessor final : public ProcessorBase { public: - explicit MomentProcessor(Core::IrSensor::DeviceFormat& device_format); + explicit MomentProcessor(Core::HID::HIDCore& hid_core_, + Core::IrSensor::DeviceFormat& device_format, std::size_t npad_index); ~MomentProcessor() override; // Called when the processor is initialized @@ -27,6 +29,9 @@ public: void SetConfig(Core::IrSensor::PackedMomentProcessorConfig config); private: + static constexpr std::size_t Columns = 8; + static constexpr std::size_t Rows = 6; + // This is nn::irsensor::MomentProcessorConfig struct MomentProcessorConfig { Core::IrSensor::CameraConfig camera_config; @@ -50,12 +55,27 @@ private: u64 timestamp; Core::IrSensor::CameraAmbientNoiseLevel ambient_noise_level; INSERT_PADDING_BYTES(4); - std::array stadistic; + std::array statistic; }; static_assert(sizeof(MomentProcessorState) == 0x258, "MomentProcessorState is an invalid size"); + struct MomentSharedMemory { + Service::IRS::Lifo moment_lifo; + }; + static_assert(sizeof(MomentSharedMemory) == 0xE20, "MomentSharedMemory is an invalid size"); + + void OnControllerUpdate(Core::HID::ControllerTriggerType type); + u8 GetPixel(const std::vector& data, std::size_t x, std::size_t y) const; + MomentStatistic GetStatistic(const std::vector& data, std::size_t start_x, + std::size_t start_y, std::size_t width, std::size_t height) const; + + MomentSharedMemory* shared_memory = nullptr; + MomentProcessorState next_state{}; + MomentProcessorConfig current_config{}; Core::IrSensor::DeviceFormat& device; + Core::HID::EmulatedController* npad_device; + int callback_key{}; }; } // namespace Service::IRS diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index c047767b4..1f35c2c58 100755 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -567,9 +567,10 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri QAction* verify_integrity = context_menu.addAction(tr("Verify Integrity")); QAction* copy_tid = context_menu.addAction(tr("Copy Title ID to Clipboard")); QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); +// TODO: Implement shortcut creation for macOS +#if !defined(__APPLE__) QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut")); QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop")); -#ifndef WIN32 QAction* create_applications_menu_shortcut = shortcut_menu->addAction(tr("Add to Applications Menu")); #endif @@ -647,10 +648,11 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); }); +// TODO: Implement shortcut creation for macOS +#if !defined(__APPLE__) connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path]() { emit CreateShortcut(program_id, path, GameListShortcutTarget::Desktop); }); -#ifndef WIN32 connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path]() { emit CreateShortcut(program_id, path, GameListShortcutTarget::Applications); }); diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 43281d43e..448bb512d 100755 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -10,6 +10,7 @@ #include #include "core/loader/nca.h" #include "core/tools/renderdoc.h" + #ifdef __APPLE__ #include // for chdir #endif @@ -2846,170 +2847,259 @@ void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id, QDesktopServices::openUrl(QUrl(QStringLiteral("https://yuzu-emu.org/game/") + directory)); } +bool GMainWindow::CreateShortcutLink(const std::filesystem::path& shortcut_path, + const std::string& comment, + const std::filesystem::path& icon_path, + const std::filesystem::path& command, + const std::string& arguments, const std::string& categories, + const std::string& keywords, const std::string& name) try { +#if defined(__linux__) || defined(__FreeBSD__) // Linux and FreeBSD + std::filesystem::path shortcut_path_full = shortcut_path / (name + ".desktop"); + std::ofstream shortcut_stream(shortcut_path_full, std::ios::binary | std::ios::trunc); + if (!shortcut_stream.is_open()) { + LOG_ERROR(Frontend, "Failed to create shortcut"); + return false; + } + // TODO: Migrate fmt::print to std::print in futures STD C++ 23. + fmt::print(shortcut_stream, "[Desktop Entry]\n"); + fmt::print(shortcut_stream, "Type=Application\n"); + fmt::print(shortcut_stream, "Version=1.0\n"); + fmt::print(shortcut_stream, "Name={}\n", name); + if (!comment.empty()) { + fmt::print(shortcut_stream, "Comment={}\n", comment); + } + if (std::filesystem::is_regular_file(icon_path)) { + fmt::print(shortcut_stream, "Icon={}\n", icon_path.string()); + } + fmt::print(shortcut_stream, "TryExec={}\n", command.string()); + fmt::print(shortcut_stream, "Exec={} {}\n", command.string(), arguments); + if (!categories.empty()) { + fmt::print(shortcut_stream, "Categories={}\n", categories); + } + if (!keywords.empty()) { + fmt::print(shortcut_stream, "Keywords={}\n", keywords); + } + return true; +#elif defined(_WIN32) // Windows + HRESULT hr = CoInitialize(nullptr); + if (FAILED(hr)) { + LOG_ERROR(Frontend, "CoInitialize failed"); + return false; + } + SCOPE_EXIT({ CoUninitialize(); }); + IShellLinkW* ps1 = nullptr; + IPersistFile* persist_file = nullptr; + SCOPE_EXIT({ + if (persist_file != nullptr) { + persist_file->Release(); + } + if (ps1 != nullptr) { + ps1->Release(); + } + }); + HRESULT hres = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_IShellLinkW, + reinterpret_cast(&ps1)); + if (FAILED(hres)) { + LOG_ERROR(Frontend, "Failed to create IShellLinkW instance"); + return false; + } + hres = ps1->SetPath(command.c_str()); + if (FAILED(hres)) { + LOG_ERROR(Frontend, "Failed to set path"); + return false; + } + if (!arguments.empty()) { + hres = ps1->SetArguments(Common::UTF8ToUTF16W(arguments).data()); + if (FAILED(hres)) { + LOG_ERROR(Frontend, "Failed to set arguments"); + return false; + } + } + if (!comment.empty()) { + hres = ps1->SetDescription(Common::UTF8ToUTF16W(comment).data()); + if (FAILED(hres)) { + LOG_ERROR(Frontend, "Failed to set description"); + return false; + } + } + if (std::filesystem::is_regular_file(icon_path)) { + hres = ps1->SetIconLocation(icon_path.c_str(), 0); + if (FAILED(hres)) { + LOG_ERROR(Frontend, "Failed to set icon location"); + return false; + } + } + hres = ps1->QueryInterface(IID_IPersistFile, reinterpret_cast(&persist_file)); + if (FAILED(hres)) { + LOG_ERROR(Frontend, "Failed to get IPersistFile interface"); + return false; + } + hres = persist_file->Save(std::filesystem::path{shortcut_path / (name + ".lnk")}.c_str(), TRUE); + if (FAILED(hres)) { + LOG_ERROR(Frontend, "Failed to save shortcut"); + return false; + } + return true; +#else // Unsupported platform + return false; +#endif +} catch (const std::exception& e) { + LOG_ERROR(Frontend, "Failed to create shortcut: {}", e.what()); + return false; +} +// Messages in pre-defined message boxes for less code spaghetti +bool GMainWindow::CreateShortcutMessagesGUI(QWidget* parent, int imsg, const QString& game_title) { + int result = 0; + QMessageBox::StandardButtons buttons; + switch (imsg) { + case GMainWindow::CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES: + buttons = QMessageBox::Yes | QMessageBox::No; + result = + QMessageBox::information(parent, tr("Create Shortcut"), + tr("Do you want to launch the game in fullscreen?"), buttons); + return result == QMessageBox::Yes; + case GMainWindow::CREATE_SHORTCUT_MSGBOX_SUCCESS: + QMessageBox::information(parent, tr("Create Shortcut"), + tr("Successfully created a shortcut to %1").arg(game_title)); + return false; + case GMainWindow::CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING: + buttons = QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Cancel; + result = + QMessageBox::warning(this, tr("Create Shortcut"), + tr("This will create a shortcut to the current AppImage. This may " + "not work well if you update. Continue?"), + buttons); + return result == QMessageBox::Ok; + default: + buttons = QMessageBox::Ok; + QMessageBox::critical(parent, tr("Create Shortcut"), + tr("Failed to create a shortcut to %1").arg(game_title), buttons); + return false; + } +} + +bool GMainWindow::MakeShortcutIcoPath(const u64 program_id, const std::string_view game_file_name, + std::filesystem::path& out_icon_path) { + // Get path to Yuzu icons directory & icon extension + std::string ico_extension = "png"; +#if defined(_WIN32) + out_icon_path = Common::FS::GetYuzuPath(Common::FS::YuzuPath::IconsDir); + ico_extension = "ico"; +#elif defined(__linux__) || defined(__FreeBSD__) + out_icon_path = Common::FS::GetDataDirectory("XDG_DATA_HOME") / "icons/hicolor/256x256"; +#endif + // Create icons directory if it doesn't exist + if (!Common::FS::CreateDirs(out_icon_path)) { + QMessageBox::critical( + this, tr("Create Icon"), + tr("Cannot create icon file. Path \"%1\" does not exist and cannot be created.") + .arg(QString::fromStdString(out_icon_path.string())), + QMessageBox::StandardButton::Ok); + out_icon_path.clear(); + return false; + } + + // Create icon file path + out_icon_path /= (program_id == 0 ? fmt::format("yuzu-{}.{}", game_file_name, ico_extension) + : fmt::format("yuzu-{:016X}.{}", program_id, ico_extension)); + return true; +} + void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& game_path, GameListShortcutTarget target) { + std::string game_title; + QString qt_game_title; + std::filesystem::path out_icon_path; // Get path to yuzu executable const QStringList args = QApplication::arguments(); std::filesystem::path yuzu_command = args[0].toStdString(); - // If relative path, make it an absolute path if (yuzu_command.c_str()[0] == '.') { yuzu_command = Common::FS::GetCurrentDir() / yuzu_command; } - + // Shortcut path + std::filesystem::path shortcut_path{}; + if (target == GameListShortcutTarget::Desktop) { + shortcut_path = + QStandardPaths::writableLocation(QStandardPaths::DesktopLocation).toStdString(); + } else if (target == GameListShortcutTarget::Applications) { + shortcut_path = + QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation).toStdString(); + } + // Icon path and title + if (std::filesystem::exists(shortcut_path)) { + // Get title from game file + const FileSys::PatchManager pm{program_id, system->GetFileSystemController(), + system->GetContentProvider()}; + const auto control = pm.GetControlMetadata(); + const auto loader = + Loader::GetLoader(*system, vfs->OpenFile(game_path, FileSys::Mode::Read)); + game_title = fmt::format("{:016X}", program_id); + if (control.first != nullptr) { + game_title = control.first->GetApplicationName(); + } else { + loader->ReadTitle(game_title); + } + // Delete illegal characters from title + const std::string illegal_chars = "<>:\"/\\|?*."; + for (auto it = game_title.rbegin(); it != game_title.rend(); ++it) { + if (illegal_chars.find(*it) != std::string::npos) { + game_title.erase(it.base() - 1); + } + } + qt_game_title = QString::fromStdString(game_title); + // Get icon from game file + std::vector icon_image_file{}; + if (control.second != nullptr) { + icon_image_file = control.second->ReadAllBytes(); + } else if (loader->ReadIcon(icon_image_file) != Loader::ResultStatus::Success) { + LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path); + } + QImage icon_data = + QImage::fromData(icon_image_file.data(), static_cast(icon_image_file.size())); + if (GMainWindow::MakeShortcutIcoPath(program_id, game_title, out_icon_path)) { + if (!SaveIconToFile(out_icon_path, icon_data)) { + LOG_ERROR(Frontend, "Could not write icon to file"); + } + } + } else { + GMainWindow::CreateShortcutMessagesGUI(this, GMainWindow::CREATE_SHORTCUT_MSGBOX_ERROR, + qt_game_title); + LOG_ERROR(Frontend, "Invalid shortcut target"); + return; + } #if defined(__linux__) + // Special case for AppImages // Warn once if we are making a shortcut to a volatile AppImage const std::string appimage_ending = std::string(Common::g_scm_rev).substr(0, 9).append(".AppImage"); if (yuzu_command.string().ends_with(appimage_ending) && !UISettings::values.shortcut_already_warned) { - if (QMessageBox::warning(this, tr("Create Shortcut"), - tr("This will create a shortcut to the current AppImage. This may " - "not work well if you update. Continue?"), - QMessageBox::StandardButton::Ok | - QMessageBox::StandardButton::Cancel) == - QMessageBox::StandardButton::Cancel) { + if (GMainWindow::CreateShortcutMessagesGUI( + this, GMainWindow::CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING, qt_game_title)) { return; } UISettings::values.shortcut_already_warned = true; } #endif // __linux__ - - std::filesystem::path target_directory{}; - - switch (target) { - case GameListShortcutTarget::Desktop: { - const QString desktop_path = - QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); - target_directory = desktop_path.toUtf8().toStdString(); - break; + // Create shortcut + std::string arguments = fmt::format("-g \"{:s}\"", game_path); + if (GMainWindow::CreateShortcutMessagesGUI( + this, GMainWindow::CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES, qt_game_title)) { + arguments = "-f " + arguments; } - case GameListShortcutTarget::Applications: { - const QString applications_path = - QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation); - if (applications_path.isEmpty()) { - const char* home = std::getenv("HOME"); - if (home != nullptr) { - target_directory = std::filesystem::path(home) / ".local/share/applications"; - } - } else { - target_directory = applications_path.toUtf8().toStdString(); - } - break; - } - default: - return; - } - - const QDir dir(QString::fromStdString(target_directory.generic_string())); - if (!dir.exists()) { - QMessageBox::critical(this, tr("Create Shortcut"), - tr("Cannot create shortcut. Path \"%1\" does not exist.") - .arg(QString::fromStdString(target_directory.generic_string())), - QMessageBox::StandardButton::Ok); - return; - } - - const std::string game_file_name = std::filesystem::path(game_path).filename().string(); - // Determine full paths for icon and shortcut -#if defined(__linux__) || defined(__FreeBSD__) || defined(__APPLE__) - const char* home = std::getenv("HOME"); - const std::filesystem::path home_path = (home == nullptr ? "~" : home); - const char* xdg_data_home = std::getenv("XDG_DATA_HOME"); - - std::filesystem::path system_icons_path = - (xdg_data_home == nullptr ? home_path / ".local/share/" - : std::filesystem::path(xdg_data_home)) / - "icons/hicolor/256x256"; - if (!Common::FS::CreateDirs(system_icons_path)) { - QMessageBox::critical( - this, tr("Create Icon"), - tr("Cannot create icon file. Path \"%1\" does not exist and cannot be created.") - .arg(QString::fromStdString(system_icons_path)), - QMessageBox::StandardButton::Ok); - return; - } - std::filesystem::path icon_path = - system_icons_path / (program_id == 0 ? fmt::format("yuzu-{}.png", game_file_name) - : fmt::format("yuzu-{:016X}.png", program_id)); - const std::filesystem::path shortcut_path = - target_directory / (program_id == 0 ? fmt::format("yuzu-{}.desktop", game_file_name) - : fmt::format("yuzu-{:016X}.desktop", program_id)); -#elif defined(WIN32) - std::filesystem::path icons_path = - Common::FS::GetYuzuPathString(Common::FS::YuzuPath::IconsDir); - std::filesystem::path icon_path = - icons_path / ((program_id == 0 ? fmt::format("yuzu-{}.ico", game_file_name) - : fmt::format("yuzu-{:016X}.ico", program_id))); -#else - std::string icon_extension; -#endif - - // Get title from game file - const FileSys::PatchManager pm{program_id, system->GetFileSystemController(), - system->GetContentProvider()}; - const auto control = pm.GetControlMetadata(); - const auto loader = Loader::GetLoader(*system, vfs->OpenFile(game_path, FileSys::Mode::Read)); - - std::string title{fmt::format("{:016X}", program_id)}; - - if (control.first != nullptr) { - title = control.first->GetApplicationName(); - } else { - loader->ReadTitle(title); - } - - // Get icon from game file - std::vector icon_image_file{}; - if (control.second != nullptr) { - icon_image_file = control.second->ReadAllBytes(); - } else if (loader->ReadIcon(icon_image_file) != Loader::ResultStatus::Success) { - LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path); - } - - QImage icon_data = - QImage::fromData(icon_image_file.data(), static_cast(icon_image_file.size())); -#if defined(__linux__) || defined(__FreeBSD__) || defined(__APPLE__) - // Convert and write the icon as a PNG - if (!icon_data.save(QString::fromStdString(icon_path.string()))) { - LOG_ERROR(Frontend, "Could not write icon as PNG to file"); - } else { - LOG_INFO(Frontend, "Wrote an icon to {}", icon_path.string()); - } -#elif defined(WIN32) - if (!SaveIconToFile(icon_path.string(), icon_data)) { - LOG_ERROR(Frontend, "Could not write icon to file"); - return; - } -#endif // __linux__ - -#ifdef _WIN32 - // Replace characters that are illegal in Windows filenames by a dash - const std::string illegal_chars = "<>:\"/\\|?*"; - for (char c : illegal_chars) { - std::replace(title.begin(), title.end(), c, '_'); - } - const std::filesystem::path shortcut_path = target_directory / (title + ".lnk").c_str(); -#endif - - const std::string comment = - tr("Start %1 with the yuzu Emulator").arg(QString::fromStdString(title)).toStdString(); - const std::string arguments = fmt::format("-g \"{:s}\"", game_path); + const std::string comment = fmt::format("Start {:s} with the yuzu Emulator", game_title); const std::string categories = "Game;Emulator;Qt;"; const std::string keywords = "Switch;Nintendo;"; - if (!CreateShortcut(shortcut_path.string(), title, comment, icon_path.string(), - yuzu_command.string(), arguments, categories, keywords)) { - QMessageBox::critical(this, tr("Create Shortcut"), - tr("Failed to create a shortcut at %1") - .arg(QString::fromStdString(shortcut_path.string()))); + if (GMainWindow::CreateShortcutLink(shortcut_path, comment, out_icon_path, yuzu_command, + arguments, categories, keywords, game_title)) { + GMainWindow::CreateShortcutMessagesGUI(this, GMainWindow::CREATE_SHORTCUT_MSGBOX_SUCCESS, + qt_game_title); return; } - - LOG_INFO(Frontend, "Wrote a shortcut to {}", shortcut_path.string()); - QMessageBox::information( - this, tr("Create Shortcut"), - tr("Successfully created a shortcut to %1").arg(QString::fromStdString(title))); + GMainWindow::CreateShortcutMessagesGUI(this, GMainWindow::CREATE_SHORTCUT_MSGBOX_ERROR, + qt_game_title); } void GMainWindow::OnGameListOpenDirectory(const QString& directory) { @@ -4004,66 +4094,6 @@ void GMainWindow::OpenPerGameConfiguration(u64 title_id, const std::string& file } } -bool GMainWindow::CreateShortcut(const std::string& shortcut_path, const std::string& title, - const std::string& comment, const std::string& icon_path, - const std::string& command, const std::string& arguments, - const std::string& categories, const std::string& keywords) { -#if defined(__linux__) || defined(__FreeBSD__) || defined(__APPLE__) - // This desktop file template was writing referencing - // https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.0.html - std::string shortcut_contents{}; - shortcut_contents.append("[Desktop Entry]\n"); - shortcut_contents.append("Type=Application\n"); - shortcut_contents.append("Version=1.0\n"); - shortcut_contents.append(fmt::format("Name={:s}\n", title)); - shortcut_contents.append(fmt::format("Comment={:s}\n", comment)); - shortcut_contents.append(fmt::format("Icon={:s}\n", icon_path)); - shortcut_contents.append(fmt::format("TryExec={:s}\n", command)); - shortcut_contents.append(fmt::format("Exec={:s} {:s}\n", command, arguments)); - shortcut_contents.append(fmt::format("Categories={:s}\n", categories)); - shortcut_contents.append(fmt::format("Keywords={:s}\n", keywords)); - - std::ofstream shortcut_stream(shortcut_path); - if (!shortcut_stream.is_open()) { - LOG_WARNING(Common, "Failed to create file {:s}", shortcut_path); - return false; - } - shortcut_stream << shortcut_contents; - shortcut_stream.close(); - - return true; -#elif defined(WIN32) - IShellLinkW* shell_link; - auto hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLinkW, - (void**)&shell_link); - if (FAILED(hres)) { - return false; - } - shell_link->SetPath( - Common::UTF8ToUTF16W(command).data()); // Path to the object we are referring to - shell_link->SetArguments(Common::UTF8ToUTF16W(arguments).data()); - shell_link->SetDescription(Common::UTF8ToUTF16W(comment).data()); - shell_link->SetIconLocation(Common::UTF8ToUTF16W(icon_path).data(), 0); - - IPersistFile* persist_file; - hres = shell_link->QueryInterface(IID_IPersistFile, (void**)&persist_file); - if (FAILED(hres)) { - return false; - } - - hres = persist_file->Save(Common::UTF8ToUTF16W(shortcut_path).data(), TRUE); - if (FAILED(hres)) { - return false; - } - - persist_file->Release(); - shell_link->Release(); - - return true; -#endif - return false; -} - void GMainWindow::OnLoadAmiibo() { if (emu_thread == nullptr || !emu_thread->IsRunning()) { return; @@ -4102,7 +4132,6 @@ void GMainWindow::OnLoadAmiibo() { bool GMainWindow::question(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { - QMessageBox* box_dialog = new QMessageBox(parent); box_dialog->setWindowTitle(title); box_dialog->setText(text); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index acc2b1160..a286efc84 100755 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -174,6 +175,13 @@ class GMainWindow : public QMainWindow { UI_EMU_STOPPING, }; + enum { + CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES, + CREATE_SHORTCUT_MSGBOX_SUCCESS, + CREATE_SHORTCUT_MSGBOX_ERROR, + CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING, + }; + public: void filterBarSetChecked(bool state); void UpdateUITheme(); @@ -456,11 +464,14 @@ private: bool ConfirmShutdownGame(); QString GetTasStateDescription() const; - bool CreateShortcut(const std::string& shortcut_path, const std::string& title, - const std::string& comment, const std::string& icon_path, - const std::string& command, const std::string& arguments, - const std::string& categories, const std::string& keywords); - + bool CreateShortcutMessagesGUI(QWidget* parent, int imsg, const QString& game_title); + bool MakeShortcutIcoPath(const u64 program_id, const std::string_view game_file_name, + std::filesystem::path& out_icon_path); + bool CreateShortcutLink(const std::filesystem::path& shortcut_path, const std::string& comment, + const std::filesystem::path& icon_path, + const std::filesystem::path& command, const std::string& arguments, + const std::string& categories, const std::string& keywords, + const std::string& name); /** * Mimic the behavior of QMessageBox::question but link controller navigation to the dialog * The only difference is that it returns a boolean. diff --git a/src/yuzu/util/util.cpp b/src/yuzu/util/util.cpp index be5f69335..b578e28ff 100755 --- a/src/yuzu/util/util.cpp +++ b/src/yuzu/util/util.cpp @@ -42,7 +42,7 @@ QPixmap CreateCirclePixmapFromColor(const QColor& color) { return circle_pixmap; } -bool SaveIconToFile(const std::string_view path, const QImage& image) { +bool SaveIconToFile(const std::filesystem::path& icon_path, const QImage& image) { #if defined(WIN32) #pragma pack(push, 2) struct IconDir { @@ -73,7 +73,7 @@ bool SaveIconToFile(const std::string_view path, const QImage& image) { .id_count = static_cast(scale_sizes.size()), }; - Common::FS::IOFile icon_file(path, Common::FS::FileAccessMode::Write, + Common::FS::IOFile icon_file(icon_path.string(), Common::FS::FileAccessMode::Write, Common::FS::FileType::BinaryFile); if (!icon_file.IsOpen()) { return false; @@ -134,6 +134,14 @@ bool SaveIconToFile(const std::string_view path, const QImage& image) { } icon_file.Close(); + return true; +#elif defined(__linux__) || defined(__FreeBSD__) + // Convert and write the icon as a PNG + if (!image.save(QString::fromStdString(icon_path.string()))) { + LOG_ERROR(Frontend, "Could not write icon as PNG to file"); + } else { + LOG_INFO(Frontend, "Wrote an icon to {}", icon_path.string()); + } return true; #else return false; diff --git a/src/yuzu/util/util.h b/src/yuzu/util/util.h index c82b17fe7..c5df018d6 100755 --- a/src/yuzu/util/util.h +++ b/src/yuzu/util/util.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include @@ -25,4 +26,4 @@ * @param image The image to save * @return bool If the operation succeeded */ -[[nodiscard]] bool SaveIconToFile(const std::string_view path, const QImage& image); +[[nodiscard]] bool SaveIconToFile(const std::filesystem::path& icon_path, const QImage& image); diff --git a/src/yuzu/vk_device_info.cpp b/src/yuzu/vk_device_info.cpp index 92f10d315..ab0d39c25 100755 --- a/src/yuzu/vk_device_info.cpp +++ b/src/yuzu/vk_device_info.cpp @@ -31,6 +31,7 @@ void PopulateRecords(std::vector& records, QWindow* window) try { // Create a test window with a Vulkan surface type for checking present modes. QWindow test_window(window); test_window.setSurfaceType(QWindow::VulkanSurface); + test_window.create(); auto wsi = QtCommon::GetWindowSystemInfo(&test_window); vk::InstanceDispatch dld;