From 831903799b51c675804c50fc0b7d0cd305dda844 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Sun, 2 Feb 2025 14:51:45 -0800 Subject: [PATCH 01/20] shader_recompiler: Insert end of divergence scope at last relevant instruction. (#2325) --- src/shader_recompiler/frontend/control_flow_graph.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/shader_recompiler/frontend/control_flow_graph.cpp b/src/shader_recompiler/frontend/control_flow_graph.cpp index ec5c117f7..12d2a2922 100644 --- a/src/shader_recompiler/frontend/control_flow_graph.cpp +++ b/src/shader_recompiler/frontend/control_flow_graph.cpp @@ -161,6 +161,12 @@ void CFG::EmitDivergenceLabels() { // scope. const auto start = inst_list.begin() + curr_begin + 1; if (!std::ranges::all_of(start, inst_list.begin() + index, IgnoresExecMask)) { + // Determine the last instruction affected by the exec mask, so that any + // trailing instructions not affected can be excluded from the scope. + s32 curr_end = index; + while (IgnoresExecMask(inst_list[curr_end - 1])) { + --curr_end; + } // Add a label to the instruction right after the open scope call. // It is the start of a new basic block. const auto& save_inst = inst_list[curr_begin]; @@ -173,8 +179,8 @@ void CFG::EmitDivergenceLabels() { // * Normal instruction at the end of the block // For the last case we must NOT add a label as that would cause // the instruction to be separated into its own basic block. - if (is_close) { - AddLabel(index_to_pc[index]); + if (curr_end != end_index - 1) { + AddLabel(index_to_pc[curr_end]); } } // Reset scope begin. From 460c266e045766e7d6a2c1e7b54a95124d476a62 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Sun, 2 Feb 2025 15:37:08 -0800 Subject: [PATCH 02/20] fix: Restore previous version of divergence PR. --- .../frontend/control_flow_graph.cpp | 65 +++++++++---------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/src/shader_recompiler/frontend/control_flow_graph.cpp b/src/shader_recompiler/frontend/control_flow_graph.cpp index 12d2a2922..126cb4eb6 100644 --- a/src/shader_recompiler/frontend/control_flow_graph.cpp +++ b/src/shader_recompiler/frontend/control_flow_graph.cpp @@ -148,47 +148,46 @@ void CFG::EmitDivergenceLabels() { const size_t end_index = GetIndex(end); s32 curr_begin = -1; + s32 last_exec_idx = -1; for (size_t index = GetIndex(start); index < end_index; index++) { const auto& inst = inst_list[index]; - const bool is_close = is_close_scope(inst); - if ((is_close || index == end_index - 1) && curr_begin != -1) { - // If there are no instructions inside scope don't do anything. - if (index - curr_begin == 1) { + if (curr_begin != -1) { + // Keep note of the last instruction that does not ignore exec, so we know where + // to end the divergence block without impacting trailing instructions that do. + if (!IgnoresExecMask(inst)) { + last_exec_idx = index; + } + // Consider a close scope on certain instruction types or at the last instruction + // before the next label. + if (is_close_scope(inst) || index == end_index - 1) { + // Only insert a scope if, since the open-scope instruction, there is at least + // one instruction that does not ignore exec. + if (index - curr_begin > 1 && last_exec_idx != -1) { + // Add a label to the instruction right after the open scope call. + // It is the start of a new basic block. + const auto& save_inst = inst_list[curr_begin]; + AddLabel(index_to_pc[curr_begin] + save_inst.length); + // Add a label to the close scope instruction. + // There are 3 cases where we need to close a scope. + // * Close scope instruction inside the block + // * Close scope instruction at the end of the block (cbranch or endpgm) + // * Normal instruction at the end of the block + // If the instruction we want to close the scope at is at the end of the + // block, we do not need to insert a new label. + if (last_exec_idx != end_index - 1) { + // Add the label after the last instruction affected by exec. + const auto& last_exec_inst = inst_list[last_exec_idx]; + AddLabel(index_to_pc[last_exec_idx] + last_exec_inst.length); + } + } + // Reset scope begin. curr_begin = -1; - continue; } - // If all instructions in the scope ignore exec masking, we shouldn't insert a - // scope. - const auto start = inst_list.begin() + curr_begin + 1; - if (!std::ranges::all_of(start, inst_list.begin() + index, IgnoresExecMask)) { - // Determine the last instruction affected by the exec mask, so that any - // trailing instructions not affected can be excluded from the scope. - s32 curr_end = index; - while (IgnoresExecMask(inst_list[curr_end - 1])) { - --curr_end; - } - // Add a label to the instruction right after the open scope call. - // It is the start of a new basic block. - const auto& save_inst = inst_list[curr_begin]; - const Label label = index_to_pc[curr_begin] + save_inst.length; - AddLabel(label); - // Add a label to the close scope instruction. - // There are 3 cases where we need to close a scope. - // * Close scope instruction inside the block - // * Close scope instruction at the end of the block (cbranch or endpgm) - // * Normal instruction at the end of the block - // For the last case we must NOT add a label as that would cause - // the instruction to be separated into its own basic block. - if (curr_end != end_index - 1) { - AddLabel(index_to_pc[curr_end]); - } - } - // Reset scope begin. - curr_begin = -1; } // Mark a potential start of an exec scope. if (is_open_scope(inst)) { curr_begin = index; + last_exec_idx = -1; } } } From c5cd4bc5cfade900bc4853a0e1a57b7032a5afd9 Mon Sep 17 00:00:00 2001 From: hspir404 Date: Mon, 3 Feb 2025 10:37:11 +0000 Subject: [PATCH 03/20] Remove log line that was consuming as much as 0.6ms frame time (#2335) --- src/core/libraries/kernel/process.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/libraries/kernel/process.cpp b/src/core/libraries/kernel/process.cpp index c21257c50..3a747bf16 100644 --- a/src/core/libraries/kernel/process.cpp +++ b/src/core/libraries/kernel/process.cpp @@ -13,7 +13,6 @@ namespace Libraries::Kernel { int PS4_SYSV_ABI sceKernelIsNeoMode() { - LOG_DEBUG(Kernel_Sce, "called"); return Config::isNeoModeConsole() && Common::ElfInfo::Instance().GetPSFAttributes().support_neo_mode; } From 0d65ef6f6e691af3133759e86d55a7cc521a5fa7 Mon Sep 17 00:00:00 2001 From: Missake212 Date: Mon, 3 Feb 2025 11:51:54 +0100 Subject: [PATCH 04/20] Adding french translation to game status and translating a forgotten sentence (#2315) * Add translation to game status and translate a forgotten sentence * menu * vulkan ts --- src/qt_gui/translations/fr.ts | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/qt_gui/translations/fr.ts b/src/qt_gui/translations/fr.ts index f2ea4fcc7..efaaa9ad1 100644 --- a/src/qt_gui/translations/fr.ts +++ b/src/qt_gui/translations/fr.ts @@ -742,7 +742,7 @@ Title Music - Title Music + Musique du titre Disable Trophy Pop-ups @@ -958,7 +958,7 @@ crashDiagnosticsCheckBox - Diagnostic de crash:\nCrée un fichier .yaml avec des informations sur l'état de Vulkan au moment du crash.\nUtile pour déboguer les erreurs "Device lost". Si cette option est activée, vous devez aussi activer Marqueur de débogage hôte ET invité.\nNe marche pas pour les GPUs Intel.\nVous devez activer le Vulkan Validation Layers ainsi que le Vulkan SDK pour que cela fonctionne. + Diagnostic de crash:\nCrée un fichier .yaml avec des informations sur l'état de Vulkan au moment du crash.\nUtile pour déboguer les erreurs "Device lost". Si cette option est activée, vous devez aussi activer Marqueur de débogage hôte ET invité.\nNe marche pas pour les GPUs Intel.\nVous devez activer la couche de validation Vulkan ainsi que le Vulkan SDK pour que cela fonctionne. copyGPUBuffersCheckBox @@ -1405,4 +1405,31 @@ TB + + CompatibilityInfoClass + + Unknown + Inconnu + + + Nothing + Rien + + + Boots + Démarre + + + Menus + Menu + + + Ingame + En jeu + + + Playable + Jouable + + From 56b2f6c4cf37aa0a55d2f5dc5a86fe9d1f2a3f30 Mon Sep 17 00:00:00 2001 From: Martin <67326368+Martini-141@users.noreply.github.com> Date: Mon, 3 Feb 2025 11:52:10 +0100 Subject: [PATCH 05/20] add missing translations nb (#2317) * add gamestatus tr nb * add missing compatibilityinfoclass tr nb * add missing GameListFrame tr nb --- src/qt_gui/translations/nb.ts | 49 ++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/qt_gui/translations/nb.ts b/src/qt_gui/translations/nb.ts index bce5791af..b2a355c95 100644 --- a/src/qt_gui/translations/nb.ts +++ b/src/qt_gui/translations/nb.ts @@ -1,7 +1,7 @@ - AboutDialog @@ -1298,6 +1298,14 @@ Game can be completed with playable performance and no major glitches Spillet kan fullføres med spillbar ytelse og uten store feil + + Click to go to issue + Klikk for å gå til rapporten + + + Last updated + Sist oppdatert + CheckUpdate @@ -1425,4 +1433,43 @@ TB + + CompatibilityInfoClass + + Unknown + Ukjent + + + Nothing + Ingenting + + + Boots + Starter opp + + + Menus + Meny + + + Ingame + I spill + + + Playable + Spillbar + + + Fetching compatibility data, please wait + Henter kompatibilitetsdata, vennligst vent + + + Cancel + Avbryt + + + Loading... + Laster... + + From 8d4261efba96a7d76b5202cbebc4894fa8340d82 Mon Sep 17 00:00:00 2001 From: DanielSvoboda Date: Mon, 3 Feb 2025 07:52:23 -0300 Subject: [PATCH 06/20] Cheats/Patches: Fix Mask Offset (#2323) --- src/common/memory_patcher.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/common/memory_patcher.cpp b/src/common/memory_patcher.cpp index f81a0ed83..899bb6bf3 100644 --- a/src/common/memory_patcher.cpp +++ b/src/common/memory_patcher.cpp @@ -174,7 +174,7 @@ void OnGameLoaded() { patchMask = MemoryPatcher::PatchMask::Mask_Jump32; MemoryPatcher::PatchMemory(currentPatchName, address, patchValue, false, - littleEndian, patchMask); + littleEndian, patchMask, maskOffsetValue); } } } @@ -278,6 +278,7 @@ void OnGameLoaded() { lineObject["Type"] = attributes.value("Type").toString(); lineObject["Address"] = attributes.value("Address").toString(); lineObject["Value"] = attributes.value("Value").toString(); + lineObject["Offset"] = attributes.value("Offset").toString(); linesArray.append(lineObject); } } @@ -321,7 +322,7 @@ void OnGameLoaded() { MemoryPatcher::PatchMemory(currentPatchName, address.toStdString(), patchValue.toStdString(), false, - littleEndian, patchMask); + littleEndian, patchMask, maskOffsetValue); } } } @@ -447,4 +448,4 @@ uintptr_t PatternScan(const std::string& signature) { return 0; } -} // namespace MemoryPatcher \ No newline at end of file +} // namespace MemoryPatcher From a55acae5ee26db97fede27498f2ac3b630c37742 Mon Sep 17 00:00:00 2001 From: Yury <27062841+f1amy@users.noreply.github.com> Date: Mon, 3 Feb 2025 15:52:41 +0500 Subject: [PATCH 07/20] Update ru_RU translation for 6.0 (#2318) * Update ru translation for 6.0 * Escape special characters for ru * Add and translate new strings for ru_RU * Translate `Auto Select` in GPU selection * Ru translation fixes - added translation for `Auto Select` * Fix typos in ru translation --- src/qt_gui/settings_dialog.cpp | 2 +- src/qt_gui/translations/ru_RU.ts | 3011 ++++++++++++++++-------------- 2 files changed, 1608 insertions(+), 1405 deletions(-) diff --git a/src/qt_gui/settings_dialog.cpp b/src/qt_gui/settings_dialog.cpp index 802325126..7505db106 100644 --- a/src/qt_gui/settings_dialog.cpp +++ b/src/qt_gui/settings_dialog.cpp @@ -72,7 +72,7 @@ SettingsDialog::SettingsDialog(std::span physical_devices, ui->buttonBox->button(QDialogButtonBox::StandardButton::Close)->setFocus(); // Add list of available GPUs - ui->graphicsAdapterBox->addItem("Auto Select"); // -1, auto selection + ui->graphicsAdapterBox->addItem(tr("Auto Select")); // -1, auto selection for (const auto& device : physical_devices) { ui->graphicsAdapterBox->addItem(device); } diff --git a/src/qt_gui/translations/ru_RU.ts b/src/qt_gui/translations/ru_RU.ts index 270396a6d..a8b3bacb4 100644 --- a/src/qt_gui/translations/ru_RU.ts +++ b/src/qt_gui/translations/ru_RU.ts @@ -1,1408 +1,1611 @@ - - - AboutDialog - - About shadPS4 - О shadPS4 - - - shadPS4 - shadPS4 - - - shadPS4 is an experimental open-source emulator for the PlayStation 4. - shadPS4 это экспериментальный эмулятор с открытым исходным кодом для PlayStation 4. - - - This software should not be used to play games you have not legally obtained. - Это програмное обеспечение не должно использоваться для запуска игр, которые вы получили нелегально. - - - - ElfViewer - - Open Folder - Открыть папку - - - - GameInfoClass - - Loading game list, please wait :3 - Загрузка списка игр, пожалуйста подождите :3 - - - Cancel - Отмена - - - Loading... - Загрузка... - - - - InstallDirSelect - - shadPS4 - Choose directory - shadPS4 - Выберите папку - - - Select which directory you want to install to. - Выберите папку, в которую вы хотите установить. - - - - GameInstallDialog - - shadPS4 - Choose directory - shadPS4 - Выберите папку - - - Directory to install games - Папка для установки игр - - - Browse - Обзор - - - Error - Ошибка - - - The value for location to install games is not valid. - Недопустимое значение местоположения для установки игр. - - - - GuiContextMenus - - Create Shortcut - Создать ярлык - - - Cheats / Patches - Читы и патчи - - - SFO Viewer - Просмотр SFO - - - Trophy Viewer - Просмотр трофеев - - - Open Folder... - Открыть папку... - - - Open Game Folder - Открыть папку с игрой - - - Open Save Data Folder - Открыть папку сохранений - - - Open Log Folder - Открыть папку логов - - - Copy info... - Копировать информацию... - - - Copy Name - Копировать имя - - - Copy Serial - Копировать серийный номер - - - Copy All - Копировать всё - - - Delete... - Удалить... - - - Delete Game - Удалить игру - - - Delete Update - Удалить обновление - - - Delete DLC - Удалить DLC - - - Compatibility... - Совместимость... - - - Update database - Обновить базу данных - - - View report - Посмотреть отчет - - - Submit a report - Отправить отчёт - - - Shortcut creation - Создание ярлыка - - - Shortcut created successfully! - Ярлык создан успешно! - - - Error - Ошибка - - - Error creating shortcut! - Ошибка создания ярлыка! - - - Install PKG - Установить PKG - - - Game - Игры - - - requiresEnableSeparateUpdateFolder_MSG - Эта функция требует включения настройки 'Отдельная папка обновлений'. Если вы хотите использовать эту функцию, пожалуйста включите её. - - - This game has no update to delete! - У этой игры нет обновлений для удаления! - - - Update - Обновления - - - This game has no DLC to delete! - У этой игры нет DLC для удаления! - - - DLC - DLC - - - Delete %1 - Удалить %1 - - - Are you sure you want to delete %1's %2 directory? - Вы уверены, что хотите удалить папку %2 %1? - - - - MainWindow - - Open/Add Elf Folder - Открыть/Добавить папку Elf - - - Install Packages (PKG) - Установить пакеты (PKG) - - - Boot Game - Запустить игру - - - Check for Updates - Проверить обновления - - - About shadPS4 - О shadPS4 - - - Configure... - Настроить... - - - Install application from a .pkg file - Установить приложение из файла .pkg - - - Recent Games - Недавние игры - - - Open shadPS4 Folder - Open shadPS4 Folder - - - Exit - Выход - - - Exit shadPS4 - Выйти из shadPS4 - - - Exit the application. - Выйти из приложения. - - - Show Game List - Показать список игр - - - Game List Refresh - Обновить список игр - - - Tiny - Крошечный - - - Small - Маленький - - - Medium - Средний - - - Large - Большой - - - List View - Список - - - Grid View - Сетка - - - Elf Viewer - Исполняемый файл - - - Game Install Directory - Каталог установки игры - - - Download Cheats/Patches - Скачать читы или патчи - - - Dump Game List - Дамп списка игр - - - PKG Viewer - Просмотр PKG - - - Search... - Поиск... - - - File - Файл - - - View - Вид - - - Game List Icons - Размер иконок списка игр - - - Game List Mode - Вид списка игр - - - Settings - Настройки - - - Utils - Утилиты - - - Themes - Темы - - - Help - Помощь - - - Dark - Темная - - - Light - Светлая - - - Green - Зеленая - - - Blue - Синяя - - - Violet - Фиолетовая - - - toolBar - Панель инструментов - - - Game List - Список игр - - - * Unsupported Vulkan Version - * Неподдерживаемая версия Vulkan - - - Download Cheats For All Installed Games - Скачать читы для всех установленных игр - - - Download Patches For All Games - Скачать патчи для всех игр - - - Download Complete - Скачивание завершено - - - You have downloaded cheats for all the games you have installed. - Вы скачали читы для всех установленных игр. - - - Patches Downloaded Successfully! - Патчи успешно скачаны! - - - All Patches available for all games have been downloaded. - Все доступные патчи для всех игр были скачаны. - - - Games: - Игры: - - - PKG File (*.PKG) - Файл PKG (*.PKG) - - - ELF files (*.bin *.elf *.oelf) - Файлы ELF (*.bin *.elf *.oelf) - - - Game Boot - Запуск игры - - - Only one file can be selected! - Можно выбрать только один файл! - - - PKG Extraction - Извлечение PKG - - - Patch detected! - Обнаружен патч! - - - PKG and Game versions match: - Версии PKG и игры совпадают: - - - Would you like to overwrite? - Хотите перезаписать? - - - PKG Version %1 is older than installed version: - Версия PKG %1 старее установленной версии: - - - Game is installed: - Игра установлена: - - - Would you like to install Patch: - Хотите установить патч: - - - DLC Installation - Установка DLC - - - Would you like to install DLC: %1? - Вы хотите установить DLC: %1? - - - DLC already installed: - DLC уже установлен: - - - Game already installed - Игра уже установлена - - - PKG is a patch, please install the game first! - PKG - это патч, сначала установите игру! - - - PKG ERROR - ОШИБКА PKG - - - Extracting PKG %1/%2 - Извлечение PKG %1/%2 - - - Extraction Finished - Извлечение завершено - - - Game successfully installed at %1 - Игра успешно установлена в %1 - - - File doesn't appear to be a valid PKG file - Файл не является допустимым файлом PKG - - - - PKGViewer - - Open Folder - Открыть папку - - - - TrophyViewer - - Trophy Viewer - Просмотр трофеев - - - - SettingsDialog - - Settings - Настройки - - - General - Общее - - - System - Система - - - Console Language - Язык консоли - - - Emulator Language - Язык эмулятора - - - Emulator - Эмулятор - - - Enable Fullscreen - Полноэкранный режим - - - Fullscreen Mode - Тип полноэкранного режима - - - Enable Separate Update Folder - Отдельная папка обновлений - - - Default tab when opening settings - Вкладка по умолчанию при открытии настроек - - - Show Game Size In List - Показать размер игры в списке - - - Show Splash - Показывать заставку - - - Is PS4 Pro - Режим PS4 Pro - - - Enable Discord Rich Presence - Включить Discord Rich Presence - - - Username - Имя пользователя - - - Trophy Key - Ключ трофеев - - - Trophy - Трофеи - - - Logger - Логирование - - - Log Type - Тип логов - - - Log Filter - Фильтр логов - - - Open Log Location - Открыть местоположение журнала - - - Input - Ввод - - - Cursor - Курсор мыши - - - Hide Cursor - Скрывать курсор - - - Hide Cursor Idle Timeout - Время скрытия курсора при бездействии - - - s - сек - - - Controller - Контроллер - - - Back Button Behavior - Поведение кнопки назад - - - Graphics - Графика - - - Gui - Интерфейс - - - User - Пользователь - - - Graphics Device - Графическое устройство - - - Width - Ширина - - - Height - Высота - - - Vblank Divider - Делитель Vblank - - - Advanced - Продвинутые - - - Enable Shaders Dumping - Включить дамп шейдеров - - - Enable NULL GPU - Включить NULL GPU - - - Paths - Пути - - - Game Folders - Игровые папки - - - Add... - Добавить... - - - Remove - Удалить - - - Debug - Отладка - - - Enable Debug Dumping - Включить отладочные дампы - - - Enable Vulkan Validation Layers - Включить слои валидации Vulkan - - - Enable Vulkan Synchronization Validation - Включить валидацию синхронизации Vulkan - - - Enable RenderDoc Debugging - Включить отладку RenderDoc - - - Enable Crash Diagnostics - Enable Crash Diagnostics - - - Collect Shaders - Collect Shaders - - - Copy GPU Buffers - Copy GPU Buffers - - - Host Debug Markers - Host Debug Markers - - - Guest Debug Markers - Guest Debug Markers - - - Update - Обновление - - - Check for Updates at Startup - Проверка при запуске - - - Update Channel - Канал обновления - - - Check for Updates - Проверить обновления - - - GUI Settings - Интерфейс - - - Title Music - Title Music - - - Disable Trophy Pop-ups - Отключить уведомления о трофеях - - - Play title music - Играть заглавную музыку - - - Update Compatibility Database On Startup - Обновлять базу совместимости при запуске - - - Game Compatibility - Совместимость игр - - - Display Compatibility Data - Показывать данные совместимости - - - Update Compatibility Database - Обновить базу совместимости - - - Volume - Громкость - - - Audio Backend - Звуковая подсистема - - - Save - Сохранить - - - Apply - Применить - - - Restore Defaults - По умолчанию - - - Close - Закрыть - - - Point your mouse at an option to display its description. - Наведите указатель мыши на опцию, чтобы отобразить ее описание. - - - consoleLanguageGroupBox - Язык консоли:\nУстановите язык, который будет использоваться в играх PS4.\nРекомендуется устанавливать язык который поддерживается игрой, так как он может отличаться в зависимости от региона. - - - emulatorLanguageGroupBox - Язык эмулятора:\nУстановите язык пользовательского интерфейса эмулятора. - - - fullscreenCheckBox - Полноэкранный режим:\nАвтоматически переводит игровое окно в полноэкранный режим.\nВы можете отключить это, нажав клавишу F11. - - - separateUpdatesCheckBox - Отдельная папка обновлений:\nПозволяет устанавливать обновления игры в отдельную папку для удобства. - - - showSplashCheckBox - Показывать заставку:\nОтображает заставку игры (специальное изображение) во время запуска. - - - ps4proCheckBox - Режим PS4 Pro:\nЗаставляет эмулятор работать как PS4 Pro, что может включить специальные функции в играх, поддерживающих это. - - - discordRPCCheckbox - Включить Discord Rich Presence:\nОтображает значок эмулятора и соответствующую информацию в вашем профиле Discord. - - - userName - Имя пользователя:\nУстановите имя пользователя аккаунта PS4. Это может отображаться в некоторых играх. - - - TrophyKey - Trophy Key:\nKey used to decrypt trophies. Must be obtained from your jailbroken console.\nMust contain only hex characters. - - - logTypeGroupBox - Тип логов:\nУстановите, синхронизировать ли вывод окна логов ради производительности. Это может негативно сказаться на эмуляции. - - - logFilter - Фильтр логов:\nФильтрует логи, чтобы показывать только определенную информацию.\nПримеры: "Core:Trace" "Lib.Pad:Debug Common.Filesystem:Error" "*:Critical" Уровни: Trace, Debug, Info, Warning, Error, Critical - в этом порядке, конкретный уровень глушит все предыдущие уровни в списке и показывает все последующие уровни. - - - updaterGroupBox - Обновление:\nRelease: Официальные версии, которые выпускаются каждый месяц и могут быть очень старыми, но они более надежные и проверенные.\nNightly: Версии разработки, которые содержат все последние функции и исправления, но могут содержать ошибки и менее стабильны. - - - GUIMusicGroupBox - Играть заглавную музыку:\nВключает воспроизведение специальной музыки при выборе игры в списке, если она это поддерживает. - - - disableTrophycheckBox - Отключить уведомления о трофеях:\nОтключает внутриигровые уведомления о трофеях. Прогресс трофеев по прежнему можно отслеживать в меню просмотра трофеев (правая кнопка мыши по игре в главном окне). - - - hideCursorGroupBox - Скрывать курсор:\nВыберите, когда курсор будет скрыт:\nНикогда: Вы всегда будете видеть курсор.\nПри бездействии: Установите время, через которое курсор будет скрыт при бездействии.\nВсегда: Курсор всегда будет скрыт. - - - idleTimeoutGroupBox - Установите время, через которое курсор исчезнет при бездействии. - - - backButtonBehaviorGroupBox - Поведение кнопки «Назад»:\nНастраивает кнопку «Назад» контроллера на эмуляцию нажатия на указанную область на сенсорной панели контроллера PS4. - - - enableCompatibilityCheckBox - Показывать данные совместимости:\nПоказывает информацию о совместимости игр в таблице. Включите «Обновлять базу совместимости при запуске» для получения актуальной информации. - - - checkCompatibilityOnStartupCheckBox - Обновлять базу совместимости при запуске:\nАвтоматически обновлять базу данных совместимости при запуске shadPS4. - - - updateCompatibilityButton - Обновить базу совместимости:\nНемедленно обновить базу данных совместимости. - - - Never - Никогда - - - Idle - При бездействии - - - Always - Всегда - - - Touchpad Left - Тачпад слева - - - Touchpad Right - Тачпад справа - - - Touchpad Center - Центр тачпада - - - None - Нет - - - graphicsAdapterGroupBox - Графическое устройство:\nВ системах с несколькими GPU выберите GPU, который будет использовать эмулятор.\nВыберите "Auto Select", чтобы определить его автоматически. - - - resolutionLayout - Ширина/Высота:\nУстановите размер окна эмулятора при запуске, который может быть изменен во время игры.\nЭто отличается от разрешения в игре. - - - heightDivider - Делитель Vblank:\nЧастота кадров, с которой обновляется эмулятор, умножается на это число. Изменение этого параметра может иметь негативные последствия, такие как увеличение скорости игры или нарушение критических функций игры, которые этого не ожидают! - - - dumpShadersCheckBox - Включить дамп шейдеров:\nДля технической отладки сохраняет шейдеры игр в папку во время рендеринга. - - - nullGpuCheckBox - Включить NULL GPU:\nДля технической отладки отключает рендеринг игры так, как будто графической карты нет. - - - gameFoldersBox - Игровые папки:\nСписок папок для проверки установленных игр. - - - addFolderButton - Добавить:\nДобавить папку в список. - - - removeFolderButton - Удалить:\nУдалить папку из списка. - - - debugDump - Включить отладочные дампы:\nСохраняет символы импорта, экспорта и информацию о заголовке файла текущей исполняемой программы PS4 в папку. - - - vkValidationCheckBox - Включить слои валидации Vulkan:\nВключает систему, которая проверяет состояние рендерера Vulkan и логирует информацию о его внутреннем состоянии. Это снизит производительность и, вероятно, изменит поведение эмуляции. - - - vkSyncValidationCheckBox - Включить валидацию синхронизации Vulkan:\nВключает систему, которая проверяет тайминг задач рендеринга Vulkan. Это снизит производительность и, вероятно, изменит поведение эмуляции. - - - rdocCheckBox - Включить отладку RenderDoc:\nЕсли включено, эмулятор обеспечит совместимость с Renderdoc, позволяя захватывать и анализировать текущие кадры во время рендеринга. - - - collectShaderCheckBox - Collect Shaders:\nYou need this enabled to edit shaders with the debug menu (Ctrl + F10). - - - crashDiagnosticsCheckBox - Crash Diagnostics:\nCreates a .yaml file with info about the Vulkan state at the time of crashing.\nUseful for debugging 'Device lost' errors. If you have this enabled, you should enable Host AND Guest Debug Markers.\nDoes not work on Intel GPUs.\nYou need Vulkan Validation Layers enabled and the Vulkan SDK for this to work. - - - copyGPUBuffersCheckBox - Copy GPU Buffers:\nGets around race conditions involving GPU submits.\nMay or may not help with PM4 type 0 crashes. - - - hostMarkersCheckBox - Host Debug Markers:\nInserts emulator-side information like markers for specific AMDGPU commands around Vulkan commands, as well as giving resources debug names.\nIf you have this enabled, you should enable Crash Diagnostics.\nUseful for programs like RenderDoc. - - - guestMarkersCheckBox - Guest Debug Markers:\nInserts any debug markers the game itself has added to the command buffer.\nIf you have this enabled, you should enable Crash Diagnostics.\nUseful for programs like RenderDoc. - - - - CheatsPatches - - Cheats / Patches for - Читы и патчи для - - - defaultTextEdit_MSG - Читы и патчи экспериментальны.\nИспользуйте с осторожностью.\n\nСкачивайте читы, выбрав репозиторий и нажав на кнопку загрузки.\nВо вкладке "Патчи" вы можете скачать все патчи сразу, выбирать какие вы хотите использовать, и сохранять выбор.\n\nПоскольку мы не разрабатываем читы/патчи,\nпожалуйста сообщайте о проблемах автору чита/патча.\n\nСоздали новый чит? Посетите:\nhttps://github.com/shadps4-emu/ps4_cheats - - - No Image Available - Изображение недоступно - - - Serial: - Серийный номер: - - - Version: - Версия: - - - Size: - Размер: - - - Select Cheat File: - Выберите файл чита: - - - Repository: - Репозиторий: - - - Download Cheats - Скачать читы - - - Delete File - Удалить файл - - - No files selected. - Файлы не выбраны. - - - You can delete the cheats you don't want after downloading them. - Вы можете удалить ненужные читы после их скачивания. - - - Do you want to delete the selected file?\n%1 - Вы хотите удалить выбранный файл?\n%1 - - - Select Patch File: - Выберите файл патча: - - - Download Patches - Скачать патчи - - - Save - Сохранить - - - Cheats - Читы - - - Patches - Патчи - - - Error - Ошибка - - - No patch selected. - Патч не выбран. - - - Unable to open files.json for reading. - Не удалось открыть файл files.json для чтения. - - - No patch file found for the current serial. - Не найден файл патча для текущего серийного номера. - - - Unable to open the file for reading. - Не удалось открыть файл для чтения. - - - Unable to open the file for writing. - Не удалось открыть файл для записи. - - - Failed to parse XML: - Не удалось разобрать XML: - - - Success - Успех - - - Options saved successfully. - Опции успешно сохранены. - - - Invalid Source - Неверный источник - - - The selected source is invalid. - Выбранный источник недействителен. - - - File Exists - Файл существует - - - File already exists. Do you want to replace it? - Файл уже существует. Хотите заменить его? - - - Failed to save file: - Не удалось сохранить файл: - - - Failed to download file: - Не удалось скачать файл: - - - Cheats Not Found - Читы не найдены - - - CheatsNotFound_MSG - Читы не найдены для этой игры в выбранном репозитории. Попробуйте другой репозиторий или другую версию игры. - - - Cheats Downloaded Successfully - Читы успешно скачаны - - - CheatsDownloadedSuccessfully_MSG - Вы успешно скачали читы для этой версии игры из выбранного репозитория. Вы можете попробовать скачать из другого репозитория. Если он доступен, его также можно будет использовать, выбрав файл из списка. - - - Failed to save: - Не удалось сохранить: - - - Failed to download: - Не удалось скачать: - - - Download Complete - Скачивание завершено - - - DownloadComplete_MSG - Патчи успешно скачаны! Все доступные патчи для всех игр были скачаны, нет необходимости скачивать их по отдельности для каждой игры, как это происходит с читами. Если патч не появляется, возможно, его не существует для конкретного серийного номера и версии игры. - - - Failed to parse JSON data from HTML. - Не удалось разобрать данные JSON из HTML. - - - Failed to retrieve HTML page. - Не удалось получить HTML-страницу. - - - The game is in version: %1 - Игра в версии: %1 - - - The downloaded patch only works on version: %1 - Скачанный патч работает только на версии: %1 - - - You may need to update your game. - Вам может понадобиться обновить игру. - - - Incompatibility Notice - Уведомление о несовместимости - - - Failed to open file: - Не удалось открыть файл: - - - XML ERROR: - ОШИБКА XML: - - - Failed to open files.json for writing - Не удалось открыть файл files.json для записи - - - Author: - Автор: - - - Directory does not exist: - Каталог не существует: - - - Failed to open files.json for reading. - Не удалось открыть файл files.json для чтения. - - - Name: - Имя: - - - Can't apply cheats before the game is started - Невозможно применить читы до начала игры - - - - GameListFrame - - Icon - Иконка - - - Name - Название - - - Serial - Серийный номер - - - Compatibility - Совместимость - - - Region - Регион - - - Firmware - Прошивка - - - Size - Размер - - - Version - Версия - - - Path - Путь - - - Play Time - Время в игре - - - Never Played - Нет - - - h - ч - - - m - м - - - s - с - - - Compatibility is untested - Совместимость не проверена - - - Game does not initialize properly / crashes the emulator - Игра не иницализируется правильно / крашит эмулятор - - - Game boots, but only displays a blank screen - Игра запускается, но показывает только пустой экран - - - Game displays an image but does not go past the menu - Игра показывает картинку, но не проходит дальше меню - - - Game has game-breaking glitches or unplayable performance - Игра имеет ломающие игру глюки или плохую производительность - - - Game can be completed with playable performance and no major glitches - Игра может быть пройдена с хорошей производительностью и без серьезных сбоев - - - - CheckUpdate - - Auto Updater - Автообновление - - - Error - Ошибка - - - Network error: - Сетевая ошибка: - - - Failed to parse update information. - Не удалось разобрать информацию об обновлении. - - - No pre-releases found. - Предварительных версий не найдено. - - - Invalid release data. - Недопустимые данные релиза. - - - No download URL found for the specified asset. - Не найден URL для загрузки указанного ресурса. - - - Your version is already up to date! - Ваша версия уже обновлена! - - - Update Available - Доступно обновление - - - Update Channel - Канал обновления - - - Current Version - Текущая версия - - - Latest Version - Последняя версия - - - Do you want to update? - Вы хотите обновиться? - - - Show Changelog - Показать журнал изменений - - - Check for Updates at Startup - Проверка при запуске - - - Update - Обновить - - - No - Нет - - - Hide Changelog - Скрыть журнал изменений - - - Changes - Журнал изменений - - - Network error occurred while trying to access the URL - Произошла сетевая ошибка при попытке доступа к URL - - - Download Complete - Скачивание завершено - - - The update has been downloaded, press OK to install. - Обновление загружено, нажмите OK для установки. - - - Failed to save the update file at - Не удалось сохранить файл обновления в - - - Starting Update... - Начало обновления... - - - Failed to create the update script file - Не удалось создать файл скрипта обновления - - - - GameListUtils - - B - Б - - - KB - КБ - - - MB - МБ - - - GB - ГБ - - - TB - ТБ - - + + + AboutDialog + + About shadPS4 + О shadPS4 + + + shadPS4 + shadPS4 + + + shadPS4 is an experimental open-source emulator for the PlayStation 4. + shadPS4 это экспериментальный эмулятор с открытым исходным кодом для PlayStation 4. + + + This software should not be used to play games you have not legally obtained. + Это программное обеспечение не должно использоваться для запуска игр, которые вы получили нелегально. + + + + ElfViewer + + Open Folder + Открыть папку + + + + GameInfoClass + + Loading game list, please wait :3 + Загрузка списка игр, пожалуйста подождите :3 + + + Cancel + Отмена + + + Loading... + Загрузка... + + + + InstallDirSelect + + shadPS4 - Choose directory + shadPS4 - Выберите папку + + + Select which directory you want to install to. + Выберите папку, в которую вы хотите установить. + + + Install All Queued to Selected Folder + Установить все из очереди в выбранную папку + + + Delete PKG File on Install + Удалить файл PKG при установке + + + + GameInstallDialog + + shadPS4 - Choose directory + shadPS4 - Выберите папку + + + Directory to install games + Каталог для установки игр + + + Browse + Обзор + + + Error + Ошибка + + + The value for location to install games is not valid. + Недопустимое значение местоположения для установки игр. + + + Directory to install DLC + Каталог для установки DLC + + + + GuiContextMenus + + Create Shortcut + Создать ярлык + + + Cheats / Patches + Читы и патчи + + + SFO Viewer + Просмотр SFO + + + Trophy Viewer + Просмотр трофеев + + + Open Folder... + Открыть папку... + + + Open Game Folder + Открыть папку с игрой + + + Open Save Data Folder + Открыть папку сохранений + + + Open Log Folder + Открыть папку логов + + + Copy info... + Копировать информацию... + + + Copy Name + Копировать имя + + + Copy Serial + Копировать серийный номер + + + Copy All + Копировать всё + + + Delete... + Удалить... + + + Delete Game + Удалить игру + + + Delete Update + Удалить обновление + + + Delete DLC + Удалить DLC + + + Compatibility... + Совместимость... + + + Update database + Обновить базу данных + + + View report + Посмотреть отчет + + + Submit a report + Отправить отчёт + + + Shortcut creation + Создание ярлыка + + + Shortcut created successfully! + Ярлык создан успешно! + + + Error + Ошибка + + + Error creating shortcut! + Ошибка создания ярлыка! + + + Install PKG + Установить PKG + + + Game + Игры + + + requiresEnableSeparateUpdateFolder_MSG + Эта функция требует включения настройки 'Отдельная папка обновлений'. Если вы хотите использовать эту функцию, пожалуйста включите её. + + + This game has no update to delete! + У этой игры нет обновлений для удаления! + + + Update + Обновления + + + This game has no DLC to delete! + У этой игры нет DLC для удаления! + + + DLC + DLC + + + Delete %1 + Удалить %1 + + + Are you sure you want to delete %1's %2 directory? + Вы уверены, что хотите удалить папку %2 %1? + + + Open Update Folder + Открыть папку обновлений + + + Delete Save Data + Удалить сохранения + + + This game has no update folder to open! + У этой игры нет папки обновлений, которую можно открыть! + + + Failed to convert icon. + Не удалось преобразовать иконку. + + + This game has no save data to delete! + У этой игры нет сохранений, которые можно удалить! + + + Save Data + Сохранения + + + + MainWindow + + Open/Add Elf Folder + Открыть/Добавить папку Elf + + + Install Packages (PKG) + Установить пакеты (PKG) + + + Boot Game + Запустить игру + + + Check for Updates + Проверить обновления + + + About shadPS4 + О shadPS4 + + + Configure... + Настроить... + + + Install application from a .pkg file + Установить приложение из файла .pkg + + + Recent Games + Недавние игры + + + Open shadPS4 Folder + Открыть папку shadPS4 + + + Exit + Выход + + + Exit shadPS4 + Выйти из shadPS4 + + + Exit the application. + Выйти из приложения. + + + Show Game List + Показать список игр + + + Game List Refresh + Обновить список игр + + + Tiny + Крошечный + + + Small + Маленький + + + Medium + Средний + + + Large + Большой + + + List View + Список + + + Grid View + Сетка + + + Elf Viewer + Исполняемый файл + + + Game Install Directory + Каталог установки игры + + + Download Cheats/Patches + Скачать читы или патчи + + + Dump Game List + Дамп списка игр + + + PKG Viewer + Просмотр PKG + + + Search... + Поиск... + + + File + Файл + + + View + Вид + + + Game List Icons + Размер иконок списка игр + + + Game List Mode + Вид списка игр + + + Settings + Настройки + + + Utils + Утилиты + + + Themes + Темы + + + Help + Помощь + + + Dark + Темная + + + Light + Светлая + + + Green + Зеленая + + + Blue + Синяя + + + Violet + Фиолетовая + + + toolBar + Панель инструментов + + + Game List + Список игр + + + * Unsupported Vulkan Version + * Неподдерживаемая версия Vulkan + + + Download Cheats For All Installed Games + Скачать читы для всех установленных игр + + + Download Patches For All Games + Скачать патчи для всех игр + + + Download Complete + Скачивание завершено + + + You have downloaded cheats for all the games you have installed. + Вы скачали читы для всех установленных игр. + + + Patches Downloaded Successfully! + Патчи успешно скачаны! + + + All Patches available for all games have been downloaded. + Все доступные патчи для всех игр были скачаны. + + + Games: + Игры: + + + PKG File (*.PKG) + Файл PKG (*.PKG) + + + ELF files (*.bin *.elf *.oelf) + Файлы ELF (*.bin *.elf *.oelf) + + + Game Boot + Запуск игры + + + Only one file can be selected! + Можно выбрать только один файл! + + + PKG Extraction + Извлечение PKG + + + Patch detected! + Обнаружен патч! + + + PKG and Game versions match: + Версии PKG и игры совпадают: + + + Would you like to overwrite? + Хотите перезаписать? + + + PKG Version %1 is older than installed version: + Версия PKG %1 старше установленной версии: + + + Game is installed: + Игра установлена: + + + Would you like to install Patch: + Хотите установить патч: + + + DLC Installation + Установка DLC + + + Would you like to install DLC: %1? + Вы хотите установить DLC: %1? + + + DLC already installed: + DLC уже установлен: + + + Game already installed + Игра уже установлена + + + PKG is a patch, please install the game first! + PKG - это патч, сначала установите игру! + + + PKG ERROR + ОШИБКА PKG + + + Extracting PKG %1/%2 + Извлечение PKG %1/%2 + + + Extraction Finished + Извлечение завершено + + + Game successfully installed at %1 + Игра успешно установлена в %1 + + + File doesn't appear to be a valid PKG file + Файл не является допустимым файлом PKG + + + Run Game + Запустить игру + + + Eboot.bin file not found + Файл eboot.bin не найден + + + PKG File (*.PKG *.pkg) + Файл PKG (*.PKG *.pkg) + + + PKG is a patch or DLC, please install the game first! + Выбранный PKG является патчем или DLC, пожалуйста, сначала установите игру! + + + Game is already running! + Игра уже запущена! + + + shadPS4 + shadPS4 + + + + PKGViewer + + Open Folder + Открыть папку + + + &File + Файл + + + PKG ERROR + ОШИБКА PKG + + + + TrophyViewer + + Trophy Viewer + Просмотр трофеев + + + + SettingsDialog + + Settings + Настройки + + + General + Общее + + + System + Система + + + Console Language + Язык консоли + + + Emulator Language + Язык эмулятора + + + Emulator + Эмулятор + + + Enable Fullscreen + Полноэкранный режим + + + Fullscreen Mode + Тип полноэкранного режима + + + Enable Separate Update Folder + Отдельная папка обновлений + + + Default tab when opening settings + Вкладка по умолчанию при открытии настроек + + + Show Game Size In List + Показать размер игры в списке + + + Show Splash + Показывать заставку + + + Is PS4 Pro + Режим PS4 Pro + + + Enable Discord Rich Presence + Включить Discord Rich Presence + + + Username + Имя пользователя + + + Trophy Key + Ключ трофеев + + + Trophy + Трофеи + + + Logger + Логирование + + + Log Type + Тип логов + + + Log Filter + Фильтр логов + + + Open Log Location + Открыть местоположение журнала + + + Input + Ввод + + + Cursor + Курсор мыши + + + Hide Cursor + Скрывать курсор + + + Hide Cursor Idle Timeout + Время скрытия курсора при бездействии + + + s + сек + + + Controller + Контроллер + + + Back Button Behavior + Поведение кнопки назад + + + Graphics + Графика + + + Gui + Интерфейс + + + User + Пользователь + + + Graphics Device + Графическое устройство + + + Width + Ширина + + + Height + Высота + + + Vblank Divider + Делитель Vblank + + + Advanced + Продвинутые + + + Enable Shaders Dumping + Включить дамп шейдеров + + + Enable NULL GPU + Включить NULL GPU + + + Paths + Пути + + + Game Folders + Игровые папки + + + Add... + Добавить... + + + Remove + Удалить + + + Debug + Отладка + + + Enable Debug Dumping + Включить отладочные дампы + + + Enable Vulkan Validation Layers + Включить слои валидации Vulkan + + + Enable Vulkan Synchronization Validation + Включить валидацию синхронизации Vulkan + + + Enable RenderDoc Debugging + Включить отладку RenderDoc + + + Enable Crash Diagnostics + Включить диагностику сбоев + + + Collect Shaders + Собирать шейдеры + + + Copy GPU Buffers + Копировать буферы GPU + + + Host Debug Markers + Маркеры отладки хоста + + + Guest Debug Markers + Маркеры отладки гостя + + + Update + Обновление + + + Check for Updates at Startup + Проверка при запуске + + + Update Channel + Канал обновления + + + Check for Updates + Проверить обновления + + + GUI Settings + Настройки интерфейса + + + Title Music + Заглавная музыка + + + Disable Trophy Pop-ups + Отключить уведомления о трофеях + + + Play title music + Играть заглавную музыку + + + Update Compatibility Database On Startup + Обновлять базу совместимости при запуске + + + Game Compatibility + Совместимость игр + + + Display Compatibility Data + Показывать данные совместимости + + + Update Compatibility Database + Обновить базу совместимости + + + Volume + Громкость + + + Audio Backend + Звуковая подсистема + + + Save + Сохранить + + + Apply + Применить + + + Restore Defaults + По умолчанию + + + Close + Закрыть + + + Point your mouse at an option to display its description. + Наведите указатель мыши на опцию, чтобы отобразить ее описание. + + + consoleLanguageGroupBox + Язык консоли:\nУстановите язык, который будет использоваться в играх PS4.\nРекомендуется устанавливать язык, который поддерживается игрой, так как он может отличаться в зависимости от региона. + + + emulatorLanguageGroupBox + Язык эмулятора:\nУстановите язык пользовательского интерфейса эмулятора. + + + fullscreenCheckBox + Полноэкранный режим:\nАвтоматически переводит игровое окно в полноэкранный режим.\nЭто можно переключить, нажав клавишу F11. + + + separateUpdatesCheckBox + Отдельная папка обновлений:\nПозволяет устанавливать обновления игры в отдельную папку для удобства.\nМожно создать вручную, добавив извлеченное обновление в папку с игрой с именем "CUSA00000-UPDATE", где идентификатор CUSA совпадает с идентификатором игры. + + + showSplashCheckBox + Показывать заставку:\nОтображает заставку игры (специальное изображение) во время запуска. + + + ps4proCheckBox + Режим PS4 Pro:\nЗаставляет эмулятор работать как PS4 Pro, что может включить специальные функции в играх, поддерживающих это. + + + discordRPCCheckbox + Включить Discord Rich Presence:\nОтображает значок эмулятора и соответствующую информацию в вашем профиле Discord. + + + userName + Имя пользователя:\nУстановите имя пользователя аккаунта PS4. Это может отображаться в некоторых играх. + + + TrophyKey + Ключ трофеев:\nКлюч, используемый для расшифровки трофеев. Должен быть получен из вашей взломанной консоли.\nДолжен содержать только шестнадцатеричные символы. + + + logTypeGroupBox + Тип логов:\nУстановите, синхронизировать ли вывод окна логов ради производительности. Это может негативно сказаться на эмуляции. + + + logFilter + Фильтр логов:\nФильтрует логи, чтобы показывать только определенную информацию.\nПримеры: "Core:Trace" "Lib.Pad:Debug Common.Filesystem:Error" "*:Critical" Уровни: Trace, Debug, Info, Warning, Error, Critical - в этом порядке, конкретный уровень глушит все предыдущие уровни в списке и показывает все последующие уровни. + + + updaterGroupBox + Обновление:\nRelease: Официальные версии, которые выпускаются каждый месяц и могут быть очень старыми, но они более надежные и проверенные.\nNightly: Версии разработки, которые содержат все последние функции и исправления, но могут содержать ошибки и менее стабильны. + + + GUIMusicGroupBox + Играть заглавную музыку:\nВключает воспроизведение специальной музыки при выборе игры в списке, если она это поддерживает. + + + disableTrophycheckBox + Отключить уведомления о трофеях:\nОтключает внутриигровые уведомления о трофеях. Прогресс трофеев по-прежнему можно отслеживать в меню просмотра трофеев (правая кнопка мыши по игре в главном окне). + + + hideCursorGroupBox + Скрывать курсор:\nВыберите, когда курсор будет скрыт:\nНикогда: Вы всегда будете видеть курсор.\nПри бездействии: Установите время, через которое курсор будет скрыт при бездействии.\nВсегда: Курсор всегда будет скрыт. + + + idleTimeoutGroupBox + Время скрытия курсора при бездействии:\nУстановите время, через которое курсор исчезнет при бездействии. + + + backButtonBehaviorGroupBox + Поведение кнопки «Назад»:\nНастраивает кнопку «Назад» контроллера на эмуляцию нажатия на указанную область на сенсорной панели контроллера PS4. + + + enableCompatibilityCheckBox + Показывать данные совместимости:\nПоказывает информацию о совместимости игр в таблице. Включите «Обновлять базу совместимости при запуске» для получения актуальной информации. + + + checkCompatibilityOnStartupCheckBox + Обновлять базу совместимости при запуске:\nАвтоматически обновлять базу данных совместимости при запуске shadPS4. + + + updateCompatibilityButton + Обновить базу совместимости:\nНемедленно обновить базу данных совместимости. + + + Never + Никогда + + + Idle + При бездействии + + + Always + Всегда + + + Touchpad Left + Тачпад слева + + + Touchpad Right + Тачпад справа + + + Touchpad Center + Центр тачпада + + + None + Нет + + + graphicsAdapterGroupBox + Графическое устройство:\nВ системах с несколькими GPU выберите тот, который будет использовать эмулятор.\nВыберите "Автовыбор", чтобы определить GPU автоматически. + + + resolutionLayout + Ширина/Высота:\nУстановите размер окна эмулятора при запуске, который может быть изменен во время игры.\nЭто отличается от разрешения в игре. + + + heightDivider + Делитель Vblank:\nЧастота кадров, с которой обновляется эмулятор, умножается на это число. Изменение этого параметра может иметь негативные последствия, такие как увеличение скорости игры или нарушение критических функций игры, которые этого не ожидают! + + + dumpShadersCheckBox + Включить дамп шейдеров:\nДля технической отладки сохраняет шейдеры игр в папку во время рендеринга. + + + nullGpuCheckBox + Включить NULL GPU:\nДля технической отладки отключает рендеринг игры так, как будто графической карты нет. + + + gameFoldersBox + Игровые папки:\nСписок папок для проверки установленных игр. + + + addFolderButton + Добавить:\nДобавить папку в список. + + + removeFolderButton + Удалить:\nУдалить папку из списка. + + + debugDump + Включить отладочные дампы:\nСохраняет символы импорта, экспорта и информацию о заголовке файла текущей исполняемой программы PS4 в папку. + + + vkValidationCheckBox + Включить слои валидации Vulkan:\nВключает систему, которая проверяет состояние рендерера Vulkan и логирует информацию о его внутреннем состоянии. Это снизит производительность и, вероятно, изменит поведение эмуляции. + + + vkSyncValidationCheckBox + Включить валидацию синхронизации Vulkan:\nВключает систему, которая проверяет тайминг задач рендеринга Vulkan. Это снизит производительность и, вероятно, изменит поведение эмуляции. + + + rdocCheckBox + Включить отладку RenderDoc:\nЕсли включено, эмулятор обеспечит совместимость с RenderDoc, позволяя захватывать и анализировать текущие кадры во время рендеринга. + + + collectShaderCheckBox + Собирать шейдеры:\nВам необходимо включить эту функцию для редактирования шейдеров с помощью меню отладки (Ctrl + F10). + + + crashDiagnosticsCheckBox + Диагностика сбоев:\nСоздает .yaml файл с информацией о состоянии Vulkan в момент падения.\nПолезно для отладки ошибок 'Device lost'. Если эта функция включена, вам следует включить Маркеры отладки хоста и Гостя.\nНе работает на видеокартах Intel.\nДля работы вам необходимо включить Слои валидации Vulkan и установить Vulkan SDK. + + + copyGPUBuffersCheckBox + Копировать буферы GPU:\nПозволяет обойти состояния гонки, связанные с отправками GPU.\nМожет помочь или не помочь при сбоях PM4 типа 0. + + + hostMarkersCheckBox + Маркеры отладки хоста:\nДобавляет информацию на стороне эмулятора, например маркеры для определенных команд AMDGPU, вокруг команд Vulkan, а также присваивает ресурсам отладочные имена.\nЕсли эта функция включена, вам следует включить Диагностику сбоев.\nПолезно для таких программ, как RenderDoc. + + + guestMarkersCheckBox + Маркеры отладки гостя:\nДобавляет любые отладочные маркеры, добавленные самой игрой, в буфер команд.\nЕсли эта функция включена, вам следует включить Диагностику сбоев.\nПолезно для таких программ, как RenderDoc. + + + saveDataBox + Путь сохранений:\nПапка, в которой будут храниться сохранения игр. + + + browseButton + Обзор:\nНайдите папку, которую можно указать в качестве пути для сохранений. + + + Borderless + Без полей + + + True + Истинный + + + Release + Release + + + Nightly + Nightly + + + GUI + Интерфейс + + + Set the volume of the background music. + Установите громкость фоновой музыки. + + + Enable Motion Controls + Включить управление движением + + + Save Data Path + Путь сохранений + + + Browse + Обзор + + + async + асинхронный + + + sync + синхронный + + + Directory to install games + Каталог для установки игр + + + Directory to save data + Каталог для сохранений + + + Auto Select + Автовыбор + + + + CheatsPatches + + Cheats / Patches for + Читы и патчи для + + + defaultTextEdit_MSG + Читы и патчи экспериментальны.\nИспользуйте с осторожностью.\n\nСкачивайте читы, выбрав репозиторий и нажав на кнопку загрузки.\nВо вкладке "Патчи" вы можете скачать все патчи сразу, выбирать какие вы хотите использовать, и сохранять выбор.\n\nПоскольку мы не разрабатываем читы/патчи,\nпожалуйста сообщайте о проблемах автору чита/патча.\n\nСоздали новый чит? Посетите:\nhttps://github.com/shadps4-emu/ps4_cheats + + + No Image Available + Изображение недоступно + + + Serial: + Серийный номер: + + + Version: + Версия: + + + Size: + Размер: + + + Select Cheat File: + Выберите файл чита: + + + Repository: + Репозиторий: + + + Download Cheats + Скачать читы + + + Delete File + Удалить файл + + + No files selected. + Файлы не выбраны. + + + You can delete the cheats you don't want after downloading them. + Вы можете удалить ненужные читы после их скачивания. + + + Do you want to delete the selected file?\n%1 + Вы хотите удалить выбранный файл?\n%1 + + + Select Patch File: + Выберите файл патча: + + + Download Patches + Скачать патчи + + + Save + Сохранить + + + Cheats + Читы + + + Patches + Патчи + + + Error + Ошибка + + + No patch selected. + Патч не выбран. + + + Unable to open files.json for reading. + Не удалось открыть файл files.json для чтения. + + + No patch file found for the current serial. + Не найден файл патча для текущего серийного номера. + + + Unable to open the file for reading. + Не удалось открыть файл для чтения. + + + Unable to open the file for writing. + Не удалось открыть файл для записи. + + + Failed to parse XML: + Не удалось разобрать XML: + + + Success + Успех + + + Options saved successfully. + Опции успешно сохранены. + + + Invalid Source + Неверный источник + + + The selected source is invalid. + Выбранный источник недействителен. + + + File Exists + Файл существует + + + File already exists. Do you want to replace it? + Файл уже существует. Хотите заменить его? + + + Failed to save file: + Не удалось сохранить файл: + + + Failed to download file: + Не удалось скачать файл: + + + Cheats Not Found + Читы не найдены + + + CheatsNotFound_MSG + Читы не найдены для этой игры в выбранном репозитории. Попробуйте другой репозиторий или другую версию игры. + + + Cheats Downloaded Successfully + Читы успешно скачаны + + + CheatsDownloadedSuccessfully_MSG + Вы успешно скачали читы для этой версии игры из выбранного репозитория. Вы можете попробовать скачать из другого репозитория. Если он доступен, его также можно будет использовать, выбрав файл из списка. + + + Failed to save: + Не удалось сохранить: + + + Failed to download: + Не удалось скачать: + + + Download Complete + Скачивание завершено + + + DownloadComplete_MSG + Патчи успешно скачаны! Все доступные патчи для всех игр были скачаны, нет необходимости скачивать их по отдельности для каждой игры, как это происходит с читами. Если патч не появляется, возможно, его не существует для конкретного серийного номера и версии игры. + + + Failed to parse JSON data from HTML. + Не удалось разобрать данные JSON из HTML. + + + Failed to retrieve HTML page. + Не удалось получить HTML-страницу. + + + The game is in version: %1 + Игра в версии: %1 + + + The downloaded patch only works on version: %1 + Скачанный патч работает только на версии: %1 + + + You may need to update your game. + Вам может понадобиться обновить игру. + + + Incompatibility Notice + Уведомление о несовместимости + + + Failed to open file: + Не удалось открыть файл: + + + XML ERROR: + ОШИБКА XML: + + + Failed to open files.json for writing + Не удалось открыть файл files.json для записи + + + Author: + Автор: + + + Directory does not exist: + Каталог не существует: + + + Failed to open files.json for reading. + Не удалось открыть файл files.json для чтения. + + + Name: + Имя: + + + Can't apply cheats before the game is started + Невозможно применить читы до начала игры + + + Close + Закрыть + + + Error: + Ошибка: + + + ERROR + ОШИБКА + + + + GameListFrame + + Icon + Иконка + + + Name + Название + + + Serial + Серийный номер + + + Compatibility + Совместимость + + + Region + Регион + + + Firmware + Прошивка + + + Size + Размер + + + Version + Версия + + + Path + Путь + + + Play Time + Время в игре + + + Never Played + Нет + + + h + ч + + + m + м + + + s + с + + + Compatibility is untested + Совместимость не проверена + + + Game does not initialize properly / crashes the emulator + Игра не инициализируется правильно / эмулятор вылетает + + + Game boots, but only displays a blank screen + Игра запускается, но показывает только пустой экран + + + Game displays an image but does not go past the menu + Игра показывает картинку, но не проходит дальше меню + + + Game has game-breaking glitches or unplayable performance + Игра имеет ломающие игру глюки или плохую производительность + + + Game can be completed with playable performance and no major glitches + Игра может быть пройдена с хорошей производительностью и без серьезных сбоев + + + Click to go to issue + Нажмите, чтобы перейти к проблеме + + + Last updated + Последнее обновление + + + + CheckUpdate + + Auto Updater + Автообновление + + + Error + Ошибка + + + Network error: + Сетевая ошибка: + + + Failed to parse update information. + Не удалось разобрать информацию об обновлении. + + + No pre-releases found. + Предварительных версий не найдено. + + + Invalid release data. + Недопустимые данные релиза. + + + No download URL found for the specified asset. + Не найден URL для загрузки указанного ресурса. + + + Your version is already up to date! + Ваша версия уже обновлена! + + + Update Available + Доступно обновление + + + Update Channel + Канал обновления + + + Current Version + Текущая версия + + + Latest Version + Последняя версия + + + Do you want to update? + Вы хотите обновиться? + + + Show Changelog + Показать журнал изменений + + + Check for Updates at Startup + Проверка при запуске + + + Update + Обновить + + + No + Нет + + + Hide Changelog + Скрыть журнал изменений + + + Changes + Журнал изменений + + + Network error occurred while trying to access the URL + Произошла сетевая ошибка при попытке доступа к URL + + + Download Complete + Скачивание завершено + + + The update has been downloaded, press OK to install. + Обновление загружено, нажмите OK для установки. + + + Failed to save the update file at + Не удалось сохранить файл обновления в + + + Starting Update... + Начинаем обновление... + + + Failed to create the update script file + Не удалось создать файл скрипта обновления + + + + GameListUtils + + B + Б + + + KB + КБ + + + MB + МБ + + + GB + ГБ + + + TB + ТБ + + + + CompatibilityInfoClass + + Fetching compatibility data, please wait + Загрузка данных о совместимости, пожалуйста, подождите + + + Cancel + Отмена + + + Loading... + Загрузка... + + + Error + Ошибка + + + Unable to update compatibility data! Try again later. + Не удалось обновить данные совместимости! Повторите попытку позже. + + + Unable to open compatibility.json for writing. + Не удалось открыть файл compatibility.json для записи. + + + Unknown + Неизвестно + + + Nothing + Ничего + + + Boots + Запускается + + + Menus + В меню + + + Ingame + В игре + + + Playable + Играбельно + + From 83abaafdfad7685f62a97e0610b169ab713d7320 Mon Sep 17 00:00:00 2001 From: C4ndyF1sh <128715345+C4ndyF1sh@users.noreply.github.com> Date: Mon, 3 Feb 2025 11:53:26 +0100 Subject: [PATCH 08/20] qt: Add more options to the "Copy info..." section + update en.ts/de.ts (#2322) * Update gui_context_menus.h * Update gui_context_menus.h * Update en.ts * Update de.ts * Update gui_context_menus.h * Update gui_context_menus.h * Update gui_context_menus.h * Update gui_context_menus.h * Update gui_context_menus.h * Update gui_context_menus.h * Update gui_context_menus.h * Update gui_context_menus.h * Update gui_context_menus.h * Update gui_context_menus.h * Update en.ts * Update de.ts * Update gui_context_menus.h (last one) * very small fix en.ts * remove empty line * merge https://github.com/shadps4-emu/shadPS4/pull/2316 Adds german translations to compatibility status --- src/qt_gui/gui_context_menus.h | 14 ++++++++++++++ src/qt_gui/translations/de.ts | 35 ++++++++++++++++++++++++++++++++++ src/qt_gui/translations/en.ts | 8 ++++++++ 3 files changed, 57 insertions(+) diff --git a/src/qt_gui/gui_context_menus.h b/src/qt_gui/gui_context_menus.h index bdc2aec0c..262a1d733 100644 --- a/src/qt_gui/gui_context_menus.h +++ b/src/qt_gui/gui_context_menus.h @@ -77,10 +77,14 @@ public: QMenu* copyMenu = new QMenu(tr("Copy info..."), widget); QAction* copyName = new QAction(tr("Copy Name"), widget); QAction* copySerial = new QAction(tr("Copy Serial"), widget); + QAction* copyVersion = new QAction(tr("Copy Version"), widget); + QAction* copySize = new QAction(tr("Copy Size"), widget); QAction* copyNameAll = new QAction(tr("Copy All"), widget); copyMenu->addAction(copyName); copyMenu->addAction(copySerial); + copyMenu->addAction(copyVersion); + copyMenu->addAction(copySize); copyMenu->addAction(copyNameAll); menu.addMenu(copyMenu); @@ -346,6 +350,16 @@ public: clipboard->setText(QString::fromStdString(m_games[itemID].serial)); } + if (selected == copyVersion) { + QClipboard* clipboard = QGuiApplication::clipboard(); + clipboard->setText(QString::fromStdString(m_games[itemID].version)); + } + + if (selected == copySize) { + QClipboard* clipboard = QGuiApplication::clipboard(); + clipboard->setText(QString::fromStdString(m_games[itemID].size)); + } + if (selected == copyNameAll) { QClipboard* clipboard = QGuiApplication::clipboard(); QString combinedText = QString("Name:%1 | Serial:%2 | Version:%3 | Size:%4") diff --git a/src/qt_gui/translations/de.ts b/src/qt_gui/translations/de.ts index 71ee066c1..4985160ff 100644 --- a/src/qt_gui/translations/de.ts +++ b/src/qt_gui/translations/de.ts @@ -128,6 +128,14 @@ Copy Serial Seriennummer kopieren + + Copy Version + Version kopieren + + + Copy Size + Größe kopieren + Copy All Alles kopieren @@ -1421,4 +1429,31 @@ TB + + CompatibilityInfoClass + + Unknown + Unbekannt + + + Nothing + Nichts + + + Boots + Startet + + + Menus + Menüs + + + Ingame + ImSpiel + + + Playable + Spielbar + + diff --git a/src/qt_gui/translations/en.ts b/src/qt_gui/translations/en.ts index 58d6e9aa8..afaa17520 100644 --- a/src/qt_gui/translations/en.ts +++ b/src/qt_gui/translations/en.ts @@ -124,6 +124,14 @@ Copy Serial Copy Serial + + Copy Version + Copy Version + + + Copy Size + Copy Size + Copy All Copy All From cef7edaea9799122f4dd3787015e15367c13562e Mon Sep 17 00:00:00 2001 From: DemoJameson Date: Mon, 3 Feb 2025 18:53:40 +0800 Subject: [PATCH 09/20] Update zh_CN translation (#2328) --- src/qt_gui/translations/zh_CN.ts | 56 ++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/src/qt_gui/translations/zh_CN.ts b/src/qt_gui/translations/zh_CN.ts index 00f4337c0..1e6124c85 100644 --- a/src/qt_gui/translations/zh_CN.ts +++ b/src/qt_gui/translations/zh_CN.ts @@ -52,7 +52,7 @@ Select which directory you want to install to. - 选择你想要安装到的目录。 + 选择您想要安装到的目录。 @@ -186,7 +186,7 @@ requiresEnableSeparateUpdateFolder_MSG - 这个功能需要“启用单独的更新目录”配置选项才能正常运行,如果你想要使用这个功能,请启用它。 + 这个功能需要“启用单独的更新目录”配置选项才能正常运行,如果您想要使用这个功能,请启用它。 This game has no update to delete! @@ -210,7 +210,7 @@ Are you sure you want to delete %1's %2 directory? - 你确定要删除 %1 的%2目录? + 您确定要删除 %1 的%2目录? @@ -702,24 +702,25 @@ Enable Crash Diagnostics - Enable Crash Diagnostics + 启用崩溃诊断 Collect Shaders - Collect Shaders + 收集着色器 Copy GPU Buffers - Copy GPU Buffers + 复制 GPU 缓冲区 Host Debug Markers - Host Debug Markers + Host 调试标记 Guest Debug Markers - Guest Debug Markers + Geust 调试标记 + Update 更新 @@ -954,23 +955,23 @@ collectShaderCheckBox - Collect Shaders:\nYou need this enabled to edit shaders with the debug menu (Ctrl + F10). + 收集着色器:\n您需要启用此功能才能使用调试菜单(Ctrl + F10)编辑着色器。 crashDiagnosticsCheckBox - Crash Diagnostics:\nCreates a .yaml file with info about the Vulkan state at the time of crashing.\nUseful for debugging 'Device lost' errors. If you have this enabled, you should enable Host AND Guest Debug Markers.\nDoes not work on Intel GPUs.\nYou need Vulkan Validation Layers enabled and the Vulkan SDK for this to work. + 崩溃诊断:\n创建一个包含崩溃时 Vulkan 状态的 .yaml 文件。\n对于调试“Device lost”错误很有用。如果您启用了此功能,您应该同时启用 Host 和 Guest 调试标记。\n此功能在 Intel 显卡上不可用。\n您需要启用 Vulkan 验证层并安装 Vulkan SDK 才能使用此功能。 copyGPUBuffersCheckBox - Copy GPU Buffers:\nGets around race conditions involving GPU submits.\nMay or may not help with PM4 type 0 crashes. + 复制 GPU 缓冲区:\n绕过涉及 GPU 提交的竞态条件。\n对于 PM4 type 0 崩溃可能有帮助,也可能没有帮助。 hostMarkersCheckBox - Host Debug Markers:\nInserts emulator-side information like markers for specific AMDGPU commands around Vulkan commands, as well as giving resources debug names.\nIf you have this enabled, you should enable Crash Diagnostics.\nUseful for programs like RenderDoc. + Host 调试标记:\n在 Vulkan 命令周围插入模拟器端信息,如特定 AMD GPU 命令的标记,以及为资源提供调试名称。\n如果您已启用此功能,应同时启用崩溃诊断。\n对 RenderDoc 等程序很有用。 guestMarkersCheckBox - Guest Debug Markers:\nInserts any debug markers the game itself has added to the command buffer.\nIf you have this enabled, you should enable Crash Diagnostics.\nUseful for programs like RenderDoc. + Guest 调试标记:\n在命令缓冲区中插入游戏本身添加的任何调试标记。\n如果您已启用此功能,应同时启用崩溃诊断。\n对 RenderDoc 等程序很有用。 saveDataBox @@ -1284,7 +1285,7 @@ Game can be completed with playable performance and no major glitches - 游戏能在可玩的性能下完成且没有重大 Bug + 游戏能在可玩的性能下通关且没有重大 Bug @@ -1413,4 +1414,31 @@ TB + + CompatibilityInfoClass + + Unknown + 未知 + + + Nothing + 无法启动 + + + Boots + 可启动 + + + Menus + 可进入菜单 + + + Ingame + 可进入游戏内 + + + Playable + 可通关 + + From 02ad2b78faf190fb9bc545ccdfadb091913c17c1 Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Mon, 3 Feb 2025 11:53:57 +0100 Subject: [PATCH 10/20] Fork detection: Fix Windows naming + add a new check for fork detection (#2321) * Possible fix for Windows * Check if remote.pushDefault is set when generating the remote link * Remove left-in lines I missed before --- CMakeLists.txt | 15 +++++++++++++-- src/emulator.cpp | 10 +++++++--- src/qt_gui/main_window.cpp | 10 +++++++--- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4822658c6..b0e1115b3 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -115,9 +115,20 @@ execute_process( OUTPUT_STRIP_TRAILING_WHITESPACE ) -# Default to origin if there's no upstream set or if the command failed +# If there's no upstream set or the command failed, check remote.pushDefault if (GIT_BRANCH_RESULT OR GIT_REMOTE_NAME STREQUAL "") - set(GIT_REMOTE_NAME "origin") + execute_process( + COMMAND git config --get remote.pushDefault + OUTPUT_VARIABLE GIT_REMOTE_NAME + RESULT_VARIABLE GIT_PUSH_DEFAULT_RESULT + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + # If remote.pushDefault is not set or fails, default to origin + if (GIT_PUSH_DEFAULT_RESULT OR GIT_REMOTE_NAME STREQUAL "") + set(GIT_REMOTE_NAME "origin") + endif() else() # Extract remote name if the output contains a remote/branch format string(FIND "${GIT_REMOTE_NAME}" "/" INDEX) diff --git a/src/emulator.cpp b/src/emulator.cpp index ba8d8917c..cd981add2 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -201,12 +201,16 @@ void Emulator::Run(const std::filesystem::path& file, const std::vector Date: Mon, 3 Feb 2025 09:37:28 -0600 Subject: [PATCH 11/20] Fix VideoOut events (#2330) * Fix event data for VideoOut events Fix is based on some decompilation work shared by red_prig. * Cleanup * Oops * Style fixes * Clang * Fix libSceVideoOut event idents Based on some decompilation work, events coming from libSceVideoOut use a separate set of values for identifiers. These values are only converted to OrbisVideoOutEventId values during calls to sceVideoOutGetEventId. For convenience, I've placed all relevant identifiers into a enum called OrbisVideoOutInternalEventId. Thanks to @red_prig for the tips. * Fix? Seems like `static_cast(hint) & 0xFF == event.ident` here, and doing those right shifts on the event.ident winds up breaking stuff. Without this change, the if always fails because event_id was getting set to 0 instead. * Clang --- src/core/libraries/kernel/equeue.cpp | 6 +++++- src/core/libraries/kernel/equeue.h | 20 +++++++++++++++++ src/core/libraries/videoout/driver.cpp | 10 +++++---- src/core/libraries/videoout/video_out.cpp | 26 +++++++++++++++++++---- src/core/libraries/videoout/video_out.h | 17 ++++++++++++++- 5 files changed, 69 insertions(+), 10 deletions(-) diff --git a/src/core/libraries/kernel/equeue.cpp b/src/core/libraries/kernel/equeue.cpp index 64d4966c0..a4916208a 100644 --- a/src/core/libraries/kernel/equeue.cpp +++ b/src/core/libraries/kernel/equeue.cpp @@ -84,7 +84,11 @@ bool EqueueInternal::TriggerEvent(u64 ident, s16 filter, void* trigger_data) { std::scoped_lock lock{m_mutex}; for (auto& event : m_events) { if (event.event.ident == ident && event.event.filter == filter) { - event.Trigger(trigger_data); + if (filter == SceKernelEvent::Filter::VideoOut) { + event.TriggerDisplay(trigger_data); + } else { + event.Trigger(trigger_data); + } has_found = true; } } diff --git a/src/core/libraries/kernel/equeue.h b/src/core/libraries/kernel/equeue.h index 2db5e6ca7..11c09bb37 100644 --- a/src/core/libraries/kernel/equeue.h +++ b/src/core/libraries/kernel/equeue.h @@ -9,6 +9,7 @@ #include #include +#include "common/rdtsc.h" #include "common/types.h" namespace Core::Loader { @@ -81,6 +82,25 @@ struct EqueueEvent { event.data = reinterpret_cast(data); } + void TriggerDisplay(void* data) { + is_triggered = true; + auto hint = reinterpret_cast(data); + if (hint != 0) { + auto hint_h = static_cast(hint >> 8) & 0xFFFFFF; + auto ident_h = static_cast(event.ident >> 40); + if ((static_cast(hint) & 0xFF) == event.ident && event.ident != 0xFE && + ((hint_h ^ ident_h) & 0xFF) == 0) { + auto time = Common::FencedRDTSC(); + auto mask = 0xF000; + if ((static_cast(event.data) & 0xF000) != 0xF000) { + mask = (static_cast(event.data) + 0x1000) & 0xF000; + } + event.data = (mask | static_cast(static_cast(time) & 0xFFF) | + (hint & 0xFFFFFFFFFFFF0000)); + } + } + } + bool IsTriggered() const { return is_triggered; } diff --git a/src/core/libraries/videoout/driver.cpp b/src/core/libraries/videoout/driver.cpp index de5421fd7..0f832910c 100644 --- a/src/core/libraries/videoout/driver.cpp +++ b/src/core/libraries/videoout/driver.cpp @@ -185,9 +185,11 @@ void VideoOutDriver::Flip(const Request& req) { // Trigger flip events for the port. for (auto& event : port->flip_events) { if (event != nullptr) { - event->TriggerEvent(u64(OrbisVideoOutEventId::Flip), - Kernel::SceKernelEvent::Filter::VideoOut, - reinterpret_cast(req.flip_arg)); + event->TriggerEvent( + static_cast(OrbisVideoOutInternalEventId::Flip), + Kernel::SceKernelEvent::Filter::VideoOut, + reinterpret_cast(static_cast(OrbisVideoOutInternalEventId::Flip) | + (req.flip_arg << 16))); } } @@ -323,7 +325,7 @@ void VideoOutDriver::PresentThread(std::stop_token token) { // Trigger flip events for the port. for (auto& event : main_port.vblank_events) { if (event != nullptr) { - event->TriggerEvent(u64(OrbisVideoOutEventId::Vblank), + event->TriggerEvent(static_cast(OrbisVideoOutInternalEventId::Vblank), Kernel::SceKernelEvent::Filter::VideoOut, nullptr); } } diff --git a/src/core/libraries/videoout/video_out.cpp b/src/core/libraries/videoout/video_out.cpp index 78a2b11a4..27a3fe082 100644 --- a/src/core/libraries/videoout/video_out.cpp +++ b/src/core/libraries/videoout/video_out.cpp @@ -50,7 +50,7 @@ s32 PS4_SYSV_ABI sceVideoOutAddFlipEvent(Kernel::SceKernelEqueue eq, s32 handle, } Kernel::EqueueEvent event{}; - event.event.ident = u64(OrbisVideoOutEventId::Flip); + event.event.ident = static_cast(OrbisVideoOutInternalEventId::Flip); event.event.filter = Kernel::SceKernelEvent::Filter::VideoOut; event.event.flags = Kernel::SceKernelEvent::Flags::Add; event.event.udata = udata; @@ -76,7 +76,7 @@ s32 PS4_SYSV_ABI sceVideoOutAddVblankEvent(Kernel::SceKernelEqueue eq, s32 handl } Kernel::EqueueEvent event{}; - event.event.ident = u64(OrbisVideoOutEventId::Vblank); + event.event.ident = static_cast(OrbisVideoOutInternalEventId::Vblank); event.event.filter = Kernel::SceKernelEvent::Filter::VideoOut; event.event.flags = Kernel::SceKernelEvent::Flags::Add; event.event.udata = udata; @@ -156,9 +156,27 @@ int PS4_SYSV_ABI sceVideoOutGetEventId(const Kernel::SceKernelEvent* ev) { return ORBIS_VIDEO_OUT_ERROR_INVALID_ADDRESS; } if (ev->filter != Kernel::SceKernelEvent::Filter::VideoOut) { - return ORBIS_VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE; + return ORBIS_VIDEO_OUT_ERROR_INVALID_EVENT; + } + + OrbisVideoOutInternalEventId internal_event_id = + static_cast(ev->ident); + switch (internal_event_id) { + case OrbisVideoOutInternalEventId::Flip: + return static_cast(OrbisVideoOutEventId::Flip); + case OrbisVideoOutInternalEventId::Vblank: + case OrbisVideoOutInternalEventId::SysVblank: + return static_cast(OrbisVideoOutEventId::Vblank); + case OrbisVideoOutInternalEventId::PreVblankStart: + return static_cast(OrbisVideoOutEventId::PreVblankStart); + case OrbisVideoOutInternalEventId::SetMode: + return static_cast(OrbisVideoOutEventId::SetMode); + case OrbisVideoOutInternalEventId::Position: + return static_cast(OrbisVideoOutEventId::Position); + default: { + return ORBIS_VIDEO_OUT_ERROR_INVALID_EVENT; + } } - return ev->ident; } int PS4_SYSV_ABI sceVideoOutGetEventData(const Kernel::SceKernelEvent* ev, int64_t* data) { diff --git a/src/core/libraries/videoout/video_out.h b/src/core/libraries/videoout/video_out.h index 5af9d550d..2918fac30 100644 --- a/src/core/libraries/videoout/video_out.h +++ b/src/core/libraries/videoout/video_out.h @@ -40,7 +40,22 @@ constexpr int SCE_VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_NONE = 0; constexpr int SCE_VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_VR = 7; constexpr int SCE_VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY = 8; -enum class OrbisVideoOutEventId : s16 { Flip = 0, Vblank = 1, PreVblankStart = 2 }; +enum class OrbisVideoOutEventId : s16 { + Flip = 0, + Vblank = 1, + PreVblankStart = 2, + SetMode = 8, + Position = 12, +}; + +enum class OrbisVideoOutInternalEventId : s16 { + Flip = 0x6, + Vblank = 0x7, + SetMode = 0x51, + Position = 0x58, + PreVblankStart = 0x59, + SysVblank = 0x63, +}; enum class AspectRatioMode : s32 { Ratio16_9 = 0, From 97441b62d1749b0ddeaf20dc8639a1c942d6941a Mon Sep 17 00:00:00 2001 From: DanielSvoboda Date: Tue, 4 Feb 2025 03:49:16 -0300 Subject: [PATCH 12/20] Fix game title sorting - Grid view (#2341) --- src/qt_gui/game_info.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/qt_gui/game_info.h b/src/qt_gui/game_info.h index 99805cd52..380c79e70 100644 --- a/src/qt_gui/game_info.h +++ b/src/qt_gui/game_info.h @@ -19,7 +19,10 @@ public: QVector m_games; static bool CompareStrings(GameInfo& a, GameInfo& b) { - return a.name < b.name; + std::string name_a = a.name, name_b = b.name; + std::transform(name_a.begin(), name_a.end(), name_a.begin(), ::tolower); + std::transform(name_b.begin(), name_b.end(), name_b.begin(), ::tolower); + return name_a < name_b; } static GameInfo readGameInfo(const std::filesystem::path& filePath) { @@ -72,4 +75,4 @@ public: } return game; } -}; \ No newline at end of file +}; From e0d85a7e58f56c67a5e2d621738afa12b0436ee7 Mon Sep 17 00:00:00 2001 From: rainmakerv2 <30595646+rainmakerv3@users.noreply.github.com> Date: Tue, 4 Feb 2025 14:50:32 +0800 Subject: [PATCH 13/20] add controller remapping GUI (#2311) * Initial Version - controller remapping GUI * rework saving to allow for multiple outputs to an input * License header * Remove PS logo from image * Add per game checkbox, fix filename, better whitespace deletion * Reorganize variables, correctly set common config label when selected * Add option to unmap, set mapping to unmapped when config entry is absent * Add XBox labels * Remove parsing from save function, make window more compact * Fix typo * Code cleanup * Additional cleanup --------- Co-authored-by: rainmakerv2 <30595646+jpau02@users.noreply.github.com> --- CMakeLists.txt | 3 + REUSE.toml | 1 + src/images/ps4_controller.png | Bin 0 -> 93841 bytes src/qt_gui/control_settings.cpp | 498 +++++++++++ src/qt_gui/control_settings.h | 52 ++ src/qt_gui/control_settings.ui | 1379 +++++++++++++++++++++++++++++++ src/qt_gui/main_window.cpp | 5 +- src/shadps4.qrc | 1 + 8 files changed, 1937 insertions(+), 2 deletions(-) create mode 100644 src/images/ps4_controller.png create mode 100644 src/qt_gui/control_settings.cpp create mode 100644 src/qt_gui/control_settings.h create mode 100644 src/qt_gui/control_settings.ui diff --git a/CMakeLists.txt b/CMakeLists.txt index b0e1115b3..8ecbbf0d6 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -897,6 +897,9 @@ set(QT_GUI src/qt_gui/about_dialog.cpp src/qt_gui/cheats_patches.h src/qt_gui/compatibility_info.cpp src/qt_gui/compatibility_info.h + src/qt_gui/control_settings.cpp + src/qt_gui/control_settings.h + src/qt_gui/control_settings.ui src/qt_gui/main_window_ui.h src/qt_gui/main_window.cpp src/qt_gui/main_window.h diff --git a/REUSE.toml b/REUSE.toml index a62974bcd..63242edb2 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -38,6 +38,7 @@ path = [ "src/images/list_mode_icon.png", "src/images/pause_icon.png", "src/images/play_icon.png", + "src/images/ps4_controller.png", "src/images/refresh_icon.png", "src/images/settings_icon.png", "src/images/stop_icon.png", diff --git a/src/images/ps4_controller.png b/src/images/ps4_controller.png new file mode 100644 index 0000000000000000000000000000000000000000..0e2c1d4f1a18ad2af8146e9f08fb0b63687b2018 GIT binary patch literal 93841 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 +#include +#include "common/path_util.h" +#include "control_settings.h" +#include "kbm_config_dialog.h" +#include "ui_control_settings.h" + +ControlSettings::ControlSettings(std::shared_ptr game_info_get, QWidget* parent) + : QDialog(parent), m_game_info(game_info_get), ui(new Ui::ControlSettings) { + + ui->setupUi(this); + ui->PerGameCheckBox->setChecked(!Config::GetUseUnifiedInputConfig()); + + AddBoxItems(); + SetUIValuestoMappings(); + + connect(ui->buttonBox, &QDialogButtonBox::clicked, this, [this](QAbstractButton* button) { + if (button == ui->buttonBox->button(QDialogButtonBox::Save)) { + SaveControllerConfig(true); + } else if (button == ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)) { + SetDefault(); + } else if (button == ui->buttonBox->button(QDialogButtonBox::Apply)) { + SaveControllerConfig(false); + } + }); + + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QWidget::close); + connect(ui->KBMButton, &QPushButton::clicked, this, [this] { + auto KBMWindow = new EditorDialog(this); + KBMWindow->exec(); + }); + connect(ui->ProfileComboBox, &QComboBox::currentTextChanged, this, [this] { + GetGameTitle(); + SetUIValuestoMappings(); + }); + + connect(ui->LeftDeadzoneSlider, &QSlider::valueChanged, this, + [this](int value) { ui->LeftDeadzoneValue->setText(QString::number(value)); }); + connect(ui->RightDeadzoneSlider, &QSlider::valueChanged, this, + [this](int value) { ui->RightDeadzoneValue->setText(QString::number(value)); }); + + connect(ui->LStickUpBox, &QComboBox::currentIndexChanged, this, + [this](int value) { ui->LStickDownBox->setCurrentIndex(value); }); + connect(ui->LStickDownBox, &QComboBox::currentIndexChanged, this, + [this](int value) { ui->LStickUpBox->setCurrentIndex(value); }); + connect(ui->LStickRightBox, &QComboBox::currentIndexChanged, this, + [this](int value) { ui->LStickLeftBox->setCurrentIndex(value); }); + connect(ui->LStickLeftBox, &QComboBox::currentIndexChanged, this, + [this](int value) { ui->LStickRightBox->setCurrentIndex(value); }); + + connect(ui->RStickUpBox, &QComboBox::currentIndexChanged, this, + [this](int value) { ui->RStickDownBox->setCurrentIndex(value); }); + connect(ui->RStickDownBox, &QComboBox::currentIndexChanged, this, + [this](int value) { ui->RStickUpBox->setCurrentIndex(value); }); + connect(ui->RStickRightBox, &QComboBox::currentIndexChanged, this, + [this](int value) { ui->RStickLeftBox->setCurrentIndex(value); }); + connect(ui->RStickLeftBox, &QComboBox::currentIndexChanged, this, + [this](int value) { ui->RStickRightBox->setCurrentIndex(value); }); +} + +void ControlSettings::SaveControllerConfig(bool CloseOnSave) { + QList list; + list << ui->RStickUpBox << ui->RStickRightBox << ui->LStickUpBox << ui->LStickRightBox; + int count_axis_left_x = 0, count_axis_left_y = 0, count_axis_right_x = 0, + count_axis_right_y = 0; + for (const auto& i : list) { + if (i->currentText() == "axis_left_x") { + count_axis_left_x = count_axis_left_x + 1; + } else if (i->currentText() == "axis_left_y") { + count_axis_left_y = count_axis_left_y + 1; + } else if (i->currentText() == "axis_right_x") { + count_axis_right_x = count_axis_right_x + 1; + } else if (i->currentText() == "axis_right_y") { + count_axis_right_y = count_axis_right_y + 1; + } + } + + if (count_axis_left_x > 1 | count_axis_left_y > 1 | count_axis_right_x > 1 | + count_axis_right_y > 1) { + QMessageBox::StandardButton nosave; + nosave = QMessageBox::information(this, "Unable to Save", + "Cannot bind axis values more than once"); + return; + } + + std::string config_id; + config_id = (ui->ProfileComboBox->currentText() == "Common Config") + ? "default" + : ui->ProfileComboBox->currentText().toStdString(); + const auto config_file = Config::GetFoolproofKbmConfigFile(config_id); + + int lineCount = 0; + std::string line; + std::vector lines; + std::string output_string = "", input_string = ""; + std::fstream file(config_file); + + while (std::getline(file, line)) { + lineCount++; + + std::size_t comment_pos = line.find('#'); + if (comment_pos != std::string::npos) { + if (!line.contains("Range of deadzones")) + lines.push_back(line); + continue; + } + + std::size_t equal_pos = line.find('='); + if (equal_pos == std::string::npos) { + lines.push_back(line); + continue; + } + + output_string = line.substr(0, equal_pos - 1); + input_string = line.substr(equal_pos + 2); + + if (std::find(ControllerInputs.begin(), ControllerInputs.end(), input_string) != + ControllerInputs.end() || + output_string == "analog_deadzone") { + line.erase(); + continue; + } + lines.push_back(line); + } + + file.close(); + + input_string = "cross"; + output_string = ui->ABox->currentText().toStdString(); + lines.push_back(output_string + " = " + input_string); + + input_string = "circle"; + output_string = ui->BBox->currentText().toStdString(); + lines.push_back(output_string + " = " + input_string); + + input_string = "square"; + output_string = ui->XBox->currentText().toStdString(); + lines.push_back(output_string + " = " + input_string); + + input_string = "triangle"; + output_string = ui->YBox->currentText().toStdString(); + lines.push_back(output_string + " = " + input_string); + + lines.push_back(""); + + input_string = "l1"; + output_string = ui->LBBox->currentText().toStdString(); + lines.push_back(output_string + " = " + input_string); + + input_string = "r1"; + output_string = ui->RBBox->currentText().toStdString(); + lines.push_back(output_string + " = " + input_string); + + input_string = "l2"; + output_string = ui->LTBox->currentText().toStdString(); + lines.push_back(output_string + " = " + input_string); + + input_string = "r2"; + output_string = ui->RTBox->currentText().toStdString(); + lines.push_back(output_string + " = " + input_string); + + input_string = "l3"; + output_string = ui->LClickBox->currentText().toStdString(); + lines.push_back(output_string + " = " + input_string); + + input_string = "r3"; + output_string = ui->RClickBox->currentText().toStdString(); + lines.push_back(output_string + " = " + input_string); + + lines.push_back(""); + + input_string = "back"; + output_string = ui->BackBox->currentText().toStdString(); + lines.push_back(output_string + " = " + input_string); + + input_string = "options"; + output_string = ui->StartBox->currentText().toStdString(); + lines.push_back(output_string + " = " + input_string); + + lines.push_back(""); + + input_string = "pad_up"; + output_string = ui->DpadUpBox->currentText().toStdString(); + lines.push_back(output_string + " = " + input_string); + + input_string = "pad_down"; + output_string = ui->DpadDownBox->currentText().toStdString(); + lines.push_back(output_string + " = " + input_string); + + input_string = "pad_left"; + output_string = ui->DpadLeftBox->currentText().toStdString(); + lines.push_back(output_string + " = " + input_string); + + input_string = "pad_right"; + output_string = ui->DpadRightBox->currentText().toStdString(); + lines.push_back(output_string + " = " + input_string); + + lines.push_back(""); + + input_string = "axis_left_x"; + output_string = ui->LStickRightBox->currentText().toStdString(); + lines.push_back(output_string + " = " + input_string); + + input_string = "axis_left_y"; + output_string = ui->LStickUpBox->currentText().toStdString(); + lines.push_back(output_string + " = " + input_string); + + input_string = "axis_right_x"; + output_string = ui->RStickRightBox->currentText().toStdString(); + lines.push_back(output_string + " = " + input_string); + + input_string = "axis_right_y"; + output_string = ui->RStickUpBox->currentText().toStdString(); + lines.push_back(output_string + " = " + input_string); + + lines.push_back(""); + lines.push_back("# Range of deadzones: 1 (almost none) to 127 (max)"); + + std::string deadzonevalue = std::to_string(ui->LeftDeadzoneSlider->value()); + lines.push_back("analog_deadzone = leftjoystick, " + deadzonevalue); + + deadzonevalue = std::to_string(ui->RightDeadzoneSlider->value()); + lines.push_back("analog_deadzone = rightjoystick, " + deadzonevalue); + + std::vector save; + bool CurrentLineEmpty = false, LastLineEmpty = false; + for (auto const& line : lines) { + LastLineEmpty = CurrentLineEmpty ? true : false; + CurrentLineEmpty = line.empty() ? true : false; + if (!CurrentLineEmpty || !LastLineEmpty) + save.push_back(line); + } + + std::ofstream output_file(config_file); + for (auto const& line : save) { + output_file << line << '\n'; + } + output_file.close(); + + Config::SetUseUnifiedInputConfig(!ui->PerGameCheckBox->isChecked()); + Config::save(Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "config.toml"); + + if (CloseOnSave) + QWidget::close(); +} + +void ControlSettings::SetDefault() { + ui->ABox->setCurrentIndex(0); + ui->BBox->setCurrentIndex(1); + ui->XBox->setCurrentIndex(2); + ui->YBox->setCurrentIndex(3); + ui->DpadUpBox->setCurrentIndex(11); + ui->DpadDownBox->setCurrentIndex(12); + ui->DpadLeftBox->setCurrentIndex(13); + ui->DpadRightBox->setCurrentIndex(14); + ui->LClickBox->setCurrentIndex(8); + ui->RClickBox->setCurrentIndex(9); + ui->LBBox->setCurrentIndex(4); + ui->RBBox->setCurrentIndex(5); + ui->LTBox->setCurrentIndex(6); + ui->RTBox->setCurrentIndex(7); + ui->StartBox->setCurrentIndex(10); + ui->BackBox->setCurrentIndex(15); + + ui->LStickUpBox->setCurrentIndex(1); + ui->LStickDownBox->setCurrentIndex(1); + ui->LStickLeftBox->setCurrentIndex(0); + ui->LStickRightBox->setCurrentIndex(0); + ui->RStickUpBox->setCurrentIndex(3); + ui->RStickDownBox->setCurrentIndex(3); + ui->RStickLeftBox->setCurrentIndex(2); + ui->RStickRightBox->setCurrentIndex(2); + + ui->LeftDeadzoneSlider->setValue(2); + ui->RightDeadzoneSlider->setValue(2); +} + +void ControlSettings::AddBoxItems() { + ui->DpadUpBox->addItems(ButtonOutputs); + ui->DpadDownBox->addItems(ButtonOutputs); + ui->DpadLeftBox->addItems(ButtonOutputs); + ui->DpadRightBox->addItems(ButtonOutputs); + ui->LBBox->addItems(ButtonOutputs); + ui->RBBox->addItems(ButtonOutputs); + ui->LTBox->addItems(ButtonOutputs); + ui->RTBox->addItems(ButtonOutputs); + ui->RClickBox->addItems(ButtonOutputs); + ui->LClickBox->addItems(ButtonOutputs); + ui->StartBox->addItems(ButtonOutputs); + ui->ABox->addItems(ButtonOutputs); + ui->BBox->addItems(ButtonOutputs); + ui->XBox->addItems(ButtonOutputs); + ui->YBox->addItems(ButtonOutputs); + ui->BackBox->addItems(ButtonOutputs); + + ui->LStickUpBox->addItems(StickOutputs); + ui->LStickDownBox->addItems(StickOutputs); + ui->LStickLeftBox->addItems(StickOutputs); + ui->LStickRightBox->addItems(StickOutputs); + ui->RStickUpBox->addItems(StickOutputs); + ui->RStickDownBox->addItems(StickOutputs); + ui->RStickLeftBox->addItems(StickOutputs); + ui->RStickRightBox->addItems(StickOutputs); + + ui->ProfileComboBox->addItem("Common Config"); + for (int i = 0; i < m_game_info->m_games.size(); i++) { + ui->ProfileComboBox->addItem(QString::fromStdString(m_game_info->m_games[i].serial)); + } + ui->ProfileComboBox->setCurrentText("Common Config"); + ui->TitleLabel->setText("Common Config"); +} + +void ControlSettings::SetUIValuestoMappings() { + std::string config_id; + config_id = (ui->ProfileComboBox->currentText() == "Common Config") + ? "default" + : ui->ProfileComboBox->currentText().toStdString(); + + const auto config_file = Config::GetFoolproofKbmConfigFile(config_id); + std::ifstream file(config_file); + + bool CrossExists = false, CircleExists = false, SquareExists = false, TriangleExists = false, + L1Exists = false, L2Exists = false, L3Exists = false, R1Exists = false, R2Exists = false, + R3Exists = false, DPadUpExists = false, DPadDownExists = false, DPadLeftExists = false, + DPadRightExists = false, StartExists = false, BackExists = false, LStickXExists = false, + LStickYExists = false, RStickXExists = false, RStickYExists = false; + int lineCount = 0; + std::string line = ""; + while (std::getline(file, line)) { + lineCount++; + + line.erase(std::remove(line.begin(), line.end(), ' '), line.end()); + if (line.empty()) + continue; + + std::size_t comment_pos = line.find('#'); + if (comment_pos != std::string::npos) + line = line.substr(0, comment_pos); + + std::size_t equal_pos = line.find('='); + if (equal_pos == std::string::npos) + continue; + + std::string output_string = line.substr(0, equal_pos); + std::string input_string = line.substr(equal_pos + 1); + + if (std::find(ControllerInputs.begin(), ControllerInputs.end(), input_string) != + ControllerInputs.end() || + output_string == "analog_deadzone") { + if (input_string == "cross") { + ui->ABox->setCurrentText(QString::fromStdString(output_string)); + CrossExists = true; + } else if (input_string == "circle") { + ui->BBox->setCurrentText(QString::fromStdString(output_string)); + CircleExists = true; + } else if (input_string == "square") { + ui->XBox->setCurrentText(QString::fromStdString(output_string)); + SquareExists = true; + } else if (input_string == "triangle") { + ui->YBox->setCurrentText(QString::fromStdString(output_string)); + TriangleExists = true; + } else if (input_string == "l1") { + ui->LBBox->setCurrentText(QString::fromStdString(output_string)); + L1Exists = true; + } else if (input_string == "l2") { + ui->LTBox->setCurrentText(QString::fromStdString(output_string)); + L2Exists = true; + } else if (input_string == "r1") { + ui->RBBox->setCurrentText(QString::fromStdString(output_string)); + R1Exists = true; + } else if (input_string == "r2") { + ui->RTBox->setCurrentText(QString::fromStdString(output_string)); + R2Exists = true; + } else if (input_string == "l3") { + ui->LClickBox->setCurrentText(QString::fromStdString(output_string)); + L3Exists = true; + } else if (input_string == "r3") { + ui->RClickBox->setCurrentText(QString::fromStdString(output_string)); + R3Exists = true; + } else if (input_string == "pad_up") { + ui->DpadUpBox->setCurrentText(QString::fromStdString(output_string)); + DPadUpExists = true; + } else if (input_string == "pad_down") { + ui->DpadDownBox->setCurrentText(QString::fromStdString(output_string)); + DPadDownExists = true; + } else if (input_string == "pad_left") { + ui->DpadLeftBox->setCurrentText(QString::fromStdString(output_string)); + DPadLeftExists = true; + } else if (input_string == "pad_right") { + ui->DpadRightBox->setCurrentText(QString::fromStdString(output_string)); + DPadRightExists = true; + } else if (input_string == "options") { + ui->StartBox->setCurrentText(QString::fromStdString(output_string)); + StartExists = true; + } else if (input_string == "back") { + ui->BackBox->setCurrentText(QString::fromStdString(output_string)); + BackExists = true; + } else if (input_string == "axis_left_x") { + ui->LStickRightBox->setCurrentText(QString::fromStdString(output_string)); + ui->LStickLeftBox->setCurrentText(QString::fromStdString(output_string)); + LStickXExists = true; + } else if (input_string == "axis_left_y") { + ui->LStickUpBox->setCurrentText(QString::fromStdString(output_string)); + ui->LStickDownBox->setCurrentText(QString::fromStdString(output_string)); + LStickYExists = true; + } else if (input_string == "axis_right_x") { + ui->RStickRightBox->setCurrentText(QString::fromStdString(output_string)); + ui->RStickLeftBox->setCurrentText(QString::fromStdString(output_string)); + RStickXExists = true; + } else if (input_string == "axis_right_y") { + ui->RStickUpBox->setCurrentText(QString::fromStdString(output_string)); + ui->RStickDownBox->setCurrentText(QString::fromStdString(output_string)); + RStickYExists = true; + } else if (input_string.contains("leftjoystick")) { + std::size_t comma_pos = line.find(','); + int deadzonevalue = std::stoi(line.substr(comma_pos + 1)); + ui->LeftDeadzoneSlider->setValue(deadzonevalue); + ui->LeftDeadzoneValue->setText(QString::number(deadzonevalue)); + } else if (input_string.contains("rightjoystick")) { + std::size_t comma_pos = line.find(','); + int deadzonevalue = std::stoi(line.substr(comma_pos + 1)); + ui->RightDeadzoneSlider->setValue(deadzonevalue); + ui->RightDeadzoneValue->setText(QString::number(deadzonevalue)); + } + } + } + + // If an entry does not exist in the config file, we assume the user wants it unmapped + if (!CrossExists) + ui->ABox->setCurrentText("unmapped"); + if (!CircleExists) + ui->BBox->setCurrentText("unmapped"); + if (!SquareExists) + ui->XBox->setCurrentText("unmapped"); + if (!TriangleExists) + ui->YBox->setCurrentText("unmapped"); + if (!L1Exists) + ui->LBBox->setCurrentText("unmapped"); + if (!L2Exists) + ui->LTBox->setCurrentText("unmapped"); + if (!L3Exists) + ui->LClickBox->setCurrentText("unmapped"); + if (!R1Exists) + ui->RBBox->setCurrentText("unmapped"); + if (!R2Exists) + ui->RTBox->setCurrentText("unmapped"); + if (!R3Exists) + ui->RClickBox->setCurrentText("unmapped"); + if (!DPadUpExists) + ui->DpadUpBox->setCurrentText("unmapped"); + if (!DPadDownExists) + ui->DpadDownBox->setCurrentText("unmapped"); + if (!DPadLeftExists) + ui->DpadLeftBox->setCurrentText("unmapped"); + if (!DPadRightExists) + ui->DpadRightBox->setCurrentText("unmapped"); + if (!BackExists) + ui->BackBox->setCurrentText("unmapped"); + if (!StartExists) + ui->StartBox->setCurrentText("unmapped"); + + if (!LStickXExists) { + ui->LStickRightBox->setCurrentText("unmapped"); + ui->LStickLeftBox->setCurrentText("unmapped"); + } + if (!LStickYExists) { + ui->LStickUpBox->setCurrentText("unmapped"); + ui->LStickDownBox->setCurrentText("unmapped"); + } + if (!RStickXExists) { + ui->RStickRightBox->setCurrentText("unmapped"); + ui->RStickLeftBox->setCurrentText("unmapped"); + } + if (!RStickYExists) { + ui->RStickUpBox->setCurrentText("unmapped"); + ui->RStickDownBox->setCurrentText("unmapped"); + } + + file.close(); +} + +void ControlSettings::GetGameTitle() { + if (ui->ProfileComboBox->currentText() == "Common Config") { + ui->TitleLabel->setText("Common Config"); + } else { + for (int i = 0; i < m_game_info->m_games.size(); i++) { + if (m_game_info->m_games[i].serial == + ui->ProfileComboBox->currentText().toStdString()) { + ui->TitleLabel->setText(QString::fromStdString(m_game_info->m_games[i].name)); + } + } + } +} + +ControlSettings::~ControlSettings() {} diff --git a/src/qt_gui/control_settings.h b/src/qt_gui/control_settings.h new file mode 100644 index 000000000..04227f3a8 --- /dev/null +++ b/src/qt_gui/control_settings.h @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include "game_info.h" + +namespace Ui { +class ControlSettings; +} + +class ControlSettings : public QDialog { + Q_OBJECT +public: + explicit ControlSettings(std::shared_ptr game_info_get, + QWidget* parent = nullptr); + ~ControlSettings(); + +private Q_SLOTS: + void SaveControllerConfig(bool CloseOnSave); + void SetDefault(); + +private: + std::unique_ptr ui; + std::shared_ptr m_game_info; + + void AddBoxItems(); + void SetUIValuestoMappings(); + void GetGameTitle(); + + const std::vector ControllerInputs = { + "cross", "circle", "square", "triangle", "l1", + "r1", "l2", "r2", "l3", + + "r3", "options", "pad_up", + + "pad_down", + + "pad_left", "pad_right", "axis_left_x", "axis_left_y", "axis_right_x", + "axis_right_y", "back"}; + + const QStringList ButtonOutputs = {"cross", "circle", "square", "triangle", "l1", + "r1", "l2", "r2", "l3", + + "r3", "options", "pad_up", + + "pad_down", + + "pad_left", "pad_right", "touchpad", "unmapped"}; + + const QStringList StickOutputs = {"axis_left_x", "axis_left_y", "axis_right_x", "axis_right_y", + "unmapped"}; +}; diff --git a/src/qt_gui/control_settings.ui b/src/qt_gui/control_settings.ui new file mode 100644 index 000000000..b6acb5ca9 --- /dev/null +++ b/src/qt_gui/control_settings.ui @@ -0,0 +1,1379 @@ + + + + ControlSettings + + + Qt::WindowModality::WindowModal + + + + 0 + 0 + 1012 + 721 + + + + Configure Controls + + + + :/rpcs3.ico:/rpcs3.ico + + + + + + QFrame::Shape::NoFrame + + + 0 + + + true + + + + + 0 + 0 + 994 + 673 + + + + + Control Settings + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + + 5 + + + + + true + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + D-Pad + + + + 6 + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 124 + 0 + + + + + 0 + 16777215 + + + + Up + + + + + + false + + + QComboBox::SizeAdjustPolicy::AdjustToContents + + + + + + + + + + + + + + + Left + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + + + + + + Right + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + false + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 124 + 16777215 + + + + Down + + + + + + true + + + + 0 + 0 + + + + + 0 + 0 + + + + + + + + + + + + + + + + + Qt::Orientation::Vertical + + + QSizePolicy::Policy::Maximum + + + + 20 + 40 + + + + + + + + + 0 + 0 + + + + Left Stick Deadzone (def:2 max:127) + + + + + + + 0 + 0 + + + + Left Deadzone + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + + 0 + 0 + + + + 1 + + + 127 + + + Qt::Orientation::Horizontal + + + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + Left Stick + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + 16777215 + 2121 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 124 + 16777215 + + + + Up + + + + + + true + + + + + + + + + + + + + + + Left + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + true + + + + + + + + + + + 179 + 16777215 + + + + Right + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + true + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 124 + 0 + + + + + 124 + 21212 + + + + Down + + + + + + true + + + false + + + false + + + + + + + + + + + + + + + + + + 0 + + + + + + 0 + 0 + + + + + 12 + true + + + + Config Selection + + + Qt::AlignmentFlag::AlignCenter + + + + + + + + + 9 + false + + + + + + + -1 + + + Common Config + + + + + + + + 10 + true + + + + Common Config + + + Qt::AlignmentFlag::AlignCenter + + + true + + + + + + + + + + 0 + 0 + + + + + 9 + false + + + + Use per-game configs + + + + + + + + + + 0 + + + + + + + L1 / LB + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + + + + + + L2 / LT + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + + + + + + + + + + 10 + + + + + + true + + + + KBM Controls + + + + + + + true + + + + KBM Editor + + + + + + + + + + Back + + + + + + + + + + + + + + + + + + R1 / RB + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + + + + + + R2 / RT + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + + + + + + + + + + + 0 + 200 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 415 + 256 + + + + :/images/ps4_controller.png + + + true + + + Qt::AlignmentFlag::AlignBottom|Qt::AlignmentFlag::AlignHCenter + + + + + + + + + + 10 + + + QLayout::SizeConstraint::SetDefaultConstraint + + + + + L3 + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + + + + + + Options / Start + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + + + + + + R3 + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + + + + + + + + + + 5 + + + + + + 0 + 0 + + + + Face Buttons + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 124 + 0 + + + + + 0 + 16777215 + + + + Triangle / Y + + + + + + true + + + + 0 + 0 + + + + + + + + + + + + + + + + Square / X + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + + + + + + Circle / B + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 124 + 0 + + + + + 124 + 16777215 + + + + Cross / A + + + + + + + + + + + + + + + + + + Qt::Orientation::Vertical + + + QSizePolicy::Policy::Maximum + + + + 20 + 40 + + + + + + + + + 0 + 0 + + + + Right Stick Deadzone (def:2, max:127) + + + + + + + 0 + 0 + + + + Right Deadzone + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + + 0 + 0 + + + + 1 + + + 127 + + + Qt::Orientation::Horizontal + + + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + Right Stick + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 124 + 1231321 + + + + Up + + + + + + true + + + + + + + + + + + + + + + Left + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + true + + + + + + + + + + Right + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 124 + 0 + + + + + 124 + 2121 + + + + Down + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + QDialogButtonBox::StandardButton::Apply|QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::RestoreDefaults|QDialogButtonBox::StandardButton::Save + + + false + + + + + + + + + + diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp index 4a6cb9103..0ab9d3a42 100644 --- a/src/qt_gui/main_window.cpp +++ b/src/qt_gui/main_window.cpp @@ -16,6 +16,7 @@ #include "common/scm_rev.h" #include "common/string_util.h" #include "common/version.h" +#include "control_settings.h" #include "core/file_format/pkg.h" #include "core/loader.h" #include "game_install_dialog.h" @@ -301,8 +302,8 @@ void MainWindow::CreateConnects() { // this is the editor for kbm keybinds connect(ui->controllerButton, &QPushButton::clicked, this, [this]() { - EditorDialog* editorWindow = new EditorDialog(this); - editorWindow->exec(); // Show the editor window modally + auto configWindow = new ControlSettings(m_game_info, this); + configWindow->exec(); }); #ifdef ENABLE_UPDATER diff --git a/src/shadps4.qrc b/src/shadps4.qrc index 30f234ed8..40aeb9fb9 100644 --- a/src/shadps4.qrc +++ b/src/shadps4.qrc @@ -30,5 +30,6 @@ images/ko-fi.png images/youtube.png images/website.png + images/ps4_controller.png From f8f732e78cd73899580c0b393c347e97a43bfa3b Mon Sep 17 00:00:00 2001 From: makigumo Date: Tue, 4 Feb 2025 07:51:07 +0100 Subject: [PATCH 14/20] fix ASSERT_MSG arguments (#2337) --- .../ir/passes/flatten_extended_userdata_pass.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shader_recompiler/ir/passes/flatten_extended_userdata_pass.cpp b/src/shader_recompiler/ir/passes/flatten_extended_userdata_pass.cpp index ef9319891..bbf3fe8fb 100644 --- a/src/shader_recompiler/ir/passes/flatten_extended_userdata_pass.cpp +++ b/src/shader_recompiler/ir/passes/flatten_extended_userdata_pass.cpp @@ -219,7 +219,7 @@ void FlattenExtendedUserdataPass(IR::Program& program) { }; auto base0 = IR::BreadthFirstSearch(ptr_composite->Arg(0), pred); auto base1 = IR::BreadthFirstSearch(ptr_composite->Arg(1), pred); - ASSERT_MSG(base0 && base1 && "ReadConst not from constant memory"); + ASSERT_MSG(base0 && base1, "ReadConst not from constant memory"); IR::Inst* ptr_lo = base0.value(); ptr_lo = pass_info.DeduplicateInstruction(ptr_lo); @@ -250,4 +250,4 @@ void FlattenExtendedUserdataPass(IR::Program& program) { info.RefreshFlatBuf(); } -} // namespace Shader::Optimization \ No newline at end of file +} // namespace Shader::Optimization From fffd3736529578eea87cf756ba01afc90c52f5f9 Mon Sep 17 00:00:00 2001 From: makigumo Date: Tue, 4 Feb 2025 08:24:56 +0100 Subject: [PATCH 15/20] Fix shader type names (#2336) Names didn't match definition in type.h --- src/shader_recompiler/ir/type.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/shader_recompiler/ir/type.cpp b/src/shader_recompiler/ir/type.cpp index 08157f108..74c56740c 100644 --- a/src/shader_recompiler/ir/type.cpp +++ b/src/shader_recompiler/ir/type.cpp @@ -9,9 +9,10 @@ namespace Shader::IR { std::string NameOf(Type type) { static constexpr std::array names{ - "Opaque", "Label", "Reg", "Pred", "Attribute", "U1", "U8", "U16", "U32", - "U64", "F16", "F32", "F64", "U32x2", "U32x3", "U32x4", "F16x2", "F16x3", - "F16x4", "F32x2", "F32x3", "F32x4", "F64x2", "F64x3", "F64x4", "StringLiteral"}; + "Opaque", "ScalarReg", "VectorReg", "Attribute", "Patch", "U1", "U8", + "U16", "U32", "U64", "F16", "F32", "F64", "U32x2", + "U32x3", "U32x4", "F16x2", "F16x3", "F16x4", "F32x2", "F32x3", + "F32x4", "F64x2", "F64x3", "F64x4", "StringLiteral"}; const size_t bits{static_cast(type)}; if (bits == 0) { return "Void"; From e4598e882116a9f8c06ce4a32cb2689ea08e4b16 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Tue, 4 Feb 2025 09:27:48 +0200 Subject: [PATCH 16/20] sceVideoOutDeleteFlipEvent (#2339) --- src/core/libraries/videoout/video_out.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/core/libraries/videoout/video_out.cpp b/src/core/libraries/videoout/video_out.cpp index 27a3fe082..65713019c 100644 --- a/src/core/libraries/videoout/video_out.cpp +++ b/src/core/libraries/videoout/video_out.cpp @@ -63,6 +63,20 @@ s32 PS4_SYSV_ABI sceVideoOutAddFlipEvent(Kernel::SceKernelEqueue eq, s32 handle, return ORBIS_OK; } +s32 PS4_SYSV_ABI sceVideoOutDeleteFlipEvent(Kernel::SceKernelEqueue eq, s32 handle) { + auto* port = driver->GetPort(handle); + if (port == nullptr) { + return ORBIS_VIDEO_OUT_ERROR_INVALID_HANDLE; + } + + if (eq == nullptr) { + return ORBIS_VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE; + } + eq->RemoveEvent(handle, Kernel::SceKernelEvent::Filter::VideoOut); + port->flip_events.erase(find(port->flip_events.begin(), port->flip_events.end(), eq)); + return ORBIS_OK; +} + s32 PS4_SYSV_ABI sceVideoOutAddVblankEvent(Kernel::SceKernelEqueue eq, s32 handle, void* udata) { LOG_INFO(Lib_VideoOut, "handle = {}", handle); @@ -374,6 +388,8 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) { sceVideoOutColorSettingsSetGamma); LIB_FUNCTION("pv9CI5VC+R0", "libSceVideoOut", 1, "libSceVideoOut", 0, 0, sceVideoOutAdjustColor); + LIB_FUNCTION("-Ozn0F1AFRg", "libSceVideoOut", 1, "libSceVideoOut", 0, 0, + sceVideoOutDeleteFlipEvent); // openOrbis appears to have libSceVideoOut_v1 module libSceVideoOut_v1.1 LIB_FUNCTION("Up36PTk687E", "libSceVideoOut", 1, "libSceVideoOut", 1, 1, sceVideoOutOpen); From 363604c6f0c4429877a8c1a55e88eb98538930ed Mon Sep 17 00:00:00 2001 From: Kolja Date: Tue, 4 Feb 2025 08:28:25 +0100 Subject: [PATCH 17/20] Add emulator category (#2320) --- dist/net.shadps4.shadPS4.desktop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/net.shadps4.shadPS4.desktop b/dist/net.shadps4.shadPS4.desktop index fbefa0566..a87829e7b 100644 --- a/dist/net.shadps4.shadPS4.desktop +++ b/dist/net.shadps4.shadPS4.desktop @@ -5,5 +5,5 @@ Terminal=false Type=Application Icon=net.shadps4.shadPS4 Comment=PlayStation 4 emulator -Categories=Game; +Categories=Game;Emulator; StartupWMClass=shadps4; From b6ad512e344c5de064d630691436548a402a37c0 Mon Sep 17 00:00:00 2001 From: pdaloxd <31321612+pablodrake@users.noreply.github.com> Date: Tue, 4 Feb 2025 08:33:38 +0100 Subject: [PATCH 18/20] Change Background Image for games (#2334) * Added opacity change instead of blur for background image * Fixed integer overflow when refreshing grid list * Added slider to control background image opacity * Added show background image button * Added UI code for checkbox and English and Spanish translations for new UI elements * Removed background image caching * Background image update on apply/save * Only recompute image if opacity or game changes * Fixed segfault when trying to change opacity after table refresh * Placed background image settings under GUI in settings file --- src/common/config.cpp | 24 +++++++++ src/common/config.h | 4 ++ src/qt_gui/game_grid_frame.cpp | 92 ++++++++++++++++++++------------ src/qt_gui/game_grid_frame.h | 2 + src/qt_gui/game_list_frame.cpp | 51 ++++++++++-------- src/qt_gui/game_list_frame.h | 5 +- src/qt_gui/game_list_utils.h | 26 +++++++++ src/qt_gui/main_window.cpp | 17 ++++++ src/qt_gui/settings_dialog.cpp | 11 ++++ src/qt_gui/settings_dialog.h | 1 + src/qt_gui/settings_dialog.ui | 70 ++++++++++++++++++++++++ src/qt_gui/translations/en.ts | 16 ++++++ src/qt_gui/translations/es_ES.ts | 16 ++++++ 13 files changed, 279 insertions(+), 56 deletions(-) diff --git a/src/common/config.cpp b/src/common/config.cpp index 2059da0b3..d9dfb861f 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -95,6 +95,8 @@ std::vector m_pkg_viewer; std::vector m_elf_viewer; std::vector m_recent_files; std::string emulator_language = "en"; +static int backgroundImageOpacity = 50; +static bool showBackgroundImage = true; // Language u32 m_language = 1; // english @@ -611,6 +613,22 @@ u32 GetLanguage() { return m_language; } +int getBackgroundImageOpacity() { + return backgroundImageOpacity; +} + +void setBackgroundImageOpacity(int opacity) { + backgroundImageOpacity = std::clamp(opacity, 0, 100); +} + +bool getShowBackgroundImage() { + return showBackgroundImage; +} + +void setShowBackgroundImage(bool show) { + showBackgroundImage = show; +} + void load(const std::filesystem::path& path) { // If the configuration file does not exist, create it and return std::error_code error; @@ -731,6 +749,8 @@ void load(const std::filesystem::path& path) { m_recent_files = toml::find_or>(gui, "recentFiles", {}); m_table_mode = toml::find_or(gui, "gameTableMode", 0); emulator_language = toml::find_or(gui, "emulatorLanguage", "en"); + backgroundImageOpacity = toml::find_or(gui, "backgroundImageOpacity", 50); + showBackgroundImage = toml::find_or(gui, "showBackgroundImage", true); } if (data.contains("Settings")) { @@ -821,6 +841,8 @@ void save(const std::filesystem::path& path) { data["GUI"]["addonInstallDir"] = std::string{fmt::UTF(settings_addon_install_dir.u8string()).data}; data["GUI"]["emulatorLanguage"] = emulator_language; + data["GUI"]["backgroundImageOpacity"] = backgroundImageOpacity; + data["GUI"]["showBackgroundImage"] = showBackgroundImage; data["Settings"]["consoleLanguage"] = m_language; std::ofstream file(path, std::ios::binary); @@ -914,6 +936,8 @@ void setDefaultValues() { separateupdatefolder = false; compatibilityData = false; checkCompatibilityOnStartup = false; + backgroundImageOpacity = 50; + showBackgroundImage = true; } constexpr std::string_view GetDefaultKeyboardConfig() { diff --git a/src/common/config.h b/src/common/config.h index 77ed69ece..69e497527 100644 --- a/src/common/config.h +++ b/src/common/config.h @@ -30,6 +30,8 @@ bool getEnableDiscordRPC(); bool getSeparateUpdateEnabled(); bool getCompatibilityEnabled(); bool getCheckCompatibilityOnStartup(); +int getBackgroundImageOpacity(); +bool getShowBackgroundImage(); std::string getLogFilter(); std::string getLogType(); @@ -88,6 +90,8 @@ void setGameInstallDirs(const std::vector& settings_insta void setSaveDataPath(const std::filesystem::path& path); void setCompatibilityEnabled(bool use); void setCheckCompatibilityOnStartup(bool use); +void setBackgroundImageOpacity(int opacity); +void setShowBackgroundImage(bool show); void setCursorState(s16 cursorState); void setCursorHideTimeout(int newcursorHideTimeout); diff --git a/src/qt_gui/game_grid_frame.cpp b/src/qt_gui/game_grid_frame.cpp index d719ac878..2db4b7e4e 100644 --- a/src/qt_gui/game_grid_frame.cpp +++ b/src/qt_gui/game_grid_frame.cpp @@ -38,17 +38,34 @@ GameGridFrame::GameGridFrame(std::shared_ptr game_info_get, void GameGridFrame::onCurrentCellChanged(int currentRow, int currentColumn, int previousRow, int previousColumn) { - crtRow = currentRow; - crtColumn = currentColumn; - columnCnt = this->columnCount(); - - auto itemID = (crtRow * columnCnt) + currentColumn; - if (itemID > m_game_info->m_games.count() - 1) { + // Early exit for invalid indices + if (currentRow < 0 || currentColumn < 0) { cellClicked = false; validCellSelected = false; BackgroundMusicPlayer::getInstance().stopMusic(); return; } + + crtRow = currentRow; + crtColumn = currentColumn; + columnCnt = this->columnCount(); + + // Prevent integer overflow + if (columnCnt <= 0 || crtRow > (std::numeric_limits::max() / columnCnt)) { + cellClicked = false; + validCellSelected = false; + BackgroundMusicPlayer::getInstance().stopMusic(); + return; + } + + auto itemID = (crtRow * columnCnt) + currentColumn; + if (itemID < 0 || itemID > m_game_info->m_games.count() - 1) { + cellClicked = false; + validCellSelected = false; + BackgroundMusicPlayer::getInstance().stopMusic(); + return; + } + cellClicked = true; validCellSelected = true; SetGridBackgroundImage(crtRow, crtColumn); @@ -65,6 +82,8 @@ void GameGridFrame::PlayBackgroundMusic(QString path) { } void GameGridFrame::PopulateGameGrid(QVector m_games_search, bool fromSearch) { + this->crtRow = -1; + this->crtColumn = -1; QVector m_games_; this->clearContents(); if (fromSearch) @@ -136,43 +155,48 @@ void GameGridFrame::PopulateGameGrid(QVector m_games_search, bool from } void GameGridFrame::SetGridBackgroundImage(int row, int column) { - int itemID = (row * this->columnCount()) + column; QWidget* item = this->cellWidget(row, column); - if (item) { - QString pic1Path; - Common::FS::PathToQString(pic1Path, (*m_games_shared)[itemID].pic_path); - const auto blurredPic1Path = Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / - (*m_games_shared)[itemID].serial / "pic1.png"; - QString blurredPic1PathQt; - Common::FS::PathToQString(blurredPic1PathQt, blurredPic1Path); - - backgroundImage = QImage(blurredPic1PathQt); - if (backgroundImage.isNull()) { - QImage image(pic1Path); - backgroundImage = m_game_list_utils.BlurImage(image, image.rect(), 16); - - std::filesystem::path img_path = - Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / - (*m_games_shared)[itemID].serial; - std::filesystem::create_directories(img_path); - if (!backgroundImage.save(blurredPic1PathQt, "PNG")) { - // qDebug() << "Error: Unable to save image."; - } - } - RefreshGridBackgroundImage(); + if (!item) { + // handle case where no item was clicked + return; } + + // If background images are hidden, clear the background image + if (!Config::getShowBackgroundImage()) { + backgroundImage = QImage(); + m_last_opacity = -1; // Reset opacity tracking when disabled + m_current_game_path.clear(); // Reset current game path + RefreshGridBackgroundImage(); + return; + } + + const auto& game = (*m_games_shared)[itemID]; + const int opacity = Config::getBackgroundImageOpacity(); + + // Recompute if opacity changed or we switched to a different game + if (opacity != m_last_opacity || game.pic_path != m_current_game_path) { + QImage original_image(QString::fromStdString(game.pic_path.string())); + if (!original_image.isNull()) { + backgroundImage = m_game_list_utils.ChangeImageOpacity( + original_image, original_image.rect(), opacity / 100.0f); + m_last_opacity = opacity; + m_current_game_path = game.pic_path; + } + } + + RefreshGridBackgroundImage(); } void GameGridFrame::RefreshGridBackgroundImage() { - if (!backgroundImage.isNull()) { - QPalette palette; + QPalette palette; + if (!backgroundImage.isNull() && Config::getShowBackgroundImage()) { palette.setBrush(QPalette::Base, QBrush(backgroundImage.scaled(size(), Qt::IgnoreAspectRatio))); - QColor transparentColor = QColor(135, 206, 235, 40); - palette.setColor(QPalette::Highlight, transparentColor); - this->setPalette(palette); } + QColor transparentColor = QColor(135, 206, 235, 40); + palette.setColor(QPalette::Highlight, transparentColor); + this->setPalette(palette); } bool GameGridFrame::IsValidCellSelected() { diff --git a/src/qt_gui/game_grid_frame.h b/src/qt_gui/game_grid_frame.h index 4825d6daf..370b71dcb 100644 --- a/src/qt_gui/game_grid_frame.h +++ b/src/qt_gui/game_grid_frame.h @@ -33,6 +33,8 @@ private: std::shared_ptr m_compat_info; std::shared_ptr> m_games_shared; bool validCellSelected = false; + int m_last_opacity = -1; // Track last opacity to avoid unnecessary recomputation + std::filesystem::path m_current_game_path; // Track current game path to detect changes public: explicit GameGridFrame(std::shared_ptr game_info_get, diff --git a/src/qt_gui/game_list_frame.cpp b/src/qt_gui/game_list_frame.cpp index f2d08f578..64c0f17ba 100644 --- a/src/qt_gui/game_list_frame.cpp +++ b/src/qt_gui/game_list_frame.cpp @@ -89,6 +89,7 @@ void GameListFrame::onCurrentCellChanged(int currentRow, int currentColumn, int if (!item) { return; } + m_current_item = item; // Store current item SetListBackgroundImage(item); PlayBackgroundMusic(item); } @@ -104,6 +105,7 @@ void GameListFrame::PlayBackgroundMusic(QTableWidgetItem* item) { } void GameListFrame::PopulateGameList(bool isInitialPopulation) { + this->m_current_item = nullptr; // Do not show status column if it is not enabled this->setColumnHidden(2, !Config::getCompatibilityEnabled()); this->setColumnHidden(6, !Config::GetLoadGameSizeEnabled()); @@ -167,38 +169,41 @@ void GameListFrame::SetListBackgroundImage(QTableWidgetItem* item) { return; } - QString pic1Path; - Common::FS::PathToQString(pic1Path, m_game_info->m_games[item->row()].pic_path); - const auto blurredPic1Path = Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / - m_game_info->m_games[item->row()].serial / "pic1.png"; - QString blurredPic1PathQt; - Common::FS::PathToQString(blurredPic1PathQt, blurredPic1Path); + // If background images are hidden, clear the background image + if (!Config::getShowBackgroundImage()) { + backgroundImage = QImage(); + m_last_opacity = -1; // Reset opacity tracking when disabled + m_current_game_path.clear(); // Reset current game path + RefreshListBackgroundImage(); + return; + } - backgroundImage = QImage(blurredPic1PathQt); - if (backgroundImage.isNull()) { - QImage image(pic1Path); - backgroundImage = m_game_list_utils.BlurImage(image, image.rect(), 16); + const auto& game = m_game_info->m_games[item->row()]; + const int opacity = Config::getBackgroundImageOpacity(); - std::filesystem::path img_path = - Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / - m_game_info->m_games[item->row()].serial; - std::filesystem::create_directories(img_path); - if (!backgroundImage.save(blurredPic1PathQt, "PNG")) { - // qDebug() << "Error: Unable to save image."; + // Recompute if opacity changed or we switched to a different game + if (opacity != m_last_opacity || game.pic_path != m_current_game_path) { + QImage original_image(QString::fromStdString(game.pic_path.string())); + if (!original_image.isNull()) { + backgroundImage = m_game_list_utils.ChangeImageOpacity( + original_image, original_image.rect(), opacity / 100.0f); + m_last_opacity = opacity; + m_current_game_path = game.pic_path; } } + RefreshListBackgroundImage(); } void GameListFrame::RefreshListBackgroundImage() { - if (!backgroundImage.isNull()) { - QPalette palette; + QPalette palette; + if (!backgroundImage.isNull() && Config::getShowBackgroundImage()) { palette.setBrush(QPalette::Base, QBrush(backgroundImage.scaled(size(), Qt::IgnoreAspectRatio))); - QColor transparentColor = QColor(135, 206, 235, 40); - palette.setColor(QPalette::Highlight, transparentColor); - this->setPalette(palette); } + QColor transparentColor = QColor(135, 206, 235, 40); + palette.setColor(QPalette::Highlight, transparentColor); + this->setPalette(palette); } void GameListFrame::SortNameAscending(int columnIndex) { @@ -392,3 +397,7 @@ QString GameListFrame::GetPlayTime(const std::string& serial) { file.close(); return playTime; } + +QTableWidgetItem* GameListFrame::GetCurrentItem() { + return m_current_item; +} \ No newline at end of file diff --git a/src/qt_gui/game_list_frame.h b/src/qt_gui/game_list_frame.h index 7e37c4ea7..b2e5f1e2f 100644 --- a/src/qt_gui/game_list_frame.h +++ b/src/qt_gui/game_list_frame.h @@ -44,11 +44,14 @@ private: QList m_columnActs; GameInfoClass* game_inf_get = nullptr; bool ListSortedAsc = true; + QTableWidgetItem* m_current_item = nullptr; + int m_last_opacity = -1; // Track last opacity to avoid unnecessary recomputation + std::filesystem::path m_current_game_path; // Track current game path to detect changes public: void PopulateGameList(bool isInitialPopulation = true); void ResizeIcons(int iconSize); - + QTableWidgetItem* GetCurrentItem(); QImage backgroundImage; GameListUtils m_game_list_utils; GuiContextMenus m_gui_context_menus; diff --git a/src/qt_gui/game_list_utils.h b/src/qt_gui/game_list_utils.h index 581a8a55f..c6b69e70e 100644 --- a/src/qt_gui/game_list_utils.h +++ b/src/qt_gui/game_list_utils.h @@ -201,4 +201,30 @@ public: return result; } + + // Opacity is a float between 0 and 1 + static QImage ChangeImageOpacity(const QImage& image, const QRect& rect, float opacity) { + // Convert to ARGB32 format to ensure alpha channel support + QImage result = image.convertToFormat(QImage::Format_ARGB32); + + // Ensure opacity is between 0 and 1 + opacity = std::clamp(opacity, 0.0f, 1.0f); + + // Convert opacity to integer alpha value (0-255) + int alpha = static_cast(opacity * 255); + + // Process only the specified rectangle area + for (int y = rect.top(); y <= rect.bottom(); ++y) { + QRgb* line = reinterpret_cast(result.scanLine(y)); + for (int x = rect.left(); x <= rect.right(); ++x) { + // Get current pixel + QRgb pixel = line[x]; + // Keep RGB values, but modify alpha while preserving relative transparency + int newAlpha = (qAlpha(pixel) * alpha) / 255; + line[x] = qRgba(qRed(pixel), qGreen(pixel), qBlue(pixel), newAlpha); + } + } + + return result; + } }; diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp index 0ab9d3a42..67615a1b6 100644 --- a/src/qt_gui/main_window.cpp +++ b/src/qt_gui/main_window.cpp @@ -297,6 +297,23 @@ void MainWindow::CreateConnects() { connect(settingsDialog, &SettingsDialog::CompatibilityChanged, this, &MainWindow::RefreshGameTable); + connect(settingsDialog, &SettingsDialog::BackgroundOpacityChanged, this, + [this](int opacity) { + Config::setBackgroundImageOpacity(opacity); + if (m_game_list_frame) { + QTableWidgetItem* current = m_game_list_frame->GetCurrentItem(); + if (current) { + m_game_list_frame->SetListBackgroundImage(current); + } + } + if (m_game_grid_frame) { + if (m_game_grid_frame->IsValidCellSelected()) { + m_game_grid_frame->SetGridBackgroundImage(m_game_grid_frame->crtRow, + m_game_grid_frame->crtColumn); + } + } + }); + settingsDialog->exec(); }); diff --git a/src/qt_gui/settings_dialog.cpp b/src/qt_gui/settings_dialog.cpp index 7505db106..8f4b22c6d 100644 --- a/src/qt_gui/settings_dialog.cpp +++ b/src/qt_gui/settings_dialog.cpp @@ -173,6 +173,9 @@ SettingsDialog::SettingsDialog(std::span physical_devices, { connect(ui->chooseHomeTabComboBox, &QComboBox::currentTextChanged, this, [](const QString& hometab) { Config::setChooseHomeTab(hometab.toStdString()); }); + + connect(ui->showBackgroundImageCheckBox, &QCheckBox::stateChanged, this, + [](int state) { Config::setShowBackgroundImage(state == Qt::Checked); }); } // Input TAB { @@ -251,6 +254,7 @@ SettingsDialog::SettingsDialog(std::span physical_devices, #ifdef ENABLE_UPDATER ui->updaterGroupBox->installEventFilter(this); #endif + ui->GUIBackgroundImageGroupBox->installEventFilter(this); ui->GUIMusicGroupBox->installEventFilter(this); ui->disableTrophycheckBox->installEventFilter(this); ui->enableCompatibilityCheckBox->installEventFilter(this); @@ -410,6 +414,8 @@ void SettingsDialog::LoadValuesFromConfig() { ui->removeFolderButton->setEnabled(!ui->gameFoldersListWidget->selectedItems().isEmpty()); ResetInstallFolders(); + ui->backgroundImageOpacitySlider->setValue(Config::getBackgroundImageOpacity()); + ui->showBackgroundImageCheckBox->setChecked(Config::getShowBackgroundImage()); } void SettingsDialog::InitializeEmulatorLanguages() { @@ -504,6 +510,8 @@ void SettingsDialog::updateNoteTextEdit(const QString& elementName) { } else if (elementName == "updaterGroupBox") { text = tr("updaterGroupBox"); #endif + } else if (elementName == "GUIBackgroundImageGroupBox") { + text = tr("GUIBackgroundImageGroupBox"); } else if (elementName == "GUIMusicGroupBox") { text = tr("GUIMusicGroupBox"); } else if (elementName == "disableTrophycheckBox") { @@ -638,6 +646,9 @@ void SettingsDialog::UpdateSettings() { Config::setChooseHomeTab(ui->chooseHomeTabComboBox->currentText().toStdString()); Config::setCompatibilityEnabled(ui->enableCompatibilityCheckBox->isChecked()); Config::setCheckCompatibilityOnStartup(ui->checkCompatibilityOnStartupCheckBox->isChecked()); + Config::setBackgroundImageOpacity(ui->backgroundImageOpacitySlider->value()); + emit BackgroundOpacityChanged(ui->backgroundImageOpacitySlider->value()); + Config::setShowBackgroundImage(ui->showBackgroundImageCheckBox->isChecked()); #ifdef ENABLE_DISCORD_RPC auto* rpc = Common::Singleton::Instance(); diff --git a/src/qt_gui/settings_dialog.h b/src/qt_gui/settings_dialog.h index 892e67671..c440351f6 100644 --- a/src/qt_gui/settings_dialog.h +++ b/src/qt_gui/settings_dialog.h @@ -33,6 +33,7 @@ public: signals: void LanguageChanged(const std::string& locale); void CompatibilityChanged(); + void BackgroundOpacityChanged(int opacity); private: void LoadValuesFromConfig(); diff --git a/src/qt_gui/settings_dialog.ui b/src/qt_gui/settings_dialog.ui index d15f49efe..80f7a117e 100644 --- a/src/qt_gui/settings_dialog.ui +++ b/src/qt_gui/settings_dialog.ui @@ -583,6 +583,76 @@ + + + + Background Image + + + + 0 + 0 + + + + + + + + 0 + 0 + + + + Show Background Image + + + + + + + 9 + + + + + + 0 + 0 + + + + Opacity + + + + + + + + 0 + 0 + + + + 0 + + + 100 + + + 50 + + + Qt::Orientation::Horizontal + + + + + + + + diff --git a/src/qt_gui/translations/en.ts b/src/qt_gui/translations/en.ts index afaa17520..d0540d7cd 100644 --- a/src/qt_gui/translations/en.ts +++ b/src/qt_gui/translations/en.ts @@ -757,6 +757,18 @@ Disable Trophy Pop-ups Disable Trophy Pop-ups + + Background Image + Background Image + + + Show Background Image + Show Background Image + + + Opacity + Opacity + Play title music Play title music @@ -853,6 +865,10 @@ updaterGroupBox Update:\nRelease: Official versions released every month that may be very outdated, but are more reliable and tested.\nNightly: Development versions that have all the latest features and fixes, but may contain bugs and are less stable. + + GUIBackgroundImageGroupBox + Background Image:\nControl the opacity of the game background image. + GUIMusicGroupBox Play Title Music:\nIf a game supports it, enable playing special music when selecting the game in the GUI. diff --git a/src/qt_gui/translations/es_ES.ts b/src/qt_gui/translations/es_ES.ts index d732e67ea..772980994 100644 --- a/src/qt_gui/translations/es_ES.ts +++ b/src/qt_gui/translations/es_ES.ts @@ -748,6 +748,18 @@ Disable Trophy Pop-ups Disable Trophy Pop-ups + + Background Image + Imagen de fondo + + + Show Background Image + Mostrar Imagen de Fondo + + + Opacity + Opacidad + Play title music Reproducir la música de apertura @@ -844,6 +856,10 @@ updaterGroupBox Actualización:\nRelease: Versiones oficiales lanzadas cada mes que pueden estar muy desactualizadas, pero son más confiables y están probadas.\nNightly: Versiones de desarrollo que tienen todas las últimas funciones y correcciones, pero pueden contener errores y son menos estables. + + GUIBackgroundImageGroupBox + Imagen de fondo:\nControle la opacidad de la imagen de fondo del juego. + GUIMusicGroupBox Reproducir Música del Título:\nSi un juego lo admite, habilita la reproducción de música especial al seleccionar el juego en la interfaz gráfica. From b879dd59c674b9dae0bf1c4c090cabdce4521d05 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Tue, 4 Feb 2025 01:01:59 -0800 Subject: [PATCH 19/20] shader_recompiler: Add workaround for drivers with unexpected unorm rounding behavior. (#2310) --- src/shader_recompiler/frontend/translate/export.cpp | 6 +++++- src/shader_recompiler/runtime_info.h | 1 + src/video_core/renderer_vulkan/vk_pipeline_cache.cpp | 11 +++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/shader_recompiler/frontend/translate/export.cpp b/src/shader_recompiler/frontend/translate/export.cpp index 84c2ee658..28c4685db 100644 --- a/src/shader_recompiler/frontend/translate/export.cpp +++ b/src/shader_recompiler/frontend/translate/export.cpp @@ -17,7 +17,11 @@ u32 SwizzleMrtComponent(const FragmentRuntimeInfo::PsColorBuffer& color_buffer, void Translator::ExportMrtValue(IR::Attribute attribute, u32 comp, const IR::F32& value, const FragmentRuntimeInfo::PsColorBuffer& color_buffer) { - const auto converted = ApplyWriteNumberConversion(ir, value, color_buffer.num_conversion); + auto converted = ApplyWriteNumberConversion(ir, value, color_buffer.num_conversion); + if (color_buffer.needs_unorm_fixup) { + // FIXME: Fix-up for GPUs where float-to-unorm rounding is off from expected. + converted = ir.FPSub(converted, ir.Imm32(1.f / 127500.f)); + } ir.SetAttribute(attribute, converted, comp); } diff --git a/src/shader_recompiler/runtime_info.h b/src/shader_recompiler/runtime_info.h index 138a707b3..d1ae2c09d 100644 --- a/src/shader_recompiler/runtime_info.h +++ b/src/shader_recompiler/runtime_info.h @@ -185,6 +185,7 @@ struct FragmentRuntimeInfo { AmdGpu::NumberConversion num_conversion; AmdGpu::CompMapping swizzle; AmdGpu::Liverpool::ShaderExportFormat export_format; + bool needs_unorm_fixup; auto operator<=>(const PsColorBuffer&) const noexcept = default; }; diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 629899a33..d8f6a08d0 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -330,6 +330,16 @@ bool PipelineCache::RefreshGraphicsKey() { continue; } + // Metal seems to have an issue where 8-bit unorm/snorm/sRGB outputs to render target + // need a bias applied to round correctly; detect and set the flag for that here. + const auto needs_unorm_fixup = instance.GetDriverID() == vk::DriverId::eMoltenvk && + (col_buf.GetNumberFmt() == AmdGpu::NumberFormat::Unorm || + col_buf.GetNumberFmt() == AmdGpu::NumberFormat::Snorm || + col_buf.GetNumberFmt() == AmdGpu::NumberFormat::Srgb) && + (col_buf.GetDataFmt() == AmdGpu::DataFormat::Format8 || + col_buf.GetDataFmt() == AmdGpu::DataFormat::Format8_8 || + col_buf.GetDataFmt() == AmdGpu::DataFormat::Format8_8_8_8); + key.color_formats[remapped_cb] = LiverpoolToVK::SurfaceFormat(col_buf.GetDataFmt(), col_buf.GetNumberFmt()); key.color_buffers[remapped_cb] = { @@ -337,6 +347,7 @@ bool PipelineCache::RefreshGraphicsKey() { .num_conversion = col_buf.GetNumberConversion(), .swizzle = col_buf.Swizzle(), .export_format = regs.color_export_format.GetFormat(cb), + .needs_unorm_fixup = needs_unorm_fixup, }; } From 131b6f90e0a15ace346dcfe64189cb4e2363c5f5 Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Tue, 4 Feb 2025 12:37:23 +0100 Subject: [PATCH 20/20] Format log lines to make it possible to ctrl click on them and go to the log location (#2345) --- src/common/logging/text_formatter.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/logging/text_formatter.cpp b/src/common/logging/text_formatter.cpp index 5f6c2172d..b4fa204bc 100644 --- a/src/common/logging/text_formatter.cpp +++ b/src/common/logging/text_formatter.cpp @@ -23,8 +23,8 @@ std::string FormatLogMessage(const Entry& entry) { const char* class_name = GetLogClassName(entry.log_class); const char* level_name = GetLevelName(entry.log_level); - return fmt::format("[{}] <{}> {}:{}:{}: {}", class_name, level_name, entry.filename, - entry.function, entry.line_num, entry.message); + return fmt::format("[{}] <{}> {}:{} {}: {}", class_name, level_name, entry.filename, + entry.line_num, entry.function, entry.message); } void PrintMessage(const Entry& entry) {